feat: pictures table, Hetzner S3 upload/delete, auto-migration

- pictures table with UUID, status enum, timestamps, blurhash, design
- Auto-trigger updates updated_at on every row change
- POST /api/pictures/:id/upload  → upload file to Hetzner snakkimo bucket
- DELETE /api/pictures/:id       → removes DB row + Hetzner file
- PATCH /api/pictures/:id        → auto-sets published/blocked timestamps
- Migration runs on every server start (idempotent)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 13:39:16 +02:00
parent b82a468197
commit 0f35459b86
6 changed files with 1145 additions and 8 deletions

42
src/s3.js Normal file
View File

@@ -0,0 +1,42 @@
const { S3Client, DeleteObjectCommand } = require('@aws-sdk/client-s3');
const { Upload } = require('@aws-sdk/lib-storage');
const BUCKET = 'snakkimo';
const ENDPOINT = 'https://fsn1.your-objectstorage.com';
const PUBLIC_BASE = `https://${BUCKET}.fsn1.your-objectstorage.com`;
const client = new S3Client({
region: 'fsn1',
endpoint: ENDPOINT,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY,
secretAccessKey: process.env.S3_SECRET_KEY,
},
forcePathStyle: false,
});
async function uploadFile(key, buffer, mimetype) {
const upload = new Upload({
client,
params: {
Bucket: BUCKET,
Key: key,
Body: buffer,
ContentType: mimetype,
ACL: 'public-read',
},
});
await upload.done();
return `${PUBLIC_BASE}/${key}`;
}
async function deleteFile(key) {
await client.send(new DeleteObjectCommand({ Bucket: BUCKET, Key: key }));
}
function keyFromUrl(url) {
if (!url) return null;
return url.replace(`${PUBLIC_BASE}/`, '');
}
module.exports = { uploadFile, deleteFile, keyFromUrl };