serviceWorker.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import assets from '../common/assets';
  2. import { version } from '../package.json';
  3. import Keychain from './keychain';
  4. import { downloadStream } from './api';
  5. import { transformStream } from './streams';
  6. import Zip from './zip';
  7. import contentDisposition from 'content-disposition';
  8. let noSave = false;
  9. const map = new Map();
  10. const IMAGES = /.*\.(png|svg|jpg)$/;
  11. const VERSIONED_ASSET = /\.[A-Fa-f0-9]{8}\.(js|css|png|svg|jpg)(#\w+)?$/;
  12. const DOWNLOAD_URL = /\/api\/download\/([A-Fa-f0-9]{4,})/;
  13. const FONT = /\.woff2?$/;
  14. self.addEventListener('install', () => {
  15. self.skipWaiting();
  16. });
  17. self.addEventListener('activate', event => {
  18. event.waitUntil(self.clients.claim().then(precache));
  19. });
  20. async function decryptStream(id) {
  21. const file = map.get(id);
  22. if (!file) {
  23. return new Response(null, { status: 400 });
  24. }
  25. try {
  26. let size = file.size;
  27. let type = file.type;
  28. const keychain = new Keychain(file.key, file.nonce);
  29. if (file.requiresPassword) {
  30. keychain.setPassword(file.password, file.url);
  31. }
  32. file.download = downloadStream(id, keychain);
  33. const body = await file.download.result;
  34. const decrypted = keychain.decryptStream(body);
  35. let zipStream = null;
  36. if (file.type === 'send-archive') {
  37. const zip = new Zip(file.manifest, decrypted);
  38. zipStream = zip.stream;
  39. type = 'application/zip';
  40. size = zip.size;
  41. }
  42. const responseStream = transformStream(
  43. zipStream || decrypted,
  44. {
  45. transform(chunk, controller) {
  46. file.progress += chunk.length;
  47. controller.enqueue(chunk);
  48. }
  49. },
  50. function oncancel() {
  51. // NOTE: cancel doesn't currently fire on chrome
  52. // https://bugs.chromium.org/p/chromium/issues/detail?id=638494
  53. file.download.cancel();
  54. map.delete(id);
  55. }
  56. );
  57. const headers = {
  58. 'Content-Disposition': contentDisposition(file.filename),
  59. 'Content-Type': type,
  60. 'Content-Length': size
  61. };
  62. return new Response(responseStream, { headers });
  63. } catch (e) {
  64. if (noSave) {
  65. return new Response(null, { status: e.message });
  66. }
  67. return new Response(null, {
  68. status: 302,
  69. headers: {
  70. Location: `/download/${id}/#${file.key}`
  71. }
  72. });
  73. }
  74. }
  75. async function precache() {
  76. try {
  77. await cleanCache();
  78. const cache = await caches.open(version);
  79. const images = assets.match(IMAGES);
  80. await cache.addAll(images);
  81. } catch (e) {
  82. console.error(e);
  83. // cache will get populated on demand
  84. }
  85. }
  86. async function cleanCache() {
  87. const oldCaches = await caches.keys();
  88. for (const c of oldCaches) {
  89. if (c !== version) {
  90. await caches.delete(c);
  91. }
  92. }
  93. }
  94. function cacheable(url) {
  95. return VERSIONED_ASSET.test(url) || FONT.test(url);
  96. }
  97. async function cachedOrFetched(req) {
  98. const cache = await caches.open(version);
  99. const cached = await cache.match(req);
  100. if (cached) {
  101. return cached;
  102. }
  103. const fetched = await fetch(req);
  104. if (fetched.ok && cacheable(req.url)) {
  105. cache.put(req, fetched.clone());
  106. }
  107. return fetched;
  108. }
  109. self.onfetch = event => {
  110. const req = event.request;
  111. if (req.method !== 'GET') return;
  112. const url = new URL(req.url);
  113. const dlmatch = DOWNLOAD_URL.exec(url.pathname);
  114. if (dlmatch) {
  115. event.respondWith(decryptStream(dlmatch[1]));
  116. } else if (cacheable(url.pathname)) {
  117. event.respondWith(cachedOrFetched(req));
  118. }
  119. };
  120. self.onmessage = event => {
  121. if (event.data.request === 'init') {
  122. noSave = event.data.noSave;
  123. const info = {
  124. key: event.data.key,
  125. nonce: event.data.nonce,
  126. filename: event.data.filename,
  127. requiresPassword: event.data.requiresPassword,
  128. password: event.data.password,
  129. url: event.data.url,
  130. type: event.data.type,
  131. manifest: event.data.manifest,
  132. size: event.data.size,
  133. progress: 0
  134. };
  135. map.set(event.data.id, info);
  136. event.ports[0].postMessage('file info received');
  137. } else if (event.data.request === 'progress') {
  138. const file = map.get(event.data.id);
  139. if (!file) {
  140. event.ports[0].postMessage({ error: 'cancelled' });
  141. } else {
  142. if (file.progress === file.size) {
  143. map.delete(event.data.id);
  144. }
  145. event.ports[0].postMessage({ progress: file.progress });
  146. }
  147. } else if (event.data.request === 'cancel') {
  148. const file = map.get(event.data.id);
  149. if (file) {
  150. if (file.download) {
  151. file.download.cancel();
  152. }
  153. map.delete(event.data.id);
  154. }
  155. event.ports[0].postMessage('download cancelled');
  156. }
  157. };