feat: add zip file extraction support with error handling for invalid archives
This commit is contained in:
@@ -19,6 +19,7 @@ import { ApiKeyManager } from "./ApiKeyManager.js";
|
||||
import { UserAiUsage } from "./UserAiUsage.js";
|
||||
import { Tabs } from "./Tabs.js";
|
||||
import { DeviiTerminal } from "./DeviiTerminal.js";
|
||||
import { ZipDownloader } from "./ZipDownloader.js";
|
||||
|
||||
class Application {
|
||||
constructor() {
|
||||
@@ -46,6 +47,7 @@ class Application {
|
||||
this.userAiUsage = new UserAiUsage();
|
||||
this.tabs = new Tabs();
|
||||
this.devii = new DeviiTerminal();
|
||||
this.zipDownloader = new ZipDownloader();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -358,6 +358,7 @@ export class ProjectFiles {
|
||||
items.push({ label: "Upload here", icon: "⬆", onSelect: () => this.uploadTo(dir) });
|
||||
}
|
||||
items.push({ separator: true });
|
||||
items.push({ label: "Download as zip", icon: "📦", onSelect: () => window.app.zipDownloader.start(`${this.base}/zip?path=${encodeURIComponent(path)}`) });
|
||||
items.push({ label: "Rename / move", icon: "✏", onSelect: () => this.rename(path) });
|
||||
items.push({ label: "Delete", icon: "🗑", danger: true, onSelect: () => this.remove(path) });
|
||||
return items;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import { Http } from "./Http.js";
|
||||
|
||||
export class ZipDownloader {
|
||||
constructor() {
|
||||
this.pollMs = 1500;
|
||||
this.maxPolls = 200;
|
||||
this.active = new Set();
|
||||
document.addEventListener("click", (event) => {
|
||||
const trigger = event.target.closest("[data-zip-download]");
|
||||
if (!trigger) return;
|
||||
event.preventDefault();
|
||||
this.start(trigger.dataset.zipDownload);
|
||||
});
|
||||
}
|
||||
|
||||
async start(enqueueUrl) {
|
||||
if (this.active.has(enqueueUrl)) return;
|
||||
this.active.add(enqueueUrl);
|
||||
window.app.toast.show("Preparing download...", { type: "info", ms: 4000 });
|
||||
try {
|
||||
const job = await Http.postJson(enqueueUrl, {});
|
||||
await this.poll(job.status_url);
|
||||
} catch (err) {
|
||||
window.app.toast.show("Could not start the download", { type: "error" });
|
||||
} finally {
|
||||
this.active.delete(enqueueUrl);
|
||||
}
|
||||
}
|
||||
|
||||
poll(statusUrl) {
|
||||
return new Promise((resolve) => {
|
||||
let attempts = 0;
|
||||
const timer = setInterval(async () => {
|
||||
attempts += 1;
|
||||
if (attempts > this.maxPolls) {
|
||||
clearInterval(timer);
|
||||
window.app.toast.show("Download timed out", { type: "error" });
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
let status;
|
||||
try {
|
||||
status = await Http.getJson(statusUrl);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (status.status === "done") {
|
||||
clearInterval(timer);
|
||||
this.trigger(status.download_url);
|
||||
window.app.toast.show("Download ready", { type: "success" });
|
||||
resolve();
|
||||
} else if (status.status === "failed") {
|
||||
clearInterval(timer);
|
||||
window.app.toast.show("Zip failed: " + (status.error || "unknown error"), { type: "error" });
|
||||
resolve();
|
||||
}
|
||||
}, this.pollMs);
|
||||
});
|
||||
}
|
||||
|
||||
trigger(url) {
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = "";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user