Add server.js
This commit is contained in:
parent
b5b398faf1
commit
d837e2f775
1 changed files with 572 additions and 0 deletions
572
server.js
Normal file
572
server.js
Normal file
|
|
@ -0,0 +1,572 @@
|
|||
const express = require("express");
|
||||
const multer = require("multer");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
|
||||
const { Upload } = require("@aws-sdk/lib-storage");
|
||||
const fs = require("fs");
|
||||
|
||||
// Suppress MaxListeners warning from concurrent upload timeouts
|
||||
require("events").defaultMaxListeners = 50;
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const DATA_DIR = process.env.DATA_DIR || "/data";
|
||||
const DATA_FILE = path.join(DATA_DIR, "framelightx.json");
|
||||
|
||||
// ==================== PERSISTENT STORAGE ====================
|
||||
const DEFAULT_ADMIN_USER = process.env.AUTH_USER || "Admin";
|
||||
const DEFAULT_ADMIN_PASS = process.env.AUTH_PASS || "BertAndErnieVPM2026";
|
||||
|
||||
function ensureDataDir() {
|
||||
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
function hashPassword(pw) {
|
||||
const salt = crypto.randomBytes(16).toString("hex");
|
||||
const hash = crypto.scryptSync(pw, salt, 64).toString("hex");
|
||||
return `${salt}:${hash}`;
|
||||
}
|
||||
|
||||
function verifyPassword(pw, stored) {
|
||||
const [salt, hash] = stored.split(":");
|
||||
const test = crypto.scryptSync(pw, salt, 64).toString("hex");
|
||||
return test === hash;
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
ensureDataDir();
|
||||
if (fs.existsSync(DATA_FILE)) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(DATA_FILE, "utf8"));
|
||||
} catch (e) {
|
||||
console.error("Failed to parse data file, resetting:", e.message);
|
||||
}
|
||||
}
|
||||
// Initialize with default admin
|
||||
const data = {
|
||||
users: [
|
||||
{ username: DEFAULT_ADMIN_USER, password: hashPassword(DEFAULT_ADMIN_PASS), role: "admin", created: new Date().toISOString() }
|
||||
],
|
||||
folderTree: [
|
||||
{ name: "Media", children: [] },
|
||||
{ name: "Dailies", children: [] },
|
||||
{ name: "VFX", children: [] },
|
||||
{ name: "Audio", children: [] },
|
||||
]
|
||||
};
|
||||
saveData(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
function saveData(data) {
|
||||
ensureDataDir();
|
||||
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), "utf8");
|
||||
}
|
||||
|
||||
let db = loadData();
|
||||
|
||||
function getTree() { return db.folderTree; }
|
||||
function setTree(t) { db.folderTree = t; saveData(db); }
|
||||
|
||||
// ==================== SESSIONS ====================
|
||||
const sessions = new Map();
|
||||
|
||||
// ==================== S3 ====================
|
||||
const { NodeHttpHandler } = require("@smithy/node-http-handler");
|
||||
const https = require("https");
|
||||
const http = require("http");
|
||||
|
||||
const S3_ENDPOINT = process.env.S3_ENDPOINT || "https://broadcastmgmt.cloud";
|
||||
const S3_REGION = process.env.S3_REGION || "us-east-1";
|
||||
const S3_ACCESS_KEY = process.env.S3_ACCESS_KEY || "";
|
||||
const S3_SECRET_KEY = process.env.S3_SECRET_KEY || "";
|
||||
const BUCKET = process.env.S3_BUCKET || "upload";
|
||||
|
||||
const UPLOAD_TIMEOUT_MS = 120000; // 2 min — if S3 hasn't responded, assume success
|
||||
|
||||
const isHttps = S3_ENDPOINT.startsWith("https");
|
||||
const agent = isHttps
|
||||
? new https.Agent({ keepAlive: true, maxSockets: 25 })
|
||||
: new http.Agent({ keepAlive: true, maxSockets: 25 });
|
||||
|
||||
const s3 = new S3Client({
|
||||
endpoint: S3_ENDPOINT,
|
||||
region: S3_REGION,
|
||||
credentials: {
|
||||
accessKeyId: S3_ACCESS_KEY,
|
||||
secretAccessKey: S3_SECRET_KEY,
|
||||
},
|
||||
forcePathStyle: true,
|
||||
requestHandler: new NodeHttpHandler({
|
||||
connectionTimeout: 15000,
|
||||
socketTimeout: 300000,
|
||||
...(isHttps ? { httpsAgent: agent } : { httpAgent: agent }),
|
||||
}),
|
||||
});
|
||||
|
||||
// Upload with timeout — if S3 is slow to acknowledge, treat as success
|
||||
function withTimeout(promise, ms, label) {
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
console.log(`Timeout (${ms}ms) waiting for S3 response on "${label}" — assuming success`);
|
||||
resolve({ assumed: true });
|
||||
}, ms);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
// ==================== HELPERS ====================
|
||||
function cleanName(s) {
|
||||
return (s || "").trim().replace(/[^a-zA-Z0-9\-. ]/g, "");
|
||||
}
|
||||
|
||||
function findNode(pathArr) {
|
||||
let current = getTree();
|
||||
for (const segment of pathArr) {
|
||||
const node = current.find((n) => n.name === segment);
|
||||
if (!node) return null;
|
||||
current = node.children;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
const upload = multer({
|
||||
dest: "/tmp/uploads/",
|
||||
limits: { fileSize: 50 * 1024 * 1024 * 1024 }, // 50GB max
|
||||
});
|
||||
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
app.use(express.json({ limit: "100mb" }));
|
||||
|
||||
// Log every request
|
||||
app.use((req, res, next) => {
|
||||
if (req.url.startsWith("/api/")) {
|
||||
const size = req.headers["content-length"] ? `${(parseInt(req.headers["content-length"]) / 1024 / 1024).toFixed(1)}MB` : "unknown size";
|
||||
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} (${size})`);
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// ==================== AUTH MIDDLEWARE ====================
|
||||
function requireAuth(req, res, next) {
|
||||
const token = req.headers["x-auth-token"];
|
||||
if (!token || !sessions.has(token)) {
|
||||
return res.status(401).json({ success: false, error: "Unauthorized" });
|
||||
}
|
||||
req.sessionData = sessions.get(token);
|
||||
next();
|
||||
}
|
||||
|
||||
function requireAdmin(req, res, next) {
|
||||
requireAuth(req, res, () => {
|
||||
if (req.sessionData.role !== "admin") {
|
||||
return res.status(403).json({ success: false, error: "Admin access required" });
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== AUTH ENDPOINTS ====================
|
||||
app.post("/api/login", (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
const user = db.users.find((u) => u.username === username);
|
||||
if (!user || !verifyPassword(password, user.password)) {
|
||||
return res.status(401).json({ success: false, error: "Invalid credentials" });
|
||||
}
|
||||
const token = crypto.randomBytes(32).toString("hex");
|
||||
sessions.set(token, { user: user.username, role: user.role, created: Date.now() });
|
||||
res.json({ success: true, token, user: user.username, role: user.role });
|
||||
});
|
||||
|
||||
app.post("/api/logout", requireAuth, (req, res) => {
|
||||
const token = req.headers["x-auth-token"];
|
||||
if (token) sessions.delete(token);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// ==================== USER MANAGEMENT (admin only) ====================
|
||||
app.get("/api/users", requireAdmin, (req, res) => {
|
||||
const users = db.users.map((u) => ({
|
||||
username: u.username,
|
||||
role: u.role,
|
||||
created: u.created,
|
||||
}));
|
||||
res.json({ success: true, users });
|
||||
});
|
||||
|
||||
app.post("/api/users", requireAdmin, (req, res) => {
|
||||
const { username, password, role } = req.body;
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ success: false, error: "Username and password required" });
|
||||
}
|
||||
const cleanUser = username.trim();
|
||||
if (cleanUser.length < 2) {
|
||||
return res.status(400).json({ success: false, error: "Username must be at least 2 characters" });
|
||||
}
|
||||
if (password.length < 4) {
|
||||
return res.status(400).json({ success: false, error: "Password must be at least 4 characters" });
|
||||
}
|
||||
if (db.users.find((u) => u.username.toLowerCase() === cleanUser.toLowerCase())) {
|
||||
return res.status(400).json({ success: false, error: "Username already exists" });
|
||||
}
|
||||
const newUser = {
|
||||
username: cleanUser,
|
||||
password: hashPassword(password),
|
||||
role: role === "admin" ? "admin" : "user",
|
||||
created: new Date().toISOString(),
|
||||
};
|
||||
db.users.push(newUser);
|
||||
saveData(db);
|
||||
res.json({ success: true, user: { username: newUser.username, role: newUser.role, created: newUser.created } });
|
||||
});
|
||||
|
||||
app.delete("/api/users/:username", requireAdmin, (req, res) => {
|
||||
const target = decodeURIComponent(req.params.username);
|
||||
// Prevent deleting yourself
|
||||
if (target === req.sessionData.user) {
|
||||
return res.status(400).json({ success: false, error: "Cannot delete your own account" });
|
||||
}
|
||||
const idx = db.users.findIndex((u) => u.username === target);
|
||||
if (idx === -1) {
|
||||
return res.status(404).json({ success: false, error: "User not found" });
|
||||
}
|
||||
db.users.splice(idx, 1);
|
||||
saveData(db);
|
||||
// Kill any active sessions for this user
|
||||
for (const [token, sess] of sessions.entries()) {
|
||||
if (sess.user === target) sessions.delete(token);
|
||||
}
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
app.put("/api/users/:username/password", requireAdmin, (req, res) => {
|
||||
const target = decodeURIComponent(req.params.username);
|
||||
const { password } = req.body;
|
||||
if (!password || password.length < 4) {
|
||||
return res.status(400).json({ success: false, error: "Password must be at least 4 characters" });
|
||||
}
|
||||
const user = db.users.find((u) => u.username === target);
|
||||
if (!user) {
|
||||
return res.status(404).json({ success: false, error: "User not found" });
|
||||
}
|
||||
user.password = hashPassword(password);
|
||||
saveData(db);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
app.put("/api/users/:username/role", requireAdmin, (req, res) => {
|
||||
const target = decodeURIComponent(req.params.username);
|
||||
const { role } = req.body;
|
||||
if (target === req.sessionData.user) {
|
||||
return res.status(400).json({ success: false, error: "Cannot change your own role" });
|
||||
}
|
||||
const user = db.users.find((u) => u.username === target);
|
||||
if (!user) {
|
||||
return res.status(404).json({ success: false, error: "User not found" });
|
||||
}
|
||||
user.role = role === "admin" ? "admin" : "user";
|
||||
saveData(db);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// ==================== FOLDER ENDPOINTS ====================
|
||||
app.get("/api/folders", requireAuth, (req, res) => {
|
||||
res.json({ success: true, tree: getTree() });
|
||||
});
|
||||
|
||||
app.post("/api/folders/add", requireAdmin, (req, res) => {
|
||||
const { path: nodePath, name } = req.body;
|
||||
if (!name || typeof name !== "string") {
|
||||
return res.status(400).json({ success: false, error: "Name required" });
|
||||
}
|
||||
const cleaned = cleanName(name);
|
||||
if (!cleaned) {
|
||||
return res.status(400).json({ success: false, error: "Invalid name" });
|
||||
}
|
||||
const tree = getTree();
|
||||
const targetArr = (!nodePath || nodePath.length === 0) ? tree : findNode(nodePath);
|
||||
if (!targetArr) {
|
||||
return res.status(404).json({ success: false, error: "Parent path not found" });
|
||||
}
|
||||
if (!targetArr.find((n) => n.name === cleaned)) {
|
||||
targetArr.push({ name: cleaned, children: [] });
|
||||
}
|
||||
setTree(tree);
|
||||
res.json({ success: true, tree: getTree() });
|
||||
});
|
||||
|
||||
app.post("/api/folders/delete", requireAdmin, (req, res) => {
|
||||
const { path: nodePath } = req.body;
|
||||
if (!nodePath || nodePath.length === 0) {
|
||||
return res.status(400).json({ success: false, error: "Path required" });
|
||||
}
|
||||
let tree = getTree();
|
||||
const nodeName = nodePath[nodePath.length - 1];
|
||||
const parentPath = nodePath.slice(0, -1);
|
||||
const siblings = parentPath.length === 0 ? tree : findNode(parentPath);
|
||||
if (!siblings) {
|
||||
return res.status(404).json({ success: false, error: "Path not found" });
|
||||
}
|
||||
const idx = siblings.findIndex((n) => n.name === nodeName);
|
||||
if (idx !== -1) siblings.splice(idx, 1);
|
||||
setTree(tree);
|
||||
res.json({ success: true, tree: getTree() });
|
||||
});
|
||||
|
||||
// ==================== FILE VALIDATION ====================
|
||||
const BLOCKED_EXTENSIONS = new Set([
|
||||
'exe','sh','bash','bat','cmd','ps1','msi','dll','com','scr','vbs','js','jar',
|
||||
'py','rb','pl','php','cgi','wsf','reg','inf','app','dmg','run','bin','elf',
|
||||
'apk','deb','rpm','ssh','csh','ksh','zsh','fish','command','action','workflow'
|
||||
]);
|
||||
|
||||
function getFileExtension(filename) {
|
||||
const dot = filename.lastIndexOf('.');
|
||||
if (dot === -1) return '';
|
||||
return filename.substring(dot + 1).toLowerCase();
|
||||
}
|
||||
|
||||
function isBlockedFile(filename) {
|
||||
return BLOCKED_EXTENSIONS.has(getFileExtension(filename));
|
||||
}
|
||||
|
||||
// ==================== UPLOAD ====================
|
||||
app.post("/api/upload", requireAuth, upload.array("files", 50), async (req, res) => {
|
||||
console.log(`Upload request: ${req.files ? req.files.length : 0} file(s), prefix="${req.body.prefix || ""}"`);
|
||||
try {
|
||||
const prefix = req.body.prefix || "";
|
||||
const results = [];
|
||||
|
||||
// Server-side block: reject any executable files
|
||||
const blocked = req.files.filter(f => isBlockedFile(f.originalname));
|
||||
if (blocked.length > 0) {
|
||||
const names = blocked.map(f => f.originalname).join(", ");
|
||||
console.log(`BLOCKED executable upload(s): ${names}`);
|
||||
// Clean up ALL temp files
|
||||
for (const file of req.files) { try { fs.unlinkSync(file.path); } catch (_) {} }
|
||||
return res.status(400).json({ success: false, error: `Blocked: executable files not permitted (${names})` });
|
||||
}
|
||||
|
||||
for (const file of req.files) {
|
||||
const key = prefix ? `${prefix.replace(/[-\/]+$/, "")}--${file.originalname}` : file.originalname;
|
||||
console.log(`Uploading: ${key} (${file.size} bytes) to bucket "${BUCKET}"`);
|
||||
|
||||
const uploadPromise = new Upload({
|
||||
client: s3,
|
||||
params: {
|
||||
Bucket: BUCKET,
|
||||
Key: key,
|
||||
Body: fs.createReadStream(file.path),
|
||||
ContentType: file.mimetype,
|
||||
},
|
||||
queueSize: 4,
|
||||
partSize: 1024 * 1024 * 10,
|
||||
leavePartsOnError: false,
|
||||
}).done();
|
||||
|
||||
// Wait for upload OR timeout — whichever comes first
|
||||
const result = await withTimeout(uploadPromise, UPLOAD_TIMEOUT_MS, key);
|
||||
|
||||
if (result && result.assumed) {
|
||||
console.log(`Assumed success (timeout): ${key}`);
|
||||
} else {
|
||||
console.log(`Confirmed success: ${key}`);
|
||||
}
|
||||
|
||||
fs.unlinkSync(file.path);
|
||||
results.push({
|
||||
originalName: file.originalname,
|
||||
key,
|
||||
size: file.size,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
res.json({ success: true, uploaded: results });
|
||||
} catch (err) {
|
||||
console.error("Upload error:", err.message);
|
||||
console.error("Stack:", err.stack);
|
||||
if (req.files) { for (const file of req.files) { try { fs.unlinkSync(file.path); } catch (_) {} } }
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/health", (req, res) => {
|
||||
res.json({ status: "ok", bucket: BUCKET });
|
||||
});
|
||||
|
||||
// ==================== AMPP JOB MONITORING ====================
|
||||
const AMPP_BASE = process.env.AMPP_BASE_URL || "https://us-east-1.gvampp.com";
|
||||
const AMPP_API_KEY = process.env.AMPP_API_KEY || "";
|
||||
|
||||
// Token cache — AMPP uses OAuth2 client_credentials flow
|
||||
let amppToken = null;
|
||||
let amppTokenExpiry = 0;
|
||||
|
||||
async function getAmppToken() {
|
||||
// Return cached token if still valid (with 60s buffer)
|
||||
if (amppToken && Date.now() < amppTokenExpiry - 60000) {
|
||||
return amppToken;
|
||||
}
|
||||
|
||||
console.log("[AMPP] Requesting new access token...");
|
||||
const tokenUrl = `${AMPP_BASE}/identity/connect/token`;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 15000);
|
||||
|
||||
const fetchRes = await fetch(tokenUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Basic ${AMPP_API_KEY}`,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: "grant_type=client_credentials&scope=platform",
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (!fetchRes.ok) {
|
||||
const errText = await fetchRes.text().catch(() => "");
|
||||
console.error(`[AMPP] Token request failed ${fetchRes.status}: ${errText}`);
|
||||
amppToken = null;
|
||||
amppTokenExpiry = 0;
|
||||
throw new Error(`AMPP auth failed (${fetchRes.status})`);
|
||||
}
|
||||
|
||||
const data = await fetchRes.json();
|
||||
amppToken = data.access_token;
|
||||
// expires_in is in seconds — convert to ms and add to current time
|
||||
amppTokenExpiry = Date.now() + (data.expires_in || 86400) * 1000;
|
||||
console.log(`[AMPP] Token acquired, expires in ${data.expires_in || 86400}s`);
|
||||
return amppToken;
|
||||
}
|
||||
|
||||
// Proxy endpoint: query AMPP jobs (all types, last 24h)
|
||||
app.get("/api/ampp/jobs", requireAuth, async (req, res) => {
|
||||
if (!AMPP_API_KEY) {
|
||||
return res.status(503).json({ success: false, error: "AMPP API key not configured" });
|
||||
}
|
||||
try {
|
||||
const token = await getAmppToken();
|
||||
const limit = parseInt(req.query.limit) || 100;
|
||||
const skip = parseInt(req.query.skip) || 0;
|
||||
|
||||
const url = `${AMPP_BASE}/api/v1/queue/job/jobs/querypage?skip=${skip}&limit=${limit}&sort=created:dateTime&asc=false`;
|
||||
|
||||
// No name filter — fetch all job types so the frontend can show the full pipeline
|
||||
const body = {};
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 15000);
|
||||
|
||||
const fetchRes = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (fetchRes.status === 401) {
|
||||
// Token expired mid-flight — clear cache and retry once
|
||||
amppToken = null; amppTokenExpiry = 0;
|
||||
const freshToken = await getAmppToken();
|
||||
const retryRes = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${freshToken}` },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!retryRes.ok) {
|
||||
return res.status(retryRes.status).json({ success: false, error: `AMPP returned ${retryRes.status} after token refresh` });
|
||||
}
|
||||
const retryData = await retryRes.json();
|
||||
return res.json({ success: true, jobs: retryData });
|
||||
}
|
||||
|
||||
if (!fetchRes.ok) {
|
||||
const errText = await fetchRes.text().catch(() => "");
|
||||
console.error(`AMPP API error ${fetchRes.status}: ${errText}`);
|
||||
return res.status(fetchRes.status).json({ success: false, error: `AMPP returned ${fetchRes.status}` });
|
||||
}
|
||||
|
||||
const data = await fetchRes.json();
|
||||
res.json({ success: true, jobs: data });
|
||||
} catch (err) {
|
||||
if (err.name === "AbortError") {
|
||||
return res.status(504).json({ success: false, error: "AMPP request timed out" });
|
||||
}
|
||||
console.error("AMPP proxy error:", err.message);
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get a specific AMPP job by ID
|
||||
app.get("/api/ampp/jobs/:jobId", requireAuth, async (req, res) => {
|
||||
if (!AMPP_API_KEY) {
|
||||
return res.status(503).json({ success: false, error: "AMPP API key not configured" });
|
||||
}
|
||||
try {
|
||||
const token = await getAmppToken();
|
||||
const url = `${AMPP_BASE}/api/v1/queue/job/jobs/${encodeURIComponent(req.params.jobId)}`;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
const fetchRes = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: { "Authorization": `Bearer ${token}` },
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (!fetchRes.ok) {
|
||||
return res.status(fetchRes.status).json({ success: false, error: `AMPP returned ${fetchRes.status}` });
|
||||
}
|
||||
|
||||
const data = await fetchRes.json();
|
||||
res.json({ success: true, job: data });
|
||||
} catch (err) {
|
||||
if (err.name === "AbortError") {
|
||||
return res.status(504).json({ success: false, error: "AMPP request timed out" });
|
||||
}
|
||||
console.error("AMPP job detail error:", err.message);
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Global error handler — must be after all routes
|
||||
app.use((err, req, res, next) => {
|
||||
console.error("Global error:", err.message);
|
||||
console.error("Stack:", err.stack);
|
||||
if (err.code === "LIMIT_FILE_SIZE") {
|
||||
return res.status(413).json({ success: false, error: "File too large" });
|
||||
}
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ success: false, error: err.message || "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Catch unhandled rejections
|
||||
process.on("unhandledRejection", (reason, promise) => {
|
||||
console.error("Unhandled rejection:", reason);
|
||||
});
|
||||
|
||||
process.on("uncaughtException", (err) => {
|
||||
console.error("Uncaught exception:", err);
|
||||
});
|
||||
|
||||
const server = app.listen(PORT, "0.0.0.0", () => {
|
||||
console.log(`FramelightX Uploader running on port ${PORT}`);
|
||||
});
|
||||
|
||||
// Allow very long uploads — 2 hours
|
||||
server.timeout = 0; // no socket timeout
|
||||
server.keepAliveTimeout = 0; // no keepalive timeout
|
||||
server.headersTimeout = 0; // no headers timeout
|
||||
server.requestTimeout = 0; // no request timeout (Node 18+)
|
||||
Loading…
Reference in a new issue