fileReceiver.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import Nanobus from 'nanobus';
  2. import Keychain from './keychain';
  3. import { delay, bytes, streamToArrayBuffer } from './utils';
  4. import { downloadFile, metadata, getApiUrl } from './api';
  5. import { blobStream } from './streams';
  6. import Zip from './zip';
  7. export default class FileReceiver extends Nanobus {
  8. constructor(fileInfo) {
  9. super('FileReceiver');
  10. this.keychain = new Keychain(fileInfo.secretKey, fileInfo.nonce);
  11. if (fileInfo.requiresPassword) {
  12. this.keychain.setPassword(fileInfo.password, fileInfo.url);
  13. }
  14. this.fileInfo = fileInfo;
  15. this.reset();
  16. }
  17. get progressRatio() {
  18. return this.progress[0] / this.progress[1];
  19. }
  20. get progressIndefinite() {
  21. return this.state !== 'downloading';
  22. }
  23. get sizes() {
  24. return {
  25. partialSize: bytes(this.progress[0]),
  26. totalSize: bytes(this.progress[1])
  27. };
  28. }
  29. cancel() {
  30. if (this.downloadRequest) {
  31. this.downloadRequest.cancel();
  32. }
  33. }
  34. reset() {
  35. this.msg = 'fileSizeProgress';
  36. this.state = 'initialized';
  37. this.progress = [0, 1];
  38. }
  39. async getMetadata() {
  40. const meta = await metadata(this.fileInfo.id, this.keychain);
  41. this.fileInfo.name = meta.name;
  42. this.fileInfo.type = meta.type;
  43. this.fileInfo.iv = meta.iv;
  44. this.fileInfo.size = +meta.size;
  45. this.fileInfo.manifest = meta.manifest;
  46. this.state = 'ready';
  47. }
  48. sendMessageToSw(msg) {
  49. return new Promise((resolve, reject) => {
  50. const channel = new MessageChannel();
  51. channel.port1.onmessage = function(event) {
  52. if (event.data === undefined) {
  53. reject('bad response from serviceWorker');
  54. } else if (event.data.error !== undefined) {
  55. reject(event.data.error);
  56. } else {
  57. resolve(event.data);
  58. }
  59. };
  60. navigator.serviceWorker.controller.postMessage(msg, [channel.port2]);
  61. });
  62. }
  63. async downloadBlob(noSave = false) {
  64. this.state = 'downloading';
  65. this.downloadRequest = await downloadFile(
  66. this.fileInfo.id,
  67. this.keychain,
  68. p => {
  69. this.progress = [p, this.fileInfo.size];
  70. this.emit('progress');
  71. }
  72. );
  73. try {
  74. const ciphertext = await this.downloadRequest.result;
  75. this.downloadRequest = null;
  76. this.msg = 'decryptingFile';
  77. this.state = 'decrypting';
  78. this.emit('decrypting');
  79. let size = this.fileInfo.size;
  80. let plainStream = this.keychain.decryptStream(blobStream(ciphertext));
  81. if (this.fileInfo.type === 'send-archive') {
  82. const zip = new Zip(this.fileInfo.manifest, plainStream);
  83. plainStream = zip.stream;
  84. size = zip.size;
  85. }
  86. const plaintext = await streamToArrayBuffer(plainStream, size);
  87. if (!noSave) {
  88. await saveFile({
  89. plaintext,
  90. name: decodeURIComponent(this.fileInfo.name),
  91. type: this.fileInfo.type
  92. });
  93. }
  94. this.msg = 'downloadFinish';
  95. this.emit('complete');
  96. this.state = 'complete';
  97. } catch (e) {
  98. this.downloadRequest = null;
  99. throw e;
  100. }
  101. }
  102. async downloadStream(noSave = false) {
  103. const start = Date.now();
  104. const onprogress = p => {
  105. this.progress = [p, this.fileInfo.size];
  106. this.emit('progress');
  107. };
  108. this.downloadRequest = {
  109. cancel: () => {
  110. this.sendMessageToSw({ request: 'cancel', id: this.fileInfo.id });
  111. }
  112. };
  113. try {
  114. this.state = 'downloading';
  115. const info = {
  116. request: 'init',
  117. id: this.fileInfo.id,
  118. filename: this.fileInfo.name,
  119. type: this.fileInfo.type,
  120. manifest: this.fileInfo.manifest,
  121. key: this.fileInfo.secretKey,
  122. requiresPassword: this.fileInfo.requiresPassword,
  123. password: this.fileInfo.password,
  124. url: this.fileInfo.url,
  125. size: this.fileInfo.size,
  126. nonce: this.keychain.nonce,
  127. noSave
  128. };
  129. await this.sendMessageToSw(info);
  130. onprogress(0);
  131. if (noSave) {
  132. const res = await fetch(getApiUrl(`/api/download/${this.fileInfo.id}`));
  133. if (res.status !== 200) {
  134. throw new Error(res.status);
  135. }
  136. } else {
  137. const downloadPath = `/api/download/${this.fileInfo.id}`;
  138. let downloadUrl = getApiUrl(downloadPath);
  139. if (downloadUrl === downloadPath) {
  140. downloadUrl = `${location.protocol}//${location.host}${downloadPath}`;
  141. }
  142. const a = document.createElement('a');
  143. a.href = downloadUrl;
  144. document.body.appendChild(a);
  145. a.click();
  146. }
  147. let prog = 0;
  148. let hangs = 0;
  149. while (prog < this.fileInfo.size) {
  150. const msg = await this.sendMessageToSw({
  151. request: 'progress',
  152. id: this.fileInfo.id
  153. });
  154. if (msg.progress === prog) {
  155. hangs++;
  156. } else {
  157. hangs = 0;
  158. }
  159. if (hangs > 30) {
  160. // TODO: On Chrome we don't get a cancel
  161. // signal so one is indistinguishable from
  162. // a hang. We may be able to detect
  163. // which end is hung in the service worker
  164. // to improve on this.
  165. const e = new Error('hung download');
  166. e.duration = Date.now() - start;
  167. e.size = this.fileInfo.size;
  168. e.progress = prog;
  169. throw e;
  170. }
  171. prog = msg.progress;
  172. onprogress(prog);
  173. await delay(1000);
  174. }
  175. this.downloadRequest = null;
  176. this.msg = 'downloadFinish';
  177. this.emit('complete');
  178. this.state = 'complete';
  179. } catch (e) {
  180. this.downloadRequest = null;
  181. if (e === 'cancelled' || e.message === '400') {
  182. throw new Error(0);
  183. }
  184. throw e;
  185. }
  186. }
  187. download(options) {
  188. if (options.stream) {
  189. return this.downloadStream(options.noSave);
  190. }
  191. return this.downloadBlob(options.noSave);
  192. }
  193. }
  194. async function saveFile(file) {
  195. return new Promise(function(resolve, reject) {
  196. const dataView = new DataView(file.plaintext);
  197. const blob = new Blob([dataView], { type: file.type });
  198. if (navigator.msSaveBlob) {
  199. navigator.msSaveBlob(blob, file.name);
  200. return resolve();
  201. } else if (/iPhone|fxios/i.test(navigator.userAgent)) {
  202. // This method is much slower but createObjectURL
  203. // is buggy on iOS
  204. const reader = new FileReader();
  205. reader.addEventListener('loadend', function() {
  206. if (reader.error) {
  207. return reject(reader.error);
  208. }
  209. if (reader.result) {
  210. const a = document.createElement('a');
  211. a.href = reader.result;
  212. a.download = file.name;
  213. document.body.appendChild(a);
  214. a.click();
  215. }
  216. resolve();
  217. });
  218. reader.readAsDataURL(blob);
  219. } else {
  220. const downloadUrl = URL.createObjectURL(blob);
  221. const a = document.createElement('a');
  222. a.href = downloadUrl;
  223. a.download = file.name;
  224. document.body.appendChild(a);
  225. a.click();
  226. URL.revokeObjectURL(downloadUrl);
  227. setTimeout(resolve, 100);
  228. }
  229. });
  230. }