|
|
|
@@ -889,7 +889,7 @@ export async function releaseResolvedParks(state: {
|
|
|
|
|
|
|
|
|
|
|
|
/** Outcome of an `addToQueue` call, so the caller can tell the user when a file was NOT
|
|
|
|
/** Outcome of an `addToQueue` call, so the caller can tell the user when a file was NOT
|
|
|
|
* actually queued (deduped, or the queue is full of un-evictable in-flight items). */
|
|
|
|
* actually queued (deduped, or the queue is full of un-evictable in-flight items). */
|
|
|
|
export type EnqueueResult = 'queued' | 'duplicate' | 'full';
|
|
|
|
export type EnqueueResult = 'queued' | 'duplicate' | 'full' | 'unreadable';
|
|
|
|
|
|
|
|
|
|
|
|
/** Chunk size for `materialise`. Bounds peak JS heap, not total copy size. */
|
|
|
|
/** Chunk size for `materialise`. Bounds peak JS heap, not total copy size. */
|
|
|
|
const MATERIALISE_CHUNK_BYTES = 4 * 1024 * 1024;
|
|
|
|
const MATERIALISE_CHUNK_BYTES = 4 * 1024 * 1024;
|
|
|
|
@@ -907,15 +907,30 @@ const MATERIALISE_CHUNK_BYTES = 4 * 1024 * 1024;
|
|
|
|
* can spill them to disk, which is exactly where a half-gigabyte video should live.
|
|
|
|
* can spill them to disk, which is exactly where a half-gigabyte video should live.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
async function materialise(file: File): Promise<Blob> {
|
|
|
|
async function materialise(file: File): Promise<Blob> {
|
|
|
|
|
|
|
|
let out: Blob;
|
|
|
|
if (file.size <= MATERIALISE_CHUNK_BYTES) {
|
|
|
|
if (file.size <= MATERIALISE_CHUNK_BYTES) {
|
|
|
|
return new Blob([await file.arrayBuffer()], { type: file.type });
|
|
|
|
out = new Blob([await file.arrayBuffer()], { type: file.type });
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const parts: Blob[] = [];
|
|
|
|
const parts: Blob[] = [];
|
|
|
|
for (let offset = 0; offset < file.size; offset += MATERIALISE_CHUNK_BYTES) {
|
|
|
|
for (let offset = 0; offset < file.size; offset += MATERIALISE_CHUNK_BYTES) {
|
|
|
|
const slice = file.slice(offset, offset + MATERIALISE_CHUNK_BYTES);
|
|
|
|
const slice = file.slice(offset, offset + MATERIALISE_CHUNK_BYTES);
|
|
|
|
parts.push(new Blob([await slice.arrayBuffer()]));
|
|
|
|
parts.push(new Blob([await slice.arrayBuffer()]));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return new Blob(parts, { type: file.type });
|
|
|
|
out = new Blob(parts, { type: file.type });
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Verify the copy. The purge this whole function exists to defeat can also land PARTWAY
|
|
|
|
|
|
|
|
// THROUGH the loop above: a 500 MB video is many seconds of reading, and once the OS file
|
|
|
|
|
|
|
|
// is gone the remaining slices read as nothing. `new Blob` is happy to build a short blob
|
|
|
|
|
|
|
|
// out of them, and short is far worse than absent — it uploads, the server stores it, and
|
|
|
|
|
|
|
|
// the guest gets a truncated video that looks like it worked. A read that returns fewer
|
|
|
|
|
|
|
|
// bytes than the file claims is never legitimate, so refuse it here, while the guest is
|
|
|
|
|
|
|
|
// still holding the phone and can pick the file again.
|
|
|
|
|
|
|
|
if (out.size !== file.size) {
|
|
|
|
|
|
|
|
throw new UnreadableBlobError(
|
|
|
|
|
|
|
|
'Diese Datei konnte nicht vollständig gelesen werden — bitte wähle sie noch einmal aus.'
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export async function addToQueue(
|
|
|
|
export async function addToQueue(
|
|
|
|
@@ -980,7 +995,15 @@ export async function addToQueue(
|
|
|
|
// Reading the file here makes IndexedDB own real bytes that no OS purge can reach. It
|
|
|
|
// Reading the file here makes IndexedDB own real bytes that no OS purge can reach. It
|
|
|
|
// costs one full read at pick time, which is also the moment the file is guaranteed still
|
|
|
|
// costs one full read at pick time, which is also the moment the file is guaranteed still
|
|
|
|
// readable — the picker has only just handed it over.
|
|
|
|
// readable — the picker has only just handed it over.
|
|
|
|
const blob = await materialise(file);
|
|
|
|
// A file the browser cannot fully read is not a queueable item. Returning a result rather
|
|
|
|
|
|
|
|
// than throwing keeps the composer's per-file loop intact: one bad photo out of five must
|
|
|
|
|
|
|
|
// not abandon the other four, which is what an exception here would do.
|
|
|
|
|
|
|
|
let blob: Blob;
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
blob = await materialise(file);
|
|
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
|
|
return 'unreadable';
|
|
|
|
|
|
|
|
}
|
|
|
|
const entry: QueueEntry = {
|
|
|
|
const entry: QueueEntry = {
|
|
|
|
id,
|
|
|
|
id,
|
|
|
|
userId,
|
|
|
|
userId,
|
|
|
|
|