mirror of
https://github.com/mudabbir-ahmad/Media-Aggregation-and-Sorting-System.git
synced 2026-10-07 19:40:20 +00:00
254 lines
9.0 KiB
JavaScript
254 lines
9.0 KiB
JavaScript
const express = require('express');
|
|
const jwt = require('jsonwebtoken');
|
|
const cors = require('cors');
|
|
const path = require('path');
|
|
require('dotenv').config({ path: path.join(__dirname, '.env') });
|
|
|
|
const nasVaultStore = require('./nasVaultStore');
|
|
|
|
const fetch = (...args) => import('node-fetch').then(({ default: nodeFetch }) => nodeFetch(...args));
|
|
|
|
const app = express();
|
|
app.use(express.json({ limit: process.env.JSON_BODY_LIMIT || '150mb' }));
|
|
app.use(express.urlencoded({ extended: true, limit: process.env.JSON_BODY_LIMIT || '150mb' }));
|
|
app.use(cors());
|
|
|
|
const SECRET_KEY = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
|
|
const connections = new Map();
|
|
|
|
app.use((req, res, next) => {
|
|
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
|
|
next();
|
|
});
|
|
|
|
function verifyNasSession(req, res, next) {
|
|
const authHeader = req.headers.authorization;
|
|
const token = authHeader?.split(' ')[1];
|
|
if (!token) return res.status(401).json({ error: 'No token provided' });
|
|
|
|
try {
|
|
const decoded = jwt.verify(token, SECRET_KEY);
|
|
const conn = connections.get(decoded.connectionId);
|
|
if (!conn) return res.status(401).json({ error: 'Session expired or not found' });
|
|
req.connectionId = decoded.connectionId;
|
|
req.connection = conn;
|
|
req.vaultKey = conn.vaultKey;
|
|
next();
|
|
} catch (err) {
|
|
res.status(401).json({ error: 'Invalid token: ' + err.message });
|
|
}
|
|
}
|
|
|
|
app.get('/api/health', (req, res) => {
|
|
res.json({
|
|
status: 'ok',
|
|
timestamp: new Date().toISOString(),
|
|
activeSessions: connections.size,
|
|
nasMode: 'rest-crudl',
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Open a NAS client session (REST vault). Credentials scope the vault; media is stored
|
|
* server-side as base64 with preserved metadata (no SFTP / exifr pipeline).
|
|
*/
|
|
app.post('/api/nas/sessions', (req, res) => {
|
|
const { host, port = 22, username, password } = req.body;
|
|
if (!host || !username || !password) {
|
|
return res.status(400).json({ error: 'Missing required fields: host, username, password' });
|
|
}
|
|
|
|
const vaultKey = nasVaultStore.vaultKeyFromCredentials(host, username);
|
|
const connectionId = `nas_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
connections.set(connectionId, {
|
|
host,
|
|
port: Number(port) || 22,
|
|
username,
|
|
password,
|
|
vaultKey,
|
|
createdAt: new Date(),
|
|
});
|
|
|
|
const token = jwt.sign({ connectionId }, SECRET_KEY, { expiresIn: '24h' });
|
|
console.log(`NAS session: vault=${vaultKey.slice(0, 12)}… (${connectionId})`);
|
|
|
|
res.json({
|
|
success: true,
|
|
connectionToken: token,
|
|
message: 'NAS REST session established',
|
|
});
|
|
});
|
|
|
|
/** List — GET /api/nas/media?offset=&limit=&include=metadata|full */
|
|
app.get('/api/nas/media', verifyNasSession, (req, res) => {
|
|
try {
|
|
const offset = Math.max(0, parseInt(req.query.offset, 10) || 0);
|
|
const limit = Math.min(500, Math.max(1, parseInt(req.query.limit, 10) || 100));
|
|
const include = req.query.include === 'full' ? 'full' : 'metadata';
|
|
const { items, total } = nasVaultStore.list(req.vaultKey, { offset, limit, include });
|
|
res.json({ items, total, offset, limit, include });
|
|
} catch (err) {
|
|
console.error('NAS list error:', err.message);
|
|
res.status(500).json({ error: err.message || 'List failed' });
|
|
}
|
|
});
|
|
|
|
/** Read — GET /api/nas/media/:id?include=metadata|full */
|
|
app.get('/api/nas/media/:id', verifyNasSession, (req, res) => {
|
|
const row = nasVaultStore.getById(req.vaultKey, req.params.id);
|
|
if (!row) return res.status(404).json({ error: 'Media not found' });
|
|
const include = req.query.include === 'full' ? 'full' : 'metadata';
|
|
res.json(nasVaultStore.shapeRow(row, include));
|
|
});
|
|
|
|
/** Create — POST /api/nas/media */
|
|
app.post('/api/nas/media', verifyNasSession, (req, res) => {
|
|
try {
|
|
const { filename, mimeType, base64, metadata } = req.body || {};
|
|
if (!base64) return res.status(400).json({ error: 'Missing base64 in body' });
|
|
const created = nasVaultStore.create(req.vaultKey, { filename, mimeType, base64, metadata });
|
|
res.status(201).json(created);
|
|
} catch (err) {
|
|
console.error('NAS create error:', err.message);
|
|
res.status(400).json({ error: err.message || 'Create failed' });
|
|
}
|
|
});
|
|
|
|
/** Update — PUT /api/nas/media/:id */
|
|
app.put('/api/nas/media/:id', verifyNasSession, (req, res) => {
|
|
try {
|
|
const updated = nasVaultStore.update(req.vaultKey, req.params.id, req.body || {});
|
|
if (!updated) return res.status(404).json({ error: 'Media not found' });
|
|
res.json(updated);
|
|
} catch (err) {
|
|
console.error('NAS update error:', err.message);
|
|
res.status(400).json({ error: err.message || 'Update failed' });
|
|
}
|
|
});
|
|
|
|
/** Delete — DELETE /api/nas/media/:id */
|
|
app.delete('/api/nas/media/:id', verifyNasSession, (req, res) => {
|
|
const ok = nasVaultStore.remove(req.vaultKey, req.params.id);
|
|
if (!ok) return res.status(404).json({ error: 'Media not found' });
|
|
res.status(204).send();
|
|
});
|
|
|
|
/** End session (does not delete vault data) */
|
|
app.delete('/api/nas/sessions/current', verifyNasSession, (req, res) => {
|
|
connections.delete(req.connectionId);
|
|
res.json({ success: true, message: 'Session closed' });
|
|
});
|
|
|
|
app.post('/api/google-photos/fetch', verifyNasSession, async (req, res) => {
|
|
const { googleAccessToken, pageSize = 50, pageToken } = req.body;
|
|
if (!googleAccessToken) {
|
|
return res.status(400).json({ error: 'Google access token is required' });
|
|
}
|
|
|
|
try {
|
|
const params = new URLSearchParams({ pageSize: String(pageSize) });
|
|
if (pageToken) params.append('pageToken', pageToken);
|
|
|
|
const response = await fetch(
|
|
`https://photoslibrary.googleapis.com/v1/mediaItems?${params.toString()}`,
|
|
{ headers: { Authorization: `Bearer ${googleAccessToken}` } }
|
|
);
|
|
|
|
if (!response.ok) {
|
|
const errText = await response.text();
|
|
return res.status(response.status).json({ error: 'Google API error', details: errText });
|
|
}
|
|
|
|
const data = await response.json();
|
|
const mediaItems = (data.mediaItems || []).map((item) => ({
|
|
id: item.id,
|
|
filename: item.filename,
|
|
mimeType: item.mimeType,
|
|
baseUrl: item.baseUrl,
|
|
productUrl: item.productUrl,
|
|
creationTime: item.mediaMetadata?.creationTime,
|
|
width: item.mediaMetadata?.width,
|
|
height: item.mediaMetadata?.height,
|
|
}));
|
|
|
|
console.log(`Fetched ${mediaItems.length} Google Photos items via NAS proxy`);
|
|
res.json({ success: true, mediaItems, nextPageToken: data.nextPageToken || null, count: mediaItems.length });
|
|
} catch (err) {
|
|
console.error('Google Photos fetch error:', err.message);
|
|
res.status(500).json({ error: 'Failed to fetch Google Photos: ' + err.message });
|
|
}
|
|
});
|
|
|
|
/** Saves a Google image into the NAS vault as base64 with metadata (CRUD Create). */
|
|
app.post('/api/google-photos/download-to-nas', verifyNasSession, async (req, res) => {
|
|
const { googleAccessToken, baseUrl, filename, metadata = {} } = req.body;
|
|
if (!googleAccessToken || !baseUrl) {
|
|
return res.status(400).json({ error: 'googleAccessToken and baseUrl are required' });
|
|
}
|
|
|
|
try {
|
|
const imageResponse = await fetch(`${baseUrl}=d`, {
|
|
headers: { Authorization: `Bearer ${googleAccessToken}` },
|
|
});
|
|
if (!imageResponse.ok) {
|
|
return res.status(imageResponse.status).json({ error: 'Failed to download from Google Photos' });
|
|
}
|
|
|
|
const arrayBuffer = await imageResponse.arrayBuffer();
|
|
const base64 = Buffer.from(arrayBuffer).toString('base64');
|
|
const safeName = filename || `gphoto_${Date.now()}.jpg`;
|
|
const mimeType = imageResponse.headers.get('content-type') || 'image/jpeg';
|
|
|
|
const created = nasVaultStore.create(req.vaultKey, {
|
|
filename: safeName,
|
|
mimeType,
|
|
base64,
|
|
metadata: {
|
|
...metadata,
|
|
source: 'google_photos_offload',
|
|
width: metadata.width,
|
|
height: metadata.height,
|
|
dateTaken: metadata.creationTime || metadata.dateTaken,
|
|
},
|
|
});
|
|
|
|
console.log(`Saved Google Photo to NAS vault: ${created.id}`);
|
|
res.json({ success: true, media: created, message: 'Photo stored in NAS vault' });
|
|
} catch (err) {
|
|
console.error('Google → NAS error:', err.message);
|
|
res.status(500).json({ error: 'Failed to save photo to NAS vault: ' + err.message });
|
|
}
|
|
});
|
|
|
|
app.post('/api/google-photos/sync-status', verifyNasSession, (req, res) => {
|
|
res.json({ success: true, status: 'idle', message: 'No active sync in progress', lastSync: null });
|
|
});
|
|
|
|
process.on('SIGINT', () => {
|
|
console.log('\nShutting down...');
|
|
connections.clear();
|
|
process.exit(0);
|
|
});
|
|
|
|
app.use((err, req, res, _next) => {
|
|
console.error('Server error:', err.message);
|
|
res.status(500).json({
|
|
error: 'Internal server error',
|
|
message: process.env.NODE_ENV === 'development' ? err.message : 'Unknown error',
|
|
});
|
|
});
|
|
|
|
app.use((req, res) => {
|
|
res.status(404).json({ error: 'Not found', path: req.path });
|
|
});
|
|
|
|
const PORT = process.env.PORT || 3001;
|
|
app.listen(PORT, () => {
|
|
console.log(`\n NAS REST backend (CRUDL + base64 vault)`);
|
|
console.log(` Port: ${PORT}`);
|
|
console.log(` URL: http://localhost:${PORT}`);
|
|
console.log(` Status: Ready\n`);
|
|
});
|
|
|
|
module.exports = app;
|