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
This commit is contained in:
+5
-5
@@ -54,12 +54,12 @@ app.use(cors({
|
|||||||
origin: (origin, callback) => {
|
origin: (origin, callback) => {
|
||||||
// Allow requests with no origin (mobile apps, curl, etc.)
|
// Allow requests with no origin (mobile apps, curl, etc.)
|
||||||
if (!origin) return callback(null, true);
|
if (!origin) return callback(null, true);
|
||||||
// Allow localhost on any port in development
|
// Allow localhost and 127.0.0.1 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)
|
|
||||||
if (config.nodeEnv === '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])\.)/;
|
const lanPattern = /^https?:\/\/(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)/;
|
||||||
if (lanPattern.test(origin)) {
|
if (lanPattern.test(origin)) {
|
||||||
return callback(null, true);
|
return callback(null, true);
|
||||||
|
|||||||
+22
-17
@@ -398,7 +398,7 @@ router.patch('/:id', authMiddleware, async (req: AuthenticatedRequest, res: Resp
|
|||||||
// Delete song
|
// Delete song
|
||||||
router.delete('/:id', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
router.delete('/:id', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
try {
|
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) {
|
if (check.rows.length === 0) {
|
||||||
res.status(404).json({ error: 'Song not found' });
|
res.status(404).json({ error: 'Song not found' });
|
||||||
return;
|
return;
|
||||||
@@ -408,25 +408,30 @@ router.delete('/:id', authMiddleware, async (req: AuthenticatedRequest, res: Res
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const audioFileResult = await pool.query(
|
const song = check.rows[0];
|
||||||
'SELECT id, storage_key, storage_provider FROM audio_files WHERE song_id = $1 AND deleted_at IS NULL',
|
const storage = getStorageProvider();
|
||||||
[req.params.id]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (audioFileResult.rows.length > 0) {
|
// Delete audio file from storage
|
||||||
const storage = getStorageProvider();
|
if (song.audio_url) {
|
||||||
for (const audioFile of audioFileResult.rows) {
|
try {
|
||||||
try {
|
// Handle local storage paths (/audio/filename.mp3 -> filename.mp3)
|
||||||
await storage.delete(audioFile.storage_key);
|
const storageKey = song.audio_url.startsWith('/audio/')
|
||||||
} catch (err) {
|
? song.audio_url.replace('/audio/', '')
|
||||||
console.error(`Failed to delete storage file ${audioFile.storage_key}:`, err);
|
: 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(
|
// Delete cover image if it's stored locally
|
||||||
'UPDATE audio_files SET deleted_at = CURRENT_TIMESTAMP WHERE song_id = $1',
|
if (song.cover_url && song.cover_url.startsWith('/audio/')) {
|
||||||
[req.params.id]
|
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]);
|
await pool.query('DELETE FROM songs WHERE id = $1', [req.params.id]);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { pool } from '../db/pool.js';
|
import { pool } from '../db/pool.js';
|
||||||
import { getStorageProvider } from './storage/factory.js';
|
|
||||||
|
|
||||||
export interface CleanupResult {
|
export interface CleanupResult {
|
||||||
deleted: number;
|
deleted: number;
|
||||||
@@ -7,57 +6,17 @@ export interface CleanupResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runCleanupJob(): Promise<CleanupResult> {
|
export async function runCleanupJob(): Promise<CleanupResult> {
|
||||||
console.log('Starting audio file cleanup job...');
|
// Local storage doesn't have expiring files - no cleanup needed
|
||||||
|
console.log('Cleanup job: No action needed for local storage');
|
||||||
const result = await pool.query(
|
return { deleted: 0, errors: 0 };
|
||||||
`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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cleanupDeletedSongs(): Promise<number> {
|
export async function cleanupDeletedSongs(): Promise<number> {
|
||||||
|
// Clean up songs without audio that are older than 7 days (SQLite syntax)
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`DELETE FROM songs
|
`DELETE FROM songs
|
||||||
WHERE audio_url IS NULL
|
WHERE audio_url IS NULL
|
||||||
AND created_at < NOW() - INTERVAL '7 days'
|
AND created_at < datetime('now', '-7 days')
|
||||||
RETURNING id`
|
RETURNING id`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user