From e616713ec11acd7db50b80f8cb16f38dae6bf400 Mon Sep 17 00:00:00 2001 From: fspecii Date: Wed, 4 Feb 2026 13:39:30 +0200 Subject: [PATCH] Fix delete song, CORS, and cleanup for local-first SQLite setup - Fix delete song route: remove reference to non-existent audio_files table, use audio_url directly from songs table instead - Fix CORS: allow 127.0.0.1 in addition to localhost in development mode - Fix cleanup service: remove audio_files reference, use SQLite datetime syntax --- server/src/index.ts | 10 +++---- server/src/routes/songs.ts | 39 ++++++++++++++------------ server/src/services/cleanup.ts | 51 ++++------------------------------ 3 files changed, 32 insertions(+), 68 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index a2497c4..902e8a6 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -54,12 +54,12 @@ app.use(cors({ origin: (origin, callback) => { // Allow requests with no origin (mobile apps, curl, etc.) if (!origin) return callback(null, true); - // Allow localhost on any port in development - if (config.nodeEnv === 'development' && origin.includes('localhost')) { - return callback(null, true); - } - // Allow LAN IPs in development (192.168.x.x, 10.x.x.x, 172.16-31.x.x) + // Allow localhost and 127.0.0.1 on any port in development if (config.nodeEnv === 'development') { + if (origin.includes('localhost') || origin.includes('127.0.0.1')) { + return callback(null, true); + } + // Allow LAN IPs (192.168.x.x, 10.x.x.x, 172.16-31.x.x) const lanPattern = /^https?:\/\/(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)/; if (lanPattern.test(origin)) { return callback(null, true); diff --git a/server/src/routes/songs.ts b/server/src/routes/songs.ts index c4e7533..93ddc4f 100644 --- a/server/src/routes/songs.ts +++ b/server/src/routes/songs.ts @@ -398,7 +398,7 @@ router.patch('/:id', authMiddleware, async (req: AuthenticatedRequest, res: Resp // Delete song router.delete('/:id', authMiddleware, async (req: AuthenticatedRequest, res: Response) => { try { - const check = await pool.query('SELECT user_id FROM songs WHERE id = $1', [req.params.id]); + const check = await pool.query('SELECT user_id, audio_url, cover_url FROM songs WHERE id = $1', [req.params.id]); if (check.rows.length === 0) { res.status(404).json({ error: 'Song not found' }); return; @@ -408,25 +408,30 @@ router.delete('/:id', authMiddleware, async (req: AuthenticatedRequest, res: Res return; } - const audioFileResult = await pool.query( - 'SELECT id, storage_key, storage_provider FROM audio_files WHERE song_id = $1 AND deleted_at IS NULL', - [req.params.id] - ); + const song = check.rows[0]; + const storage = getStorageProvider(); - if (audioFileResult.rows.length > 0) { - const storage = getStorageProvider(); - for (const audioFile of audioFileResult.rows) { - try { - await storage.delete(audioFile.storage_key); - } catch (err) { - console.error(`Failed to delete storage file ${audioFile.storage_key}:`, err); - } + // Delete audio file from storage + if (song.audio_url) { + try { + // Handle local storage paths (/audio/filename.mp3 -> filename.mp3) + const storageKey = song.audio_url.startsWith('/audio/') + ? song.audio_url.replace('/audio/', '') + : song.audio_url.replace('s3://', ''); + await storage.delete(storageKey); + } catch (err) { + console.error(`Failed to delete audio file ${song.audio_url}:`, err); } + } - await pool.query( - 'UPDATE audio_files SET deleted_at = CURRENT_TIMESTAMP WHERE song_id = $1', - [req.params.id] - ); + // Delete cover image if it's stored locally + if (song.cover_url && song.cover_url.startsWith('/audio/')) { + try { + const coverKey = song.cover_url.replace('/audio/', ''); + await storage.delete(coverKey); + } catch (err) { + console.error(`Failed to delete cover ${song.cover_url}:`, err); + } } await pool.query('DELETE FROM songs WHERE id = $1', [req.params.id]); diff --git a/server/src/services/cleanup.ts b/server/src/services/cleanup.ts index 1fc3348..ceb4715 100644 --- a/server/src/services/cleanup.ts +++ b/server/src/services/cleanup.ts @@ -1,5 +1,4 @@ import { pool } from '../db/pool.js'; -import { getStorageProvider } from './storage/factory.js'; export interface CleanupResult { deleted: number; @@ -7,57 +6,17 @@ export interface CleanupResult { } export async function runCleanupJob(): Promise { - console.log('Starting audio file cleanup job...'); - - const result = await pool.query( - `SELECT af.id, af.song_id, af.storage_key, af.storage_provider - FROM audio_files af - WHERE af.expires_at < NOW() - AND af.deleted_at IS NULL` - ); - - if (result.rows.length === 0) { - console.log('No expired audio files to clean up'); - return { deleted: 0, errors: 0 }; - } - - console.log(`Found ${result.rows.length} expired audio files`); - - const storage = getStorageProvider(); - let deleted = 0; - let errors = 0; - - for (const audioFile of result.rows) { - try { - await storage.delete(audioFile.storage_key); - - await pool.query( - 'UPDATE audio_files SET deleted_at = CURRENT_TIMESTAMP WHERE id = $1', - [audioFile.id] - ); - - await pool.query( - 'UPDATE songs SET audio_url = NULL WHERE id = $1', - [audioFile.song_id] - ); - - deleted++; - console.log(`Deleted expired audio: ${audioFile.storage_key}`); - } catch (err) { - errors++; - console.error(`Failed to delete audio ${audioFile.storage_key}:`, err); - } - } - - console.log(`Cleanup complete: ${deleted} deleted, ${errors} errors`); - return { deleted, errors }; + // Local storage doesn't have expiring files - no cleanup needed + console.log('Cleanup job: No action needed for local storage'); + return { deleted: 0, errors: 0 }; } export async function cleanupDeletedSongs(): Promise { + // Clean up songs without audio that are older than 7 days (SQLite syntax) const result = await pool.query( `DELETE FROM songs WHERE audio_url IS NULL - AND created_at < NOW() - INTERVAL '7 days' + AND created_at < datetime('now', '-7 days') RETURNING id` );