voxblog/apps/api/src/media.ts

152 lines
5.6 KiB
TypeScript

import express from 'express';
import multer from 'multer';
import crypto from 'crypto';
import { uploadBuffer, downloadObject, listObjects, deleteObject as s3DeleteObject } from './storage/s3';
import { pool } from './db';
const router = express.Router();
const upload = multer({ storage: multer.memoryStorage() });
router.get('/list', async (
req: express.Request,
res: express.Response
) => {
try {
const bucket = (req.query.bucket as string) || process.env.S3_BUCKET || '';
const prefix = (req.query.prefix as string) || '';
if (!bucket) return res.status(400).json({ error: 'bucket is required' });
const out = await listObjects({ bucket, prefix, maxKeys: 200 });
return res.json(out);
} catch (err) {
console.error('List objects failed:', err);
return res.status(500).json({ error: 'List failed' });
}
});
router.delete('/obj', async (
req: express.Request,
res: express.Response
) => {
try {
const { bucket: bodyBucket, key } = req.body as { bucket?: string; key?: string };
const bucket = bodyBucket || process.env.S3_BUCKET || '';
if (!bucket || !key) return res.status(400).json({ error: 'bucket and key are required' });
await s3DeleteObject({ bucket, key });
return res.json({ success: true });
} catch (err) {
console.error('Delete object failed:', err);
return res.status(500).json({ error: 'Delete failed' });
}
});
router.post('/image', upload.single('image'), async (
req: express.Request,
res: express.Response
) => {
try {
console.log('[API] POST /api/media/image');
const { S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY } = process.env;
if (!S3_ENDPOINT || !S3_ACCESS_KEY || !S3_SECRET_KEY) {
console.error('Image upload failed: missing S3 config');
return res.status(500).json({ error: 'Object storage not configured' });
}
if (!req.file) return res.status(400).json({ error: 'No image file' });
const bucket = process.env.S3_BUCKET || 'voxblog';
const mime = req.file.mimetype || 'application/octet-stream';
const ext = mime.split('/')[1] || 'bin';
const key = `images/${new Date().toISOString().slice(0,10)}/${crypto.randomUUID()}.${ext}`;
console.log('[API] Uploading image', { mime, size: req.file.size, bucket, key });
const out = await uploadBuffer({
bucket,
key,
body: req.file.buffer,
contentType: mime,
});
// Provide a proxied URL for immediate use in editor
const url = `/api/media/obj?bucket=${encodeURIComponent(out.bucket)}&key=${encodeURIComponent(out.key)}`;
console.log('[API] Image upload success', out);
return res.status(200).json({ success: true, ...out, url });
} catch (err) {
console.error('Image upload failed:', err);
return res.status(500).json({ error: 'Image upload failed' });
}
});
router.get('/obj', async (
req: express.Request,
res: express.Response
) => {
try {
const bucket = (req.query.bucket as string) || process.env.S3_BUCKET || '';
const key = req.query.key as string;
if (!bucket || !key) return res.status(400).json({ error: 'bucket and key are required' });
const { buffer, contentType } = await downloadObject({ bucket, key });
res.setHeader('Content-Type', contentType || 'application/octet-stream');
// Basic cache headers for media
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
return res.send(buffer);
} catch (err) {
console.error('Object fetch failed:', err);
return res.status(404).json({ error: 'Object not found' });
}
});
router.post('/audio', upload.single('audio'), async (
req: express.Request,
res: express.Response
) => {
try {
console.log('[API] POST /api/media/audio');
const { S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY } = process.env;
if (!S3_ENDPOINT || !S3_ACCESS_KEY || !S3_SECRET_KEY) {
console.error('Upload failed: missing S3 config (S3_ENDPOINT/S3_ACCESS_KEY/S3_SECRET_KEY)');
return res.status(500).json({ error: 'Object storage not configured' });
}
if (!req.file) return res.status(400).json({ error: 'No audio file' });
const bucket = process.env.S3_BUCKET || 'voxblog';
const mime = req.file.mimetype || 'application/octet-stream';
const ext = mime === 'audio/webm' ? 'webm' : (mime === 'audio/mp4' ? 'm4a' : (mime.split('/')[1] || 'bin'));
const postId = (req.query.postId as string) || '';
const clipId = crypto.randomUUID();
const key = postId
? `audio/posts/${encodeURIComponent(postId)}/${clipId}.${ext}`
: `audio/${new Date().toISOString().slice(0,10)}/${crypto.randomUUID()}.${ext}`;
console.log('[API] Uploading file', { mime, size: req.file.size, bucket, key, postId: postId || '(none)', clipId });
const out = await uploadBuffer({
bucket,
key,
body: req.file.buffer,
contentType: mime,
});
// If postId provided, insert into audio_clips table
if (postId) {
try {
const now = new Date();
await pool.query(
'INSERT INTO audio_clips (id, post_id, bucket, object_key, mime, created_at) VALUES (?, ?, ?, ?, ?, ?)',
[clipId, postId, out.bucket, out.key, mime, now]
);
} catch (e) {
console.error('[API] DB insert audio_clip failed:', e);
// continue anyway, response still returns S3 info
}
}
console.log('[API] Upload success', out);
return res.status(200).json({ success: true, ...out, clipId: postId ? clipId : undefined, postId: postId || undefined });
} catch (err) {
console.error('Upload failed:', err);
return res.status(500).json({ error: 'Upload failed' });
}
});
export default router;