recoder-code
Version:
🚀 AI-powered development platform - Chat with 32+ models, build projects, automate workflows. Free models included!
600 lines (508 loc) • 18 kB
text/typescript
/**
* 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;
}
}
}