feat: Datei-Manager Multi-Select + Bulk-Download (ZIP) + Bulk-Delete
Diagnostic + App bekommen Mehrfach-Auswahl im Datei-Manager. Mehr als eine
Datei ausgewaehlt → Download als ZIP. Genau eine ausgewaehlt → einzeln.
Bulk-Delete loescht alle markierten in einem Rutsch.
diagnostic/Dockerfile
zip via apk add — fuer das ZIP-Streaming im /api/files-download-zip.
diagnostic/server.js
POST /api/files-download-zip Body: {paths:[...]} → spawnt 'zip -j -q -',
Pipes stdout in Response. Whitelist auf
/shared/uploads/.
POST /api/files-delete-batch Body: {paths:[...]} → loescht alle, broadcastet
file_deleted pro Pfad an Browser + RVS.
diagnostic/index.html
filesSelected Set + Checkbox-UI pro Datei + "Alle markieren". Wenn 2+
ausgewaehlt: POST an /api/files-download-zip, Browser saugt das als
Blob runter. Bei 1: normaler Single-Download.
bridge/aria_bridge.py
file_delete_batch_request → ruft Diagnostic /api/files-delete-batch,
antwortet mit file_delete_batch_response.
file_zip_request {paths,reqId} → ruft Diagnostic /api/files-download-zip,
base64-kodiert, capped auf 30 MB,
sendet file_zip_response.
rvs/server.js
ALLOWED_TYPES: file_delete_batch_request/response, file_zip_request/response.
android/src/screens/SettingsScreen.tsx
fileManagerSelected Set + Checkbox-UI pro Datei + "Alle markieren"-Zeile
oben. Bulk-Bar oben mit count, "⬇ ZIP" / "⬇ Download" (je nach Anzahl),
und "🗑 Löschen". ZIP-Response landet base64 → RNFS in Downloads-Folder
(aria-files-<timestamp>.zip), Toast mit Pfad.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1361,6 +1361,77 @@ const server = http.createServer((req, res) => {
|
||||
});
|
||||
fs.createReadStream(safe).pipe(res);
|
||||
return;
|
||||
} else if (req.url === "/api/files-download-zip" && req.method === "POST") {
|
||||
// Multi-Datei-Download als ZIP. Body: {paths: ["/shared/uploads/...", ...]}.
|
||||
// Streamt zip stdout direkt in die Response.
|
||||
let body = "";
|
||||
req.on("data", c => { body += c; if (body.length > 65536) req.destroy(); });
|
||||
req.on("end", () => {
|
||||
let paths = [];
|
||||
try { paths = (JSON.parse(body || "{}").paths || []); } catch { paths = []; }
|
||||
// Whitelist: nur /shared/uploads/, existieren muessen sie
|
||||
paths = paths
|
||||
.map(p => path.resolve(String(p)))
|
||||
.filter(p => p.startsWith("/shared/uploads/") && fs.existsSync(p));
|
||||
if (!paths.length) {
|
||||
res.writeHead(400, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: false, error: "Keine gueltigen Pfade" }));
|
||||
return;
|
||||
}
|
||||
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
||||
const fname = `aria-files-${ts}.zip`;
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "application/zip",
|
||||
"Content-Disposition": `attachment; filename="${fname}"`,
|
||||
});
|
||||
// zip -j: junk paths (Dateien ohne Verzeichnisstruktur ablegen)
|
||||
const { spawn } = require("child_process");
|
||||
const zip = spawn("zip", ["-j", "-q", "-", ...paths]);
|
||||
zip.stdout.pipe(res);
|
||||
let stderr = "";
|
||||
zip.stderr.on("data", d => stderr += d.toString());
|
||||
zip.on("close", code => {
|
||||
if (code !== 0 && code !== 12) {
|
||||
log("error", "server", `zip exit ${code}: ${stderr.slice(0, 200)}`);
|
||||
}
|
||||
});
|
||||
req.on("close", () => { if (!zip.killed) zip.kill("SIGTERM"); });
|
||||
});
|
||||
return;
|
||||
} else if (req.url === "/api/files-delete-batch" && req.method === "POST") {
|
||||
let body = "";
|
||||
req.on("data", c => { body += c; if (body.length > 65536) req.destroy(); });
|
||||
req.on("end", () => {
|
||||
try {
|
||||
let paths = (JSON.parse(body || "{}").paths || []);
|
||||
paths = paths
|
||||
.map(p => path.resolve(String(p)))
|
||||
.filter(p => p.startsWith("/shared/uploads/"));
|
||||
const deleted = [];
|
||||
const errors = [];
|
||||
for (const p of paths) {
|
||||
try {
|
||||
if (fs.existsSync(p)) fs.unlinkSync(p);
|
||||
deleted.push(p);
|
||||
broadcast({ type: "file_deleted", path: p });
|
||||
sendToRVS_raw({ type: "file_deleted", payload: { path: p }, timestamp: Date.now() });
|
||||
try {
|
||||
fs.appendFileSync("/shared/config/chat_backup.jsonl",
|
||||
JSON.stringify({ type: "file_deleted", path: p, ts: Date.now(), by: "user" }) + "\n");
|
||||
} catch {}
|
||||
} catch (e) {
|
||||
errors.push({ path: p, error: e.message });
|
||||
}
|
||||
}
|
||||
log("info", "server", `Bulk-Delete: ${deleted.length} OK, ${errors.length} Fehler`);
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: true, deleted, errors }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: false, error: err.message }));
|
||||
}
|
||||
});
|
||||
return;
|
||||
} else if (req.url === "/api/files-delete" && req.method === "POST") {
|
||||
let body = "";
|
||||
req.on("data", c => { body += c; if (body.length > 4096) req.destroy(); });
|
||||
|
||||
Reference in New Issue
Block a user