diff --git a/.env.example b/.env.example index 5b1dfb7..80dd9ab 100644 --- a/.env.example +++ b/.env.example @@ -71,3 +71,12 @@ HIDE_BILLING=true # (logged, not sent). Leave UNSET in production. Useful locally so test signups # can't email real users. # GRAPH_DEV_RESTRICT_TO=me@example.com +# Optional: Supabase (PostgreSQL migration configuration - if fully migrated in future) +VITE_SUPABASE_URL= +VITE_SUPABASE_ANON_KEY= +SUPABASE_SERVICE_ROLE_KEY= + +# Optional: BunnyCDN Storage (for remote uploads instead of local disk) +VITE_BUNNY_STORAGE_ENDPOINT= +VITE_BUNNY_API_KEY= +VITE_BUNNY_PULL_ZONE= diff --git a/server/config.js b/server/config.js index 3611fd1..f55c51c 100644 --- a/server/config.js +++ b/server/config.js @@ -338,4 +338,9 @@ module.exports = { // #143 throttle the reclaim-deferred log to once per device per window, so a // retrying/stuck device can't flood stdout (same discipline as the content-ack shed log). reclaimRejectLogWindowMs: parseInt(process.env.RECLAIM_REJECT_LOG_WINDOW_MS) || 60000, + + // BunnyStorage Integration + bunnyStorageEndpoint: process.env.VITE_BUNNY_STORAGE_ENDPOINT || '', + bunnyApiKey: process.env.VITE_BUNNY_API_KEY || '', + bunnyPullZone: process.env.VITE_BUNNY_PULL_ZONE || '', }; diff --git a/server/lib/content-ingest.js b/server/lib/content-ingest.js index f11c16a..c0076fc 100644 --- a/server/lib/content-ingest.js +++ b/server/lib/content-ingest.js @@ -69,6 +69,41 @@ async function ingestUploadedFile({ file, userId, workspaceId, folderId = null } console.warn('Thumbnail/metadata generation failed:', e.message); } + if (config.bunnyStorageEndpoint && config.bunnyApiKey && config.bunnyPullZone) { + try { + const fs = require('fs'); + const uploadToBunny = async (localPath, bunnyFilename) => { + const fileStream = fs.createReadStream(localPath); + const url = `${config.bunnyStorageEndpoint.replace(/\/$/, '')}/${bunnyFilename}`; + const res = await fetch(url, { + method: 'PUT', + headers: { 'AccessKey': config.bunnyApiKey }, + body: fileStream, + duplex: 'half' + }); + if (!res.ok) throw new Error(`BunnyCDN upload failed: ${res.statusText}`); + return `${config.bunnyPullZone.replace(/\/$/, '')}/${bunnyFilename}`; + }; + + const bunnyFileUrl = await uploadToBunny(file.path, filepath); + let bunnyThumbUrl = null; + if (thumbnailPath) { + bunnyThumbUrl = await uploadToBunny(path.join(config.contentDir, thumbnailPath), thumbnailPath); + fs.unlink(path.join(config.contentDir, thumbnailPath), () => {}); + } + fs.unlink(file.path, () => {}); + + db.prepare(` + INSERT INTO content (id, user_id, workspace_id, filename, filepath, mime_type, file_size, duration_sec, thumbnail_path, width, height, folder_id, remote_url) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run(id, userId, workspaceId, safeFilename(file.originalname), '', file.mimetype, file.size, durationSec, bunnyThumbUrl, width, height, folderId || null, bunnyFileUrl); + + return db.prepare('SELECT * FROM content WHERE id = ?').get(id); + } catch (e) { + console.warn('BunnyCDN upload failed, falling back to local storage:', e.message); + } + } + db.prepare(` INSERT INTO content (id, user_id, workspace_id, filename, filepath, mime_type, file_size, duration_sec, thumbnail_path, width, height, folder_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)