feat(files): add folder download as zip
- Add zip folder API endpoint - Add download button to folder rows - Enforce max archive size limit - Document endpoint in OpenAPI
This commit is contained in:
+33
-9
@@ -162,6 +162,15 @@
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function downloadFolder(fullPath, folderName) {
|
||||
const link = document.createElement("a");
|
||||
link.href = `/api/v1/files/zip?path=${encodeURIComponent(fullPath)}`;
|
||||
link.download = `${folderName}.zip`;
|
||||
document.body.append(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
|
||||
async function deleteFile(fullPath, name) {
|
||||
if (!confirm(`Delete ${name}?`)) {
|
||||
return;
|
||||
@@ -184,21 +193,37 @@
|
||||
for (const entry of entries) {
|
||||
const tr = document.createElement("tr");
|
||||
const name = document.createElement("td");
|
||||
name.className = entry.type === "dir" ? "name" : "name file";
|
||||
name.textContent = entry.name;
|
||||
name.addEventListener("click", () => {
|
||||
const full = joinPath(data.path, entry.name);
|
||||
if (entry.type === "dir") {
|
||||
const full = joinPath(data.path, entry.name);
|
||||
if (entry.type === "dir") {
|
||||
name.className = "name dir";
|
||||
const label = document.createElement("span");
|
||||
label.className = "dir-label";
|
||||
label.textContent = entry.name;
|
||||
label.addEventListener("click", () => {
|
||||
listFiles(full).catch((err) => showError(err.message));
|
||||
} else {
|
||||
});
|
||||
const zipBtn = document.createElement("button");
|
||||
zipBtn.type = "button";
|
||||
zipBtn.className = "folder-zip";
|
||||
zipBtn.textContent = "Download";
|
||||
zipBtn.title = "Download folder as zip";
|
||||
zipBtn.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
downloadFolder(full, entry.name);
|
||||
});
|
||||
name.append(label, zipBtn);
|
||||
} else {
|
||||
name.className = "name file";
|
||||
name.textContent = entry.name;
|
||||
name.addEventListener("click", () => {
|
||||
const link = document.createElement("a");
|
||||
link.href = `/api/v1/download?path=${encodeURIComponent(full)}`;
|
||||
link.download = entry.name;
|
||||
document.body.append(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
const type = document.createElement("td");
|
||||
type.textContent = entry.type;
|
||||
const size = document.createElement("td");
|
||||
@@ -207,7 +232,6 @@
|
||||
modified.textContent = entry.modified_time ? new Date(entry.modified_time).toLocaleString() : "";
|
||||
const action = document.createElement("td");
|
||||
action.className = "actions";
|
||||
const full = joinPath(data.path, entry.name);
|
||||
const del = document.createElement("button");
|
||||
del.type = "button";
|
||||
del.className = "danger";
|
||||
|
||||
@@ -543,6 +543,20 @@
|
||||
tbody tr:hover td { background: rgba(94, 234, 212, 0.04); }
|
||||
tbody tr:last-child td { border-bottom: 0; }
|
||||
td.name { cursor: pointer; color: var(--accent); font-weight: 500; }
|
||||
td.name.dir { display: flex; align-items: center; gap: 0.5rem; }
|
||||
td.name .dir-label { flex: 1; min-width: 0; }
|
||||
td.name .folder-zip {
|
||||
flex-shrink: 0;
|
||||
padding: 0.15rem 0.45rem;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
td.name .folder-zip:hover { color: var(--accent-bright); border-color: var(--accent-dim); }
|
||||
td.name.file { color: var(--text); font-weight: 400; }
|
||||
td.name.file:hover { color: var(--accent-bright); }
|
||||
td.actions { white-space: nowrap; text-align: right; }
|
||||
|
||||
Reference in New Issue
Block a user