1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
/**
* Service worker: the only way to stream a download to disk in Firefox.
*
* Chrome and Edge have the File System Access API — the page opens a file and
* writes to it. Firefox and Safari do not, and the alternative there was to
* collect the whole download in memory and hand the browser a blob, which is
* not an option for a file measured in gigabytes.
*
* So the page makes up a URL, tells this worker what stream answers it, and
* navigates a hidden iframe there. The worker replies with the stream and a
* Content-Disposition header, and the browser does what it does with any
* download: writes it to disk as it arrives, showing its own progress, with
* nothing buffered in the tab.
*
* It caches nothing and intercepts nothing else. Every request that is not one
* of these downloads falls through untouched.
*/
const PREFIX = '/_mbdl/';
const pending = new Map();
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim()));
self.addEventListener('message', (event) => {
const data = event.data || {};
if (data.type !== 'mbdl' || !data.id || !data.readable) return;
pending.set(data.id, {
readable: data.readable,
filename: data.filename || 'download',
size: Number(data.size) || 0,
});
// A tab that is closed before it navigates would leave a stream here for the
// life of the worker.
setTimeout(() => pending.delete(data.id), 60000);
});
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.origin !== self.location.origin || !url.pathname.startsWith(PREFIX)) {
return; // not ours — the network handles it
}
const entry = pending.get(url.pathname.slice(PREFIX.length));
if (!entry) return;
pending.delete(url.pathname.slice(PREFIX.length));
const headers = {
'Content-Type': 'application/octet-stream',
// filename* so a name with accents or spaces survives the trip.
'Content-Disposition':
`attachment; filename*=UTF-8''${encodeURIComponent(entry.filename)}`,
'Cache-Control': 'no-store',
};
// Only when it is known. A zip is assembled as it goes and announcing a
// length we then miss would truncate the file.
if (entry.size > 0) headers['Content-Length'] = String(entry.size);
event.respondWith(new Response(entry.readable, { headers }));
});
|