philomena/assets/js/sw/bulk.ts

88 lines
2.5 KiB
TypeScript
Raw Normal View History

2021-10-28 05:36:13 +02:00
import { wait, json, u8Array } from 'utils/async';
2021-10-26 05:42:29 +02:00
import { evenlyDivide } from 'utils/array';
import { fetchBackoff } from 'utils/requests';
import { Zip } from 'utils/zip';
interface Image {
id: number;
name: string;
view_url: string; // eslint-disable-line camelcase
}
interface PageResult {
images: Image[];
total: number;
}
2021-10-28 05:36:13 +02:00
export function handleBulk(event: FetchEvent, url: URL): void {
const concurrency = parseInt(url.searchParams.get('concurrency') || '1', 10);
2021-10-26 05:42:29 +02:00
const queryString = url.searchParams.get('q');
const failures = [];
const zipper = new Zip();
if (!queryString) {
return event.respondWith(new Response('No query specified', { status: 400 }));
}
// Maximum ID to fetch -- start with largest possible ID
let maxId = (2 ** 31) - 1;
const stream = new ReadableStream({
pull(controller) {
// Path to fetch next
const nextQuery = encodeURIComponent(`(${queryString}),id.lte:${maxId}`);
2021-10-28 05:36:13 +02:00
const consumer = (buf: Uint8Array) => controller.enqueue(buf);
2021-10-26 05:42:29 +02:00
return fetchBackoff(`/search/download?q=${nextQuery}`)
.then(json)
.then(({ images, total }: PageResult): Promise<void> => {
if (total === 0) {
2021-10-28 05:36:13 +02:00
// Finalize zip
zipper.finalize(consumer);
// Close stream
controller.close();
// Done
return Promise.resolve();
2021-10-26 05:42:29 +02:00
}
// Decrease maximum ID for next round below current minimum
maxId = images[images.length - 1].id - 1;
// Set up concurrent fetches
const imageBins = evenlyDivide(images, concurrency);
const fetchers = imageBins.map(downloadIntoZip);
// Run all concurrent fetches
return Promise
.all(fetchers)
.then(() => wait(5000));
});
// Function to fetch each image and push it into the zip stream
function downloadIntoZip(images: Image[]): Promise<void> {
let promise = Promise.resolve();
// eslint-disable-next-line camelcase
for (const { name, view_url } of images) {
promise = promise
2021-10-28 05:36:13 +02:00
.then(() => fetchBackoff(view_url).then(u8Array))
.then(file => zipper.storeFile(name, file.buffer, consumer))
2021-10-26 05:42:29 +02:00
.catch(() => { failures.push(view_url); });
}
return promise;
}
}
});
event.respondWith(new Response(stream, {
headers: {
'content-type': 'application/x-zip',
'content-disposition': 'attachment; filename="image_export.zip"'
}
}));
}