// ============================================================ // AUDIO BACKUPS ROUTES — Server-side audio backup storage // Stores compressed audio in PostgreSQL, auto-deletes after 24h // ============================================================ var express = require('express'); var router = express.Router(); var zlib = require('zlib'); var multer = require('multer'); var db = require('../db/database'); var { authMiddleware } = require('../middleware/auth'); var cryptoUtil = require('../utils/crypto'); var { serverError } = require('../utils/errors'); // 25MB upload limit (same as transcribe) var upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 25 * 1024 * 1024 } }); router.use(authMiddleware); // ── POST save audio backup (compressed) ────────────────────────────────── router.post('/audio-backups', upload.single('audio'), async function(req, res) { console.log('[AudioBackup] POST received, hasFile=' + !!req.file + ', user=' + (req.user ? req.user.id : 'none')); try { if (!req.file) return res.status(400).json({ error: 'No audio file' }); var module = req.body.module || 'encounter'; var originalSize = req.file.size; // Gzip compress, then AES-256-GCM encrypt the audio data. var compressed = await new Promise(function(resolve, reject) { zlib.gzip(req.file.buffer, { level: 6 }, function(err, result) { if (err) reject(err); else resolve(result); }); }); var stored = cryptoUtil.encryptBuffer(compressed); var result = await db.run( 'INSERT INTO audio_backups (user_id, module, mime_type, size_bytes, compressed_bytes, audio_data) VALUES ($1,$2,$3,$4,$5,$6)', [req.user.id, module, req.file.mimetype || 'audio/webm', originalSize, stored.length, stored] ); var ratio = originalSize > 0 ? Math.round((1 - compressed.length / originalSize) * 100) : 0; console.log('[AudioBackup] Saved ' + (originalSize / 1024).toFixed(0) + 'KB → ' + (compressed.length / 1024).toFixed(0) + 'KB (' + ratio + '% compression) id=' + result.lastInsertRowid + ' user=' + req.user.id); res.json({ success: true, id: result.lastInsertRowid, originalSize: originalSize, compressedSize: compressed.length }); } catch (e) { return serverError(res, 'AudioBackup save', e, 'Could not save audio backup'); } }); // ── GET list audio backups ─────────────────────────────────────────────── router.get('/audio-backups', async function(req, res) { try { var rows = await db.all( "SELECT id, module, mime_type, size_bytes, compressed_bytes, created_at, expires_at FROM audio_backups WHERE user_id = $1 AND expires_at > NOW() ORDER BY created_at DESC", [req.user.id] ); res.json({ success: true, backups: rows }); } catch (e) { return serverError(res, 'AudioBackup list', e, 'Could not list backups'); } }); // ── GET download audio backup (decompressed) ───────────────────────────── router.get('/audio-backups/:id/audio', async function(req, res) { try { var row = await db.get( 'SELECT audio_data, mime_type, size_bytes FROM audio_backups WHERE id = $1 AND user_id = $2 AND expires_at > NOW()', [req.params.id, req.user.id] ); if (!row) return res.status(404).json({ error: 'Backup not found or expired' }); // Decrypt (if encrypted row) then gunzip. Legacy rows are passed through. var ciphertext = row.audio_data; var gzipped = cryptoUtil.isEncryptedBuffer(ciphertext) ? cryptoUtil.decryptBuffer(ciphertext) : ciphertext; var decompressed = await new Promise(function(resolve, reject) { zlib.gunzip(gzipped, function(err, result) { if (err) reject(err); else resolve(result); }); }); res.setHeader('Content-Type', row.mime_type || 'audio/webm'); res.setHeader('Content-Length', decompressed.length); res.send(decompressed); } catch (e) { return serverError(res, 'AudioBackup read', e, 'Could not retrieve audio backup'); } }); // ── DELETE audio backup ────────────────────────────────────────────────── router.delete('/audio-backups/:id', async function(req, res) { try { var result = await db.run( 'DELETE FROM audio_backups WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id] ); if (result.changes === 0) return res.status(404).json({ error: 'Not found' }); res.json({ success: true }); } catch (e) { return serverError(res, 'AudioBackup delete', e, 'Could not delete backup'); } }); module.exports = router;