/** * Local File Storage * * Stores uploaded files locally in the uploads/ directory. * Files are served via Express static middleware. */ import fs from "fs/promises"; import path from "path"; import { fileURLToPath } from "url"; import { nanoid } from "nanoid"; // Get directory name in ESM const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Base directory for uploads (relative to project root) const UPLOADS_DIR = path.resolve(__dirname, "../uploads"); // Subdirectories const TRACKS_DIR = path.join(UPLOADS_DIR, "tracks"); const STEMS_DIR = path.join(UPLOADS_DIR, "stems"); /** * Ensure upload directories exist */ export async function ensureUploadDirs(): Promise { await fs.mkdir(TRACKS_DIR, { recursive: true }); await fs.mkdir(STEMS_DIR, { recursive: true }); } /** * Get the uploads base directory path */ export function getUploadsDir(): string { return UPLOADS_DIR; } /** * Save an uploaded track file * * @param userId User ID * @param fileName Original filename * @param data File data as Buffer * @returns Object with fileKey (relative path) and filePath (absolute path) */ export async function saveTrackFile( userId: number, fileName: string, data: Buffer ): Promise<{ fileKey: string; filePath: string; fileUrl: string }> { await ensureUploadDirs(); // Create user-specific directory const userDir = path.join(TRACKS_DIR, String(userId)); await fs.mkdir(userDir, { recursive: true }); // Generate unique filename const ext = path.extname(fileName) || ".mp3"; const uniqueName = `${nanoid(16)}${ext}`; const filePath = path.join(userDir, uniqueName); // Write file await fs.writeFile(filePath, data); // Generate relative key for database storage const fileKey = `tracks/${userId}/${uniqueName}`; // URL path for serving (will be served via /uploads/ route) const fileUrl = `/uploads/${fileKey}`; return { fileKey, filePath, fileUrl }; } /** * Save a stem file from ML processing * * @param trackId Track ID * @param stemType Stem type (vocals, drums, bass, other) * @param sourcePath Source file path from ML output * @returns Object with fileKey, filePath, and fileUrl */ export async function saveStemFile( trackId: number, stemType: string, sourcePath: string ): Promise<{ fileKey: string; filePath: string; fileUrl: string }> { await ensureUploadDirs(); // Create track-specific stems directory const trackStemsDir = path.join(STEMS_DIR, String(trackId)); await fs.mkdir(trackStemsDir, { recursive: true }); // Get extension from source const ext = path.extname(sourcePath) || ".mp3"; const uniqueName = `${stemType}-${nanoid(8)}${ext}`; const filePath = path.join(trackStemsDir, uniqueName); // Copy file from ML output to uploads directory await fs.copyFile(sourcePath, filePath); // Generate relative key for database storage const fileKey = `stems/${trackId}/${uniqueName}`; // URL path for serving const fileUrl = `/uploads/${fileKey}`; return { fileKey, filePath, fileUrl }; } /** * Get the absolute path for a file key */ export function getFilePath(fileKey: string): string { return path.join(UPLOADS_DIR, fileKey); } /** * Check if a file exists */ export async function fileExists(fileKey: string): Promise { try { await fs.access(getFilePath(fileKey)); return true; } catch { return false; } } /** * Delete a file */ export async function deleteFile(fileKey: string): Promise { try { await fs.unlink(getFilePath(fileKey)); } catch { // Ignore if file doesn't exist } } /** * Get file stats */ export async function getFileStats(fileKey: string): Promise<{ size: number } | null> { try { const stats = await fs.stat(getFilePath(fileKey)); return { size: stats.size }; } catch { return null; } } /** * Create a temp directory for ML processing output */ export async function createTempDir(prefix: string): Promise { const tempDir = path.join(UPLOADS_DIR, "temp", `${prefix}-${nanoid(8)}`); await fs.mkdir(tempDir, { recursive: true }); return tempDir; } /** * Clean up a temp directory */ export async function cleanupTempDir(tempDir: string): Promise { try { await fs.rm(tempDir, { recursive: true, force: true }); } catch { // Ignore errors } }