mirror of
https://github.com/mudabbir-ahmad/Media-Aggregation-and-Sorting-System.git
synced 2026-10-07 19:40:20 +00:00
385 lines
12 KiB
JavaScript
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 };
|