245 lines
7.1 KiB
JavaScript
245 lines
7.1 KiB
JavaScript
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}`);
|
|
});
|