import express from 'express'; import multer from 'multer'; import cors from 'cors'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { createClient } from "@libsql/client"; import dotenv from 'dotenv'; dotenv.config(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); const PORT = process.env.PORT || 8080; app.use(cors()); app.use(express.json()); // Directories const DATA_DIR = path.join(__dirname, 'data'); const UPLOAD_DIR = path.join(__dirname, 'uploads'); // Ensure directories exist if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true }); if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true }); // Initialize LibSQL const db = createClient({ url: "file:data/data.db" }); // Initialize Schema const initDB = async () => { try { await db.execute(` CREATE TABLE IF NOT EXISTS photos ( id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL, title TEXT, description TEXT, date TEXT, location TEXT, camera TEXT, lens TEXT, iso TEXT, aperture TEXT, shutter TEXT, focalLength TEXT, gps_lat REAL, gps_lng REAL, file_name TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); console.log("Database initialized"); } catch (err) { console.error("Database initialization failed", err); } }; initDB(); // Multer Storage const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, UPLOAD_DIR); }, filename: (req, file, cb) => { // Use timestamp to ensure unique filenames const ext = path.extname(file.originalname); const name = path.basename(file.originalname, ext).replace(/[^a-zA-Z0-9]/g, '-'); cb(null, `${Date.now()}-${name}${ext}`); } }); const upload = multer({ storage }); // API Routes // Get all photos app.get('/api/photos', async (req, res) => { try { const result = await db.execute("SELECT * FROM photos ORDER BY date DESC"); // Map result rows to object format if needed, but LibSQL returns objects usually // We might need to reconstruct the nested 'settings' or 'gps' objects for the frontend const photos = result.rows.map(row => ({ id: row.id, url: row.url, title: row.title, description: row.description, date: row.date, location: row.location, camera: row.camera, lens: row.lens, settings: { iso: row.iso, aperture: row.aperture, shutter: row.shutter, focalLength: row.focalLength }, gps: (row.gps_lat && row.gps_lng) ? { lat: row.gps_lat, lng: row.gps_lng } : null, fileName: row.file_name })); res.json(photos); } catch (err) { console.error(err); res.status(500).json({ error: "Failed to fetch photos" }); } }); // Upload a photo app.post('/api/photos', upload.single('image'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'No image uploaded' }); } const metadata = JSON.parse(req.body.metadata || '{}'); const fileName = req.file.filename; const url = `/uploads/${fileName}`; // Insert into DB await db.execute({ sql: `INSERT INTO photos ( url, title, description, date, location, camera, lens, iso, aperture, shutter, focalLength, gps_lat, gps_lng, file_name ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, args: [ url, metadata.title || '', metadata.description || '', metadata.date || '', metadata.location || '', metadata.camera || '', metadata.lens || '', metadata.settings?.iso || '', metadata.settings?.aperture || '', metadata.settings?.shutter || '', metadata.settings?.focalLength || '', metadata.gps?.lat || null, metadata.gps?.lng || null, fileName ] }); // We could fetch the inserted row, but for now just return success // Simulating the returned object const newPhoto = { id: Date.now(), // approximation, real ID is in DB url, ...metadata }; res.status(201).json(newPhoto); } catch (error) { console.error("Upload error", error); res.status(500).json({ error: 'Upload failed' }); } }); // Delete a photo app.delete('/api/photos/:id', async (req, res) => { const id = parseInt(req.params.id); try { // Get filename first const result = await db.execute({ sql: "SELECT file_name FROM photos WHERE id = ?", args: [id] }); if (result.rows.length === 0) { return res.status(404).json({ error: 'Photo not found' }); } const fileName = result.rows[0].file_name; // Remove from DB await db.execute({ sql: "DELETE FROM photos WHERE id = ?", args: [id] }); // Remove file if (fileName) { const filePath = path.join(UPLOAD_DIR, fileName); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } } res.json({ success: true }); } catch (err) { console.error("Delete error", err); res.status(500).json({ error: "Delete failed" }); } }); // Serve Uploads app.use('/uploads', express.static(UPLOAD_DIR)); // Favicon Storage const faviconStorage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, UPLOAD_DIR); }, filename: (req, file, cb) => { cb(null, 'favicon.png'); } }); const uploadFavicon = multer({ storage: faviconStorage }); // Upload Favicon app.post('/api/favicon', uploadFavicon.single('favicon'), (req, res) => { if (!req.file) return res.status(400).json({ error: 'No file uploaded' }); res.json({ success: true, url: '/uploads/favicon.png' }); }); // Serve Favicon (Dynamic) app.get('/api/favicon', (req, res) => { const faviconPath = path.join(UPLOAD_DIR, 'favicon.png'); if (fs.existsSync(faviconPath)) { res.sendFile(faviconPath); } else { // Fallback to default res.status(404).send('Not found'); } }); // Serve Frontend (Production) const DIST_DIR = path.join(__dirname, 'dist'); if (fs.existsSync(DIST_DIR)) { app.use(express.static(DIST_DIR)); // SPA Fallback app.get(/.*/, (req, res) => { res.sendFile(path.join(DIST_DIR, 'index.html')); }); } app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });