import { createReadStream } from 'fs'; import type { CleanupResult, MediaManagerService as MediaManagerServiceContract, StorageAdapter, StorageStrategy, UploadInput, UploadResult, } from './types'; import { validateRelativePath } from './validators/path-validator'; export class MediaManagerService implements MediaManagerServiceContract { constructor( private readonly localAdapter: StorageAdapter, private readonly s3Adapter: StorageAdapter, ) {} async upload(input: UploadInput): Promise { const { storageStrategy, relativePath, localStoragePrefix = 'local', fileStreams, } = input; if (relativePath.length !== 1 || fileStreams.length !== 1) { return this.buildFailure(storageStrategy, relativePath); } const pathEntry = relativePath[0]; try { validateRelativePath(pathEntry); } catch { return this.buildFailure(storageStrategy, relativePath); } const stream = fileStreams[0]; if (!stream) { return this.buildFailure(storageStrategy, relativePath); } switch (storageStrategy) { case 'LOCAL_ONLY': return this.uploadLocal( pathEntry, localStoragePrefix, storageStrategy, stream, ); case 'S3_ONLY': return this.uploadS3Only( pathEntry, localStoragePrefix, storageStrategy, stream, ); case 'S3_AND_LOCAL': return this.uploadS3AndLocal( pathEntry, localStoragePrefix, storageStrategy, stream, ); default: return this.buildFailure(storageStrategy, relativePath); } } async cleanup( storageStrategy: StorageStrategy, relativePath: string[], localStoragePrefix: string = 'local', ): Promise { if (relativePath.length === 0) { return { status: 0 }; } for (const pathEntry of relativePath) { try { validateRelativePath(pathEntry); } catch { return { status: 0 }; } } try { switch (storageStrategy) { case 'LOCAL_ONLY': await this.localAdapter.delete(relativePath, localStoragePrefix); return { status: 1 }; case 'S3_ONLY': await this.s3Adapter.delete(relativePath, localStoragePrefix); return { status: 1 }; case 'S3_AND_LOCAL': { const localResult = await this.localAdapter .delete(relativePath, localStoragePrefix) .then(() => true) .catch(() => false); const s3Result = await this.s3Adapter .delete(relativePath, localStoragePrefix) .then(() => true) .catch(() => false); return { status: localResult && s3Result ? 1 : 0 }; } default: return { status: 0 }; } } catch { return { status: 0 }; } } private async uploadLocal( relativePath: string, localStoragePrefix: string, storageStrategy: StorageStrategy, stream: NodeJS.ReadableStream, ): Promise { try { const { savedPath } = await this.localAdapter.put( relativePath, localStoragePrefix, stream, ); return this.buildSuccess(storageStrategy, [relativePath], savedPath); } catch { return this.buildFailure(storageStrategy, [relativePath]); } } private async uploadS3Only( relativePath: string, localStoragePrefix: string, storageStrategy: StorageStrategy, stream: NodeJS.ReadableStream, ): Promise { let localResult; try { localResult = await this.localAdapter.put( relativePath, localStoragePrefix, stream, ); } catch { return this.buildFailure(storageStrategy, [relativePath]); } let s3Stream: ReturnType | undefined; let s3SavedPath = ''; try { s3Stream = createReadStream(localResult.savedPath); const s3Result = await this.s3Adapter.put( relativePath, localStoragePrefix, s3Stream, ); s3SavedPath = s3Result.savedPath; } catch { await this.localAdapter .delete([relativePath], localStoragePrefix) .catch(() => undefined); return this.buildFailure(storageStrategy, [relativePath]); } finally { if (s3Stream) { s3Stream.destroy(); } } await this.localAdapter .delete([relativePath], localStoragePrefix) .catch(() => undefined); return this.buildSuccess('S3_ONLY', [relativePath], s3SavedPath); } private async uploadS3AndLocal( relativePath: string, localStoragePrefix: string, storageStrategy: StorageStrategy, stream: NodeJS.ReadableStream, ): Promise { let localResult; try { localResult = await this.localAdapter.put( relativePath, localStoragePrefix, stream, ); } catch { return this.buildFailure(storageStrategy, [relativePath]); } let s3Stream: ReturnType | undefined; try { s3Stream = createReadStream(localResult.savedPath); await this.s3Adapter.put(relativePath, localStoragePrefix, s3Stream); return this.buildSuccess( 'S3_AND_LOCAL', [relativePath], localResult.savedPath, ); } catch { return this.buildSuccess( 'LOCAL_ONLY', [relativePath], localResult.savedPath, ); } finally { if (s3Stream) { s3Stream.destroy(); } } } private buildFailure( storageStrategy: StorageStrategy, relativePath: string[], ): UploadResult { return { status: 0, storageStrategy, relativePath, savedPath: '', }; } private buildSuccess( storageStrategy: StorageStrategy, relativePath: string[], savedPath: string, ): UploadResult { return { status: 1, storageStrategy, relativePath, savedPath, }; } }