Addresses feedback from Gavin (VPM): - Sort folders alphabetically in both upload tree and admin tree views - Auto-select VPM as default folder on login instead of Root - Fix S3 key construction for nested folders: convert "/" to "--" so FLX correctly maps subfolders (e.g. Content/TEST - AMPP Demo now produces Content--TEST - AMPP Demo--file.ext instead of Content/TEST - AMPP Demo--file.ext) - Clarify HTTP mode note: "Files are processed 6 at a time" instead of "up to 6 concurrent files" which implied a total file limit Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
113 lines
4 KiB
JavaScript
113 lines
4 KiB
JavaScript
"use strict";
|
|
const crypto = require("crypto");
|
|
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
|
|
const { PutObjectCommand } = require("@aws-sdk/client-s3");
|
|
|
|
/**
|
|
* Dragon Wind Upload Manager
|
|
* Tracks upload sessions for both HTTP (presigned S3) and UDP (relay) modes.
|
|
*/
|
|
class UploadManager {
|
|
constructor(getS3Client, getConfig) {
|
|
// Accept getter functions so config stays dynamic
|
|
this._getS3 = getS3Client;
|
|
this._getConfig = getConfig;
|
|
this.sessions = new Map();
|
|
}
|
|
|
|
get config() { return this._getConfig(); }
|
|
get s3() { return this._getS3(); }
|
|
|
|
createSession({ filename, size, mode = "http", prefix = "" }) {
|
|
if (!["http", "udp"].includes(mode)) throw new Error("mode must be 'http' or 'udp'");
|
|
const sessionId = crypto.randomBytes(16).toString("hex");
|
|
// Normalize prefix: UI uses "/" for nested folders, FLX expects "--" as delimiter
|
|
const normalized = prefix ? prefix.replace(/\//g, "--").replace(/[-]+$/, "") : "";
|
|
const key = normalized ? `${normalized}--${filename}` : filename;
|
|
const session = {
|
|
sessionId, filename, size, mode, key, prefix,
|
|
status: "pending",
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
expiresAt: Date.now() + 2 * 60 * 60 * 1000, // 2h
|
|
presignedUrl: null,
|
|
uploadedBytes: 0,
|
|
s3Key: key,
|
|
error: null,
|
|
};
|
|
this.sessions.set(sessionId, session);
|
|
// Auto-cleanup
|
|
setTimeout(() => this.sessions.delete(sessionId), 2 * 60 * 60 * 1000);
|
|
return session;
|
|
}
|
|
|
|
getSession(sessionId) { return this.sessions.get(sessionId) || null; }
|
|
|
|
async getPresignedUrl(sessionId) {
|
|
const session = this.sessions.get(sessionId);
|
|
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
if (!this.s3) throw new Error("S3 client not initialized — configure S3 in Admin settings");
|
|
const cfg = this.config;
|
|
const cmd = new PutObjectCommand({ Bucket: cfg.bucket, Key: session.key });
|
|
const url = await getSignedUrl(this.s3, cmd, { expiresIn: 3600 });
|
|
session.presignedUrl = url;
|
|
session.updatedAt = Date.now();
|
|
return { url, key: session.key, bucket: cfg.bucket, sessionId };
|
|
}
|
|
|
|
async initializeUdpSession(sessionId) {
|
|
const session = this.sessions.get(sessionId);
|
|
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
const cfg = this.config;
|
|
if (!cfg.relayUrl) throw new Error("UDP relay not configured — set relay URL in Admin settings");
|
|
session.status = "udp_pending";
|
|
session.updatedAt = Date.now();
|
|
return {
|
|
sessionId,
|
|
relayUrl: cfg.relayUrl,
|
|
udpPort: cfg.udpPort || 5000,
|
|
key: session.key,
|
|
bucket: cfg.bucket,
|
|
filename: session.filename,
|
|
size: session.size,
|
|
};
|
|
}
|
|
|
|
completeUpload(sessionId, success = true, error = null) {
|
|
const session = this.sessions.get(sessionId);
|
|
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
session.status = success ? "completed" : "failed";
|
|
session.error = error;
|
|
session.updatedAt = Date.now();
|
|
return session;
|
|
}
|
|
|
|
getStats() {
|
|
const all = Array.from(this.sessions.values());
|
|
return {
|
|
total: all.length,
|
|
pending: all.filter((s) => s.status === "pending").length,
|
|
completed: all.filter((s) => s.status === "completed").length,
|
|
failed: all.filter((s) => s.status === "failed").length,
|
|
http: all.filter((s) => s.mode === "http").length,
|
|
udp: all.filter((s) => s.mode === "udp").length,
|
|
};
|
|
}
|
|
|
|
async getRelayHealth(relayUrl) {
|
|
const url = relayUrl || this.config.relayUrl;
|
|
if (!url) return { healthy: false, error: "Relay URL not configured" };
|
|
try {
|
|
const controller = new AbortController();
|
|
const t = setTimeout(() => controller.abort(), 5000);
|
|
const r = await fetch(`${url}/health`, { signal: controller.signal });
|
|
clearTimeout(t);
|
|
const data = await r.json().catch(() => ({}));
|
|
return { healthy: r.ok, status: r.status, ...data };
|
|
} catch (err) {
|
|
return { healthy: false, error: err.message };
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = UploadManager;
|