- 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 {