import { Injectable } from '@nestjs/common'; import { mkdir, access } from 'node:fs/promises'; import { constants } from 'node:fs'; import { dirname, join } from 'node:path'; import { AppConfigService } from '../config/config.service'; export interface StorageStatus { root: string; writable: boolean; } @Injectable() export class LocalFilesystemStorageService { constructor(private readonly configService: AppConfigService) {} get root(): string { return this.configService.storageRoot; } resolve(relativePath: string): string { return join(this.root, relativePath); } userAudioAssetStorageKey(userId: string, sha256: string): string { return join('users', userId, 'audio', `${sha256}.mp3`); } userAudioAssetPath(userId: string, sha256: string): string { return this.resolve(this.userAudioAssetStorageKey(userId, sha256)); } userArtworkAssetStorageKey( userId: string, sha256: string, fileExtension: string, ): string { return join('users', userId, 'artwork', `${sha256}.${fileExtension}`); } userArtworkAssetPath( userId: string, sha256: string, fileExtension: string, ): string { return this.resolve( this.userArtworkAssetStorageKey(userId, sha256, fileExtension), ); } tempUploadStorageKey(uploadId: string): string { return join('temp', 'uploads', `${uploadId}.part`); } tempUploadPath(uploadId: string): string { return this.resolve(this.tempUploadStorageKey(uploadId)); } async ensureDirectory(path: string): Promise { await mkdir(path, { recursive: true }); } async ensureParentDirectory(path: string): Promise { await this.ensureDirectory(dirname(path)); } async checkReadiness(): Promise { const paths = [ this.root, join(this.root, 'users'), join(this.root, 'temp'), join(this.root, 'temp', 'uploads'), join(this.root, 'incoming'), join(this.root, 'quarantine'), join(this.root, 'library', 'audio'), join(this.root, 'library', 'artwork'), ]; for (const path of paths) { await mkdir(path, { recursive: true }); await access(path, constants.R_OK | constants.W_OK); } return { root: this.root, writable: true, }; } }