Files
Media-Aggregation-and-Sorti…/ServerFiles/server.js
T

385 lines
12 KiB
JavaScript

const express = require('express');
const cors = require('cors');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const config = require('./config');
const db = require('./db');
const app = express();
app.use(cors());
app.use(express.json({ limit: '100mb' }));
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
next();
});
function getPhotosPath() {
const p = config.getPhotosPath();
if (!p) throw new Error('Photos path not configured. Set it in nas_config.txt');
if (!fs.existsSync(p)) {
fs.mkdirSync(p, { recursive: true });
}
return p;
}
function sanitizeUsername(username) {
return username.trim().replace(/[^a-zA-Z0-9_\-.]/g, '_');
}
// ─── Health ──────────────────────────────────────────────────
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// ─── User Registration ──────────────────────────────────────
// Device sends its username; NAS registers and creates user folder
app.post('/api/register', (req, res) => {
try {
const { username } = req.body;
if (!username || typeof username !== 'string' || username.trim().length === 0) {
return res.status(400).json({ error: 'Username is required' });
}
const sanitized = sanitizeUsername(username);
const userId = db.ensureUser(sanitized);
const photosPath = getPhotosPath();
const userDir = path.join(photosPath, sanitized);
if (!fs.existsSync(userDir)) {
fs.mkdirSync(userDir, { recursive: true });
}
res.json({ success: true, userId, username: sanitized });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ─── Index (NAS builds its database) ────────────────────────
app.post('/api/index', (req, res) => {
try {
const { username } = req.body;
if (!username) return res.status(400).json({ error: 'Username is required' });
const photosPath = getPhotosPath();
const sanitized = sanitizeUsername(username);
const removed = db.removeOrphanedEntries(sanitized, photosPath);
const result = db.indexPhotosForUser(sanitized, photosPath);
res.json({
success: true,
username: sanitized,
indexed: result.indexed,
skipped: result.skipped,
errors: result.errors,
totalFiles: result.total,
orphansRemoved: removed,
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ─── Photo List (POST fetch cycle) ─────────────────────────
app.post('/api/photos', (req, res) => {
try {
const { username } = req.body;
if (!username) return res.status(400).json({ error: 'Username is required' });
const sanitized = sanitizeUsername(username);
const photos = db.getPhotoIndex(sanitized);
res.json({
success: true,
username: sanitized,
count: photos.length,
photos: photos.map(p => ({
id: p.id,
filename: p.filename,
sha256Hash: p.sha256_hash,
fileSize: p.file_size,
mimeType: p.mime_type,
indexedAt: p.indexed_at,
})),
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ─── Fetch Single Photo as Base64 with hash ─────────────────
app.post('/api/photo/fetch', (req, res) => {
try {
const { photoId, hash } = req.body;
if (!photoId && !hash) {
return res.status(400).json({ error: 'photoId or hash is required' });
}
let photo;
if (photoId) {
photo = db.getPhotoById(photoId);
} else {
photo = db.getPhotoByHash(hash);
}
if (!photo) return res.status(404).json({ error: 'Photo not found' });
if (!fs.existsSync(photo.filepath)) {
return res.status(404).json({ error: 'Photo file missing from disk' });
}
const fileBuffer = fs.readFileSync(photo.filepath);
const base64Data = fileBuffer.toString('base64');
const transportHash = crypto.createHash('sha256').update(base64Data).digest('hex');
res.json({
success: true,
photo: {
id: photo.id,
filename: photo.filename,
mimeType: photo.mime_type,
fileSize: photo.file_size,
sha256Hash: photo.sha256_hash,
transportHash,
base64: base64Data,
},
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ─── Upload Photo from Device to NAS ────────────────────────
app.post('/api/photo/upload', (req, res) => {
try {
const { username, filename, base64, transportHash } = req.body;
if (!username || !filename || !base64) {
return res.status(400).json({ error: 'username, filename, and base64 are required' });
}
if (transportHash) {
const computed = crypto.createHash('sha256').update(base64).digest('hex');
if (computed !== transportHash) {
return res.status(400).json({ error: 'Transport integrity check failed' });
}
}
const sanitized = sanitizeUsername(username);
const photosPath = getPhotosPath();
const userDir = path.join(photosPath, sanitized);
if (!fs.existsSync(userDir)) {
fs.mkdirSync(userDir, { recursive: true });
}
const safeFilename = filename.replace(/[^a-zA-Z0-9_\-.]/g, '_');
const destPath = path.join(userDir, safeFilename);
const buffer = Buffer.from(base64, 'base64');
fs.writeFileSync(destPath, buffer);
db.ensureUser(sanitized);
const result = db.indexPhotosForUser(sanitized, photosPath);
res.json({ success: true, filename: safeFilename, indexed: result.indexed });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ─── Delete Photo from NAS ──────────────────────────────────
app.post('/api/photo/delete', (req, res) => {
try {
const { photoId } = req.body;
if (!photoId) return res.status(400).json({ error: 'photoId is required' });
const result = db.deletePhoto(photoId);
res.json(result);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ─── Refresh (re-index + return list) ───────────────────────
app.post('/api/refresh', (req, res) => {
try {
const { username } = req.body;
if (!username) return res.status(400).json({ error: 'Username is required' });
const sanitized = sanitizeUsername(username);
const photosPath = getPhotosPath();
db.ensureUser(sanitized);
db.removeOrphanedEntries(sanitized, photosPath);
db.indexPhotosForUser(sanitized, photosPath);
const photos = db.getPhotoIndex(sanitized);
res.json({
success: true,
username: sanitized,
count: photos.length,
photos: photos.map(p => ({
id: p.id,
filename: p.filename,
sha256Hash: p.sha256_hash,
fileSize: p.file_size,
mimeType: p.mime_type,
indexedAt: p.indexed_at,
})),
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ─── Album Endpoints ────────────────────────────────────────
app.post('/api/albums', (req, res) => {
try {
const { username } = req.body;
if (!username) return res.status(400).json({ error: 'Username is required' });
const sanitized = sanitizeUsername(username);
const albums = db.getAlbums(sanitized);
res.json({ success: true, albums });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.post('/api/album/create', (req, res) => {
try {
const { username, name } = req.body;
if (!username || !name) return res.status(400).json({ error: 'username and name are required' });
const sanitized = sanitizeUsername(username);
const result = db.createAlbum(sanitized, name.trim());
res.json(result);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.post('/api/album/photos', (req, res) => {
try {
const { albumId } = req.body;
if (!albumId) return res.status(400).json({ error: 'albumId is required' });
const photos = db.getAlbumPhotos(albumId);
res.json({
success: true,
photos: photos.map(p => ({
id: p.id,
filename: p.filename,
sha256Hash: p.sha256_hash,
fileSize: p.file_size,
mimeType: p.mime_type,
indexedAt: p.indexed_at,
})),
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.post('/api/album/add-photo', (req, res) => {
try {
const { albumId, photoId } = req.body;
if (!albumId || !photoId) return res.status(400).json({ error: 'albumId and photoId are required' });
const result = db.addPhotoToAlbum(albumId, photoId);
res.json(result);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.post('/api/album/remove-photo', (req, res) => {
try {
const { albumId, photoId } = req.body;
if (!albumId || !photoId) return res.status(400).json({ error: 'albumId and photoId are required' });
const result = db.removePhotoFromAlbum(albumId, photoId);
res.json(result);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.post('/api/album/delete', (req, res) => {
try {
const { albumId } = req.body;
if (!albumId) return res.status(400).json({ error: 'albumId is required' });
const result = db.deleteAlbum(albumId);
res.json(result);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.post('/api/album/rename', (req, res) => {
try {
const { albumId, name } = req.body;
if (!albumId || !name) return res.status(400).json({ error: 'albumId and name are required' });
const result = db.renameAlbum(albumId, name.trim());
res.json(result);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ─── Users ──────────────────────────────────────────────────
app.get('/api/users', (req, res) => {
try {
const users = db.getAllUsers();
res.json({ success: true, users });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ─── Error handling ─────────────────────────────────────────
app.use((err, req, res, _next) => {
console.error('Server error:', err.message);
res.status(500).json({ error: 'Internal server error' });
});
app.use((req, res) => {
res.status(404).json({ error: 'Not found', path: req.path });
});
// ─── Lifecycle ──────────────────────────────────────────────
function startServer(port) {
const p = port || config.DEFAULT_PORT;
app.listen(p, '0.0.0.0', () => {
console.log(`[NAS Server] Running on http://0.0.0.0:${p}`);
console.log(`[NAS Server] Photos path: ${config.getPhotosPath() || 'NOT SET'}`);
});
}
if (require.main === module) {
const photosPath = config.getPhotosPath();
if (!photosPath) {
console.error('ERROR: Photos path not set. Write the path to your photos directory in nas_config.txt');
process.exit(1);
}
// Auto-create the photos directory if it doesn't exist yet
if (!fs.existsSync(photosPath)) {
fs.mkdirSync(photosPath, { recursive: true });
console.log(`[NAS Server] Created photos directory: ${photosPath}`);
}
startServer();
}
process.on('SIGINT', () => {
console.log('\nShutting down...');
db.closeDb();
process.exit(0);
});
module.exports = { app, startServer };