diff --git a/lib/upload-manager.js b/lib/upload-manager.js index 0eb6148..0a64c7b 100644 --- a/lib/upload-manager.js +++ b/lib/upload-manager.js @@ -21,7 +21,9 @@ class UploadManager { 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"); - const key = prefix ? `${prefix.replace(/[-\/]+$/, "")}--${filename}` : filename; + // 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", diff --git a/public/index.html b/public/index.html index d5c97d2..7a4a818 100644 --- a/public/index.html +++ b/public/index.html @@ -467,7 +467,7 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
- HTTP Mode: Direct-to-S3 upload via presigned URLs (up to 6 concurrent files). Browser uploads straight to storage, bypassing the server. + HTTP Mode: Direct-to-S3 upload via presigned URLs. Browser uploads straight to storage, bypassing the server. Files are processed 6 at a time.
Electron App — Aspera-speed desktop application (coming soon)
@@ -946,7 +946,7 @@ function setMode(mode) { if (mode === 'http') { btn.className = 'btn-upload'; if (label) label.textContent = 'HTTP Mode:'; - if (detail) detail.textContent = 'Direct-to-S3 upload via presigned URLs (up to 6 concurrent files). Browser uploads straight to storage, bypassing the server.'; + if (detail) detail.textContent = 'Direct-to-S3 upload via presigned URLs. Browser uploads straight to storage, bypassing the server. Files are processed 6 at a time.'; if (hint) hint.style.display = 'none'; if (btnHttp) { btnHttp.className = 'mode-btn active-http'; } if (btnUdp) { btnUdp.className = 'mode-btn'; } @@ -974,6 +974,11 @@ async function loadFolders() { try { const d = await api('GET','/api/folders'); folderTree = d.tree || []; + // Auto-select VPM as default folder if nothing is selected yet + if (!selectedPrefix && folderTree.some(n => n.name === 'VPM')) { + selectedPrefix = 'VPM'; + updatePrefixDisplay(); + } renderFolderTree(); } catch(e) { console.error('loadFolders:',e); } } @@ -1003,7 +1008,8 @@ function renderFolderTree() { } function addRows(nodes, pathArr, container) { - nodes.forEach(n => { + const sorted = [...nodes].sort((a, b) => a.name.localeCompare(b.name, undefined, {sensitivity: 'base'})); + sorted.forEach(n => { if (filter && !matchesFilter(n, pathArr)) return; const fullPath = [...pathArr, n.name]; const key = fullPath.join('/'); @@ -1702,7 +1708,8 @@ async function loadAdminFolders() { function renderAdminFolderTree(nodes,container,pathArr) { container.innerHTML=''; if(!nodes.length){container.innerHTML='
No folders. Add one below.
';return;} - nodes.forEach(n=>{ + const sorted=[...nodes].sort((a,b)=>a.name.localeCompare(b.name,undefined,{sensitivity:'base'})); + sorted.forEach(n=>{ const fp=[...pathArr,n.name]; const div=document.createElement('div');div.style.paddingLeft=pathArr.length*20+'px';div.style.marginBottom='.3rem'; const row=document.createElement('div'); diff --git a/server.js b/server.js index 66adf76..55741b0 100644 --- a/server.js +++ b/server.js @@ -186,6 +186,15 @@ function cleanName(s) { return (s || "").trim().replace(/[^a-zA-Z0-9'\-.,&()! ]/g, ""); } +// Build S3 object key from a folder prefix and filename. +// The prefix may use "/" to separate nested folders (from the UI tree), +// but FLX expects "--" as the folder delimiter in object keys. +function buildS3Key(prefix, filename) { + if (!prefix) return filename; + const normalized = prefix.replace(/\//g, "--").replace(/[-]+$/, ""); + return `${normalized}--${filename}`; +} + function findNode(pathArr) { let current = getTree(); for (const segment of pathArr) { @@ -532,7 +541,7 @@ app.post("/api/upload", requireAuth, upload.array("files", 50), async (req, res) } const bucket = db.s3Config?.bucket || ""; for (const file of req.files) { - const key = prefix ? `${prefix.replace(/[-\/]+$/, "")}--${file.originalname}` : file.originalname; + const key = buildS3Key(prefix, file.originalname); const contentType = getMimeType(file.originalname, file.mimetype); console.log(`Uploading: ${key} (${file.size} bytes, ${contentType})`); // Use PutObjectCommand (single PUT) — compatible with RustFS/MinIO/generic S3. @@ -579,7 +588,7 @@ app.post("/api/presigned", requireAuth, async (req, res) => { } const bucket = db.s3Config?.bucket || ""; - const key = prefix ? `${prefix.replace(/[-\/]+$/, "")}--${filename}` : filename; + const key = buildS3Key(prefix, filename); const mime = contentType || getMimeType(filename, "application/octet-stream"); try { const url = await getSignedUrl(s3Client, new PutObjectCommand({ Bucket: bucket, Key: key, ContentType: mime }), { expiresIn: 3600 }); @@ -614,7 +623,7 @@ app.post("/api/udp/session", requireAuth, async (req, res) => { if (!relayUrl) return res.status(503).json({ success: false, error: "UDP relay not configured. Go to Admin → Relay Settings." }); const publicRelayUrl = (db.relayConfig?.publicRelayUrl || "").trim() || relayUrl; const sessionId = crypto.randomBytes(16).toString("hex"); - const key = (prefix ? `${prefix.replace(/[-\/]+$/, "")}--${filename}` : filename); + const key = buildS3Key(prefix, filename); const s3Cfg = db.s3Config || {}; // Register session on the relay server so it can accept chunks and upload to S3 @@ -873,7 +882,7 @@ app.post("/api/sharelinks/:token/upload", upload.array("files", 50), async (req, const bucket = db.s3Config?.bucket || ""; const results = []; for (const file of req.files) { - const key = prefix ? `${prefix.replace(/[-\/]+$/, "")}--${file.originalname}` : file.originalname; + const key = buildS3Key(prefix, file.originalname); const contentType = getMimeType(file.originalname, file.mimetype); const fileBuffer = fs.readFileSync(file.path); const putPromise = s3Client.send(new PutObjectCommand({ @@ -964,7 +973,7 @@ app.post("/api/upload/initiate", requireAuth, async (req, res) => { const bucket = db.s3Config?.bucket || ""; if (!bucket) return res.status(503).json({ success: false, error: "S3 bucket not configured" }); - const key = prefix ? `${prefix.replace(/[-\/]+$/, "")}--${filename}` : filename; + const key = buildS3Key(prefix, filename); const mime = contentType || getMimeType(filename, "application/octet-stream"); try {