Fix folder sorting, default selection, subfolder S3 keys, and HTTP mode description
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>
This commit is contained in:
parent
5b0a3ef2cc
commit
84cf9cccbe
3 changed files with 28 additions and 10 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -467,7 +467,7 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
|
|||
<button class="mode-btn" id="btn-udp" disabled style="opacity:.4;cursor:not-allowed;pointer-events:none">🖥️ Electron App</button>
|
||||
</div>
|
||||
<div class="mode-desc" id="mode-desc" style="margin-bottom:0">
|
||||
<strong id="mode-label">HTTP Mode:</strong> <span id="mode-detail">Direct-to-S3 upload via presigned URLs (up to 6 concurrent files). Browser uploads straight to storage, bypassing the server.</span>
|
||||
<strong id="mode-label">HTTP Mode:</strong> <span id="mode-detail">Direct-to-S3 upload via presigned URLs. Browser uploads straight to storage, bypassing the server. Files are processed 6 at a time.</span>
|
||||
</div>
|
||||
<div style="text-align:right;margin-top:.3rem;font-size:.7rem;color:var(--text-dim);opacity:.6">Electron App — Aspera-speed desktop application (coming soon)</div>
|
||||
</div>
|
||||
|
|
@ -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='<div style="color:var(--text-dim);font-size:.8rem;padding:.5rem">No folders. Add one below.</div>';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');
|
||||
|
|
|
|||
19
server.js
19
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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue