UNPKG

recoder-code

Version:

Complete AI-powered development platform with ML model training, plugin registry, real-time collaboration, monitoring, infrastructure automation, and enterprise deployment capabilities

600 lines (508 loc) 18 kB
/** * StorageService * Handles package storage, retrieval, and management across multiple backends */ import { Config } from '../config'; import * as fs from 'fs-extra'; import * as path from 'path'; import * as crypto from 'crypto'; import { PackageVersion } from '../entities/PackageVersion'; export interface StorageConfig { type: 'local' | 's3' | 'gcs' | 'azure'; endpoint?: string; region?: string; bucket?: string; credentials?: any; basePath?: string; } export interface StorageMetadata { size: number; contentType: string; etag: string; lastModified: Date; checksums: { md5: string; sha1: string; sha256: string; sha512: string; }; } export interface UploadResult { success: boolean; key: string; url: string; metadata: StorageMetadata; error?: string; } export interface DownloadResult { success: boolean; data?: Buffer; metadata?: StorageMetadata; error?: string; } export class StorageService { private readonly config: StorageConfig; private readonly logger = { log: (message: string) => console.log(`[StorageService] ${message}`), warn: (message: string, error?: any) => console.warn(`[StorageService] ${message}`, error), error: (message: string, error?: any) => console.error(`[StorageService] ${message}`, error), debug: (message: string) => console.debug(`[StorageService] ${message}`) }; constructor(appConfig?: Config) { this.config = { type: 'local', basePath: '/tmp/packages' }; } async storeTarball(packageName: string, version: string, tarballBuffer: Buffer): Promise<string> { // Simple storage implementation - just return a path return `${packageName}-${version}.tgz`; } async getTarballStream(tarballPath: string): Promise<any> { // Simple implementation - return null for now return null; } async deleteTarball(tarballPath: string): Promise<void> { // Simple implementation - do nothing for now } async uploadPackage( packageName: string, version: string, data: Buffer, contentType: string = 'application/octet-stream' ): Promise<UploadResult> { try { const key = this.generatePackageKey(packageName, version); this.logger.log(`Uploading package ${packageName}@${version} to ${key}`); // Calculate checksums const metadata = await this.calculateMetadata(data, contentType); switch (this.config.type) { case 'local': return await this.uploadToLocal(key, data, metadata); case 's3': return await this.uploadToS3(key, data, metadata); case 'gcs': return await this.uploadToGCS(key, data, metadata); case 'azure': return await this.uploadToAzure(key, data, metadata); default: throw new Error(`Unsupported storage type: ${this.config.type}`); } } catch (error) { const err = error as Error; this.logger.error(`Failed to upload package ${packageName}@${version}: ${err.message}`, err.stack); return { success: false, key: '', url: '', metadata: { size: 0, contentType: '', etag: '', lastModified: new Date(0), checksums: { md5: '', sha1: '', sha256: '', sha512: '' } }, error: err.message }; } } async downloadPackage(packageName: string, version: string): Promise<DownloadResult> { try { const key = this.generatePackageKey(packageName, version); this.logger.debug(`Downloading package ${packageName}@${version} from ${key}`); switch (this.config.type) { case 'local': return await this.downloadFromLocal(key); case 's3': return await this.downloadFromS3(key); case 'gcs': return await this.downloadFromGCS(key); case 'azure': return await this.downloadFromAzure(key); default: throw new Error(`Unsupported storage type: ${this.config.type}`); } } catch (error) { const err = error as Error; this.logger.error(`Failed to download package ${packageName}@${version}: ${err.message}`, err.stack); return { success: false, error: err.message }; } } async deletePackage(packageName: string, version: string): Promise<boolean> { try { const key = this.generatePackageKey(packageName, version); this.logger.log(`Deleting package ${packageName}@${version} from ${key}`); switch (this.config.type) { case 'local': return await this.deleteFromLocal(key); case 's3': return await this.deleteFromS3(key); case 'gcs': return await this.deleteFromGCS(key); case 'azure': return await this.deleteFromAzure(key); default: throw new Error(`Unsupported storage type: ${this.config.type}`); } } catch (error) { const err = error as Error; this.logger.error(`Failed to delete package ${packageName}@${version}: ${err.message}`, err.stack); return false; } } async getPackageMetadata(packageName: string, version: string): Promise<StorageMetadata | null> { try { const key = this.generatePackageKey(packageName, version); switch (this.config.type) { case 'local': return await this.getLocalMetadata(key); case 's3': return await this.getS3Metadata(key); case 'gcs': return await this.getGCSMetadata(key); case 'azure': return await this.getAzureMetadata(key); default: throw new Error(`Unsupported storage type: ${this.config.type}`); } } catch (error) { const err = error as Error; this.logger.error(`Failed to get metadata for ${packageName}@${version}: ${err.message}`); return null; } } async packageExists(packageName: string, version: string): Promise<boolean> { const metadata = await this.getPackageMetadata(packageName, version); return metadata !== null; } async listPackageVersions(packageName: string): Promise<string[]> { try { const prefix = this.generatePackagePrefix(packageName); switch (this.config.type) { case 'local': return await this.listLocalVersions(prefix); case 's3': return await this.listS3Versions(prefix); case 'gcs': return await this.listGCSVersions(prefix); case 'azure': return await this.listAzureVersions(prefix); default: throw new Error(`Unsupported storage type: ${this.config.type}`); } } catch (error) { const err = error as Error; this.logger.error(`Failed to list versions for ${packageName}: ${err.message}`); return []; } } getDownloadUrl(packageName: string, version: string): string { const key = this.generatePackageKey(packageName, version); switch (this.config.type) { case 'local': return `/packages/${key}`; case 's3': return `https://${this.config.bucket}.s3.${this.config.region}.amazonaws.com/${key}`; case 'gcs': return `https://storage.googleapis.com/${this.config.bucket}/${key}`; case 'azure': return `https://${this.config.endpoint}/${this.config.bucket}/${key}`; default: return ''; } } private async initializeStorage(): Promise<void> { switch (this.config.type) { case 'local': await fs.ensureDir(this.config.basePath || '/tmp/packages'); break; case 's3': // Initialize S3 client await this.initializeS3(); break; case 'gcs': // Initialize GCS client await this.initializeGCS(); break; case 'azure': // Initialize Azure client await this.initializeAzure(); break; } } private generatePackageKey(packageName: string, version: string): string { // Handle scoped packages const normalizedName = packageName.replace('@', '').replace('/', '-'); return `${normalizedName}/${version}/${normalizedName}-${version}.tgz`; } private generatePackagePrefix(packageName: string): string { const normalizedName = packageName.replace('@', '').replace('/', '-'); return `${normalizedName}/`; } private async calculateMetadata(data: Buffer, contentType: string): Promise<StorageMetadata> { const md5 = crypto.createHash('md5').update(data).digest('hex'); const sha1 = crypto.createHash('sha1').update(data).digest('hex'); const sha256 = crypto.createHash('sha256').update(data).digest('hex'); const sha512 = crypto.createHash('sha512').update(data).digest('hex'); return { size: data.length, contentType, etag: md5, lastModified: new Date(), checksums: { md5, sha1, sha256, sha512 } }; } // Local Storage Implementation private async uploadToLocal(key: string, data: Buffer, metadata: StorageMetadata): Promise<UploadResult> { const filePath = path.join(this.config.basePath || '/tmp/packages', key); const dir = path.dirname(filePath); await fs.ensureDir(dir); await fs.writeFile(filePath, data); // Store metadata const metadataPath = filePath + '.meta.json'; await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2)); return { success: true, key, url: this.getDownloadUrl('', '').replace('//', '/') + key, metadata }; } private async downloadFromLocal(key: string): Promise<DownloadResult> { const filePath = path.join(this.config.basePath || '/tmp/packages', key); if (!await fs.pathExists(filePath)) { return { success: false, error: 'File not found' }; } const data = await fs.readFile(filePath); const metadata = await this.getLocalMetadata(key) || undefined; return { success: true, data, metadata }; } private async deleteFromLocal(key: string): Promise<boolean> { const filePath = path.join(this.config.basePath || '/tmp/packages', key); const metadataPath = filePath + '.meta.json'; await fs.remove(filePath); await fs.remove(metadataPath); return true; } private async getLocalMetadata(key: string): Promise<StorageMetadata | null> { const filePath = path.join(this.config.basePath || '/tmp/packages', key); const metadataPath = filePath + '.meta.json'; if (!await fs.pathExists(metadataPath)) { // Fallback: calculate metadata from file if (await fs.pathExists(filePath)) { const data = await fs.readFile(filePath); return this.calculateMetadata(data, 'application/octet-stream'); } return null; } const metadata = await fs.readJSON(metadataPath); return metadata; } private async listLocalVersions(prefix: string): Promise<string[]> { const basePath = path.join(this.config.basePath || '/tmp/packages', prefix); if (!await fs.pathExists(basePath)) { return []; } const dirs = await fs.readdir(basePath); return dirs.filter(dir => !dir.startsWith('.')); } // S3 Storage Implementation private s3Client: any; private async initializeS3(): Promise<void> { try { const { S3Client } = await import('@aws-sdk/client-s3'); this.s3Client = new S3Client({ region: this.config.region, endpoint: this.config.endpoint, credentials: this.config.credentials }); } catch (error) { this.logger.warn('S3 client not available:', (error as Error).message); } } private async uploadToS3(key: string, data: Buffer, metadata: StorageMetadata): Promise<UploadResult> { if (!this.s3Client) { throw new Error('S3 client not initialized'); } const { PutObjectCommand } = await import('@aws-sdk/client-s3'); const command = new PutObjectCommand({ Bucket: this.config.bucket, Key: key, Body: data, ContentType: metadata.contentType, Metadata: { 'md5': metadata.checksums.md5, 'sha256': metadata.checksums.sha256 } }); await this.s3Client.send(command); return { success: true, key, url: this.getDownloadUrl('', ''), metadata }; } private async downloadFromS3(key: string): Promise<DownloadResult> { if (!this.s3Client) { throw new Error('S3 client not initialized'); } const { GetObjectCommand } = await import('@aws-sdk/client-s3'); const command = new GetObjectCommand({ Bucket: this.config.bucket, Key: key }); const response = await this.s3Client.send(command); const chunks: Buffer[] = []; for await (const chunk of response.Body as any) { chunks.push(chunk); } const data = Buffer.concat(chunks); const metadata: StorageMetadata = { size: response.ContentLength, contentType: response.ContentType, etag: response.ETag, lastModified: response.LastModified, checksums: { md5: response.Metadata?.['md5'] || '', sha1: '', sha256: response.Metadata?.['sha256'] || '', sha512: '' } }; return { success: true, data, metadata }; } private async deleteFromS3(key: string): Promise<boolean> { if (!this.s3Client) { throw new Error('S3 client not initialized'); } const { DeleteObjectCommand } = await import('@aws-sdk/client-s3'); const command = new DeleteObjectCommand({ Bucket: this.config.bucket, Key: key }); await this.s3Client.send(command); return true; } private async getS3Metadata(key: string): Promise<StorageMetadata | null> { if (!this.s3Client) { throw new Error('S3 client not initialized'); } const { HeadObjectCommand } = await import('@aws-sdk/client-s3'); try { const command = new HeadObjectCommand({ Bucket: this.config.bucket, Key: key }); const response = await this.s3Client.send(command); return { size: response.ContentLength, contentType: response.ContentType, etag: response.ETag, lastModified: response.LastModified, checksums: { md5: response.Metadata?.['md5'] || '', sha1: '', sha256: response.Metadata?.['sha256'] || '', sha512: '' } }; } catch (error) { if (typeof error === 'object' && error !== null && 'name' in error && (error as any).name === 'NotFound') { return null; } throw error; } } private async listS3Versions(prefix: string): Promise<string[]> { if (!this.s3Client) { throw new Error('S3 client not initialized'); } const { ListObjectsV2Command } = await import('@aws-sdk/client-s3'); const command = new ListObjectsV2Command({ Bucket: this.config.bucket, Prefix: prefix, Delimiter: '/' }); const response = await this.s3Client.send(command); return (response.CommonPrefixes || []) .map((prefix: { Prefix: string; }) => { // Extract the version from the prefix string, which is like 'packageName/version/' const parts = prefix.Prefix.split('/'); // The version is the second part (index 1) return parts.length > 1 ? parts[1] : ''; }) .filter((version: any) => version); } // Placeholder implementations for GCS and Azure private async initializeGCS(): Promise<void> { this.logger.warn('GCS storage not implemented yet'); } private async initializeAzure(): Promise<void> { this.logger.warn('Azure storage not implemented yet'); } private async uploadToGCS(key: string, data: Buffer, metadata: StorageMetadata): Promise<UploadResult> { throw new Error('GCS storage not implemented'); } private async uploadToAzure(key: string, data: Buffer, metadata: StorageMetadata): Promise<UploadResult> { throw new Error('Azure storage not implemented'); } private async downloadFromGCS(key: string): Promise<DownloadResult> { throw new Error('GCS storage not implemented'); } private async downloadFromAzure(key: string): Promise<DownloadResult> { throw new Error('Azure storage not implemented'); } private async deleteFromGCS(key: string): Promise<boolean> { throw new Error('GCS storage not implemented'); } private async deleteFromAzure(key: string): Promise<boolean> { throw new Error('Azure storage not implemented'); } private async getGCSMetadata(key: string): Promise<StorageMetadata | null> { throw new Error('GCS storage not implemented'); } private async getAzureMetadata(key: string): Promise<StorageMetadata | null> { throw new Error('Azure storage not implemented'); } private async listGCSVersions(prefix: string): Promise<string[]> { throw new Error('GCS storage not implemented'); } private async listAzureVersions(prefix: string): Promise<string[]> { throw new Error('Azure storage not implemented'); } async testConnection(): Promise<boolean> { try { switch (this.config.type) { case 'local': // Test local storage by ensuring directory exists const basePath = this.config.basePath || '/tmp/packages'; await fs.ensureDir(basePath); return true; case 's3': // For S3, we'd test with a simple head operation // For now, just return true as a placeholder return true; case 'gcs': // For GCS, we'd test bucket access return true; case 'azure': // For Azure, we'd test container access return true; default: return false; } } catch (error) { console.error('Storage connection test failed:', error); return false; } } }