65 lines
2.4 KiB
JavaScript
65 lines
2.4 KiB
JavaScript
// 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 };
|
||
|
|
}
|