Administration backs the server up, downloads, uploads and restores it
A Backups section (for administrators, PrivaPub's /clientapi/admin/backups): the backups kept with their kind, size, media and the logins deleted since, what runs, the restore waiting and the last restore's report. Back up now; download for the password through a link good for ten minutes, which the browser saves to disk; upload a downloaded backup in pieces straight from the picked file (js/transfer.js PUTs file.slice at the offset PrivaPub has, never through .NET's memory, and carries on after a refused or lost piece); delete after asking; restore for the password and the host typed out, saying what it keeps and what it loses, then showing PrivaPub restarting until the ended session sends the page to sign in. e2e: a backup made, downloaded and uploaded again; and, only with --filter "Category=Restore", a real restore of the pasture, which signs the town in again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
70cb389537
commit
dc2190e271
11 files changed
+1058
-3
No files matched your search
@@ -0,0 +1,65 @@
|
||||
// Files sent to PrivaPub in pieces, read by the browser and never copied into .NET's memory: a backup can be gigabytes.
|
||||
// Each piece is a slice of the picked file PUT at the offset PrivaPub has received; a piece refused (409) or lost to the
|
||||
// network carries on from what PrivaPub says it has, so a broken upload resumes. Progress goes to listener.Progress.
|
||||
|
||||
const attempts = 6;
|
||||
|
||||
function pause(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function received(endpoint, token) {
|
||||
const response = await fetch(endpoint, { headers: { Authorization: `Bearer ${token}` } });
|
||||
if (!response.ok)
|
||||
throw new Error(`PrivaPub answered ${response.status}`);
|
||||
return (await response.json()).received;
|
||||
}
|
||||
|
||||
// the picked file's name and size, or null
|
||||
export function picked(selector) {
|
||||
const file = document.querySelector(selector)?.files?.[0];
|
||||
return file ? { name: file.name, size: file.size } : null;
|
||||
}
|
||||
|
||||
// sends the picked file: { ok, size } or { ok: false, status, error }
|
||||
export async function upload(selector, endpoint, token, chunkBytes, listener) {
|
||||
const file = document.querySelector(selector)?.files?.[0];
|
||||
if (!file)
|
||||
return { ok: false, status: 0, error: "no file" };
|
||||
let offset = 0;
|
||||
let failures = 0;
|
||||
while (offset < file.size) {
|
||||
const end = Math.min(offset + chunkBytes, file.size);
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(`${endpoint}?offset=${offset}`, {
|
||||
method: "PUT",
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/octet-stream" },
|
||||
body: file.slice(offset, end)
|
||||
});
|
||||
} catch (error) {
|
||||
if (++failures >= attempts)
|
||||
return { ok: false, status: 0, error: String(error) };
|
||||
await pause(1000 * 2 ** failures);
|
||||
try {
|
||||
offset = await received(endpoint, token);
|
||||
} catch {
|
||||
// still away: try the same piece again
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (response.ok || response.status === 409) {
|
||||
const now = (await response.json()).received;
|
||||
// refused where it already stood: the piece itself is refused, and sending it again changes nothing
|
||||
if (!response.ok && now === offset && ++failures >= attempts)
|
||||
return { ok: false, status: 409, error: "the piece was refused" };
|
||||
if (now !== offset)
|
||||
failures = 0;
|
||||
offset = now;
|
||||
await listener.invokeMethodAsync("Progress", offset, file.size);
|
||||
continue;
|
||||
}
|
||||
return { ok: false, status: response.status, error: await response.text() };
|
||||
}
|
||||
return { ok: true, size: file.size };
|
||||
}
|
||||
Reference in new issue
Block a user