fileReceiver.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import Nanobus from 'nanobus';
  2. import Keychain from './keychain';
  3. import { delay, bytes, streamToArrayBuffer } from './utils';
  4. import { downloadFile, metadata, getApiUrl, reportLink } 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. async reportLink(reason) {
  49. await reportLink(this.fileInfo.id, this.keychain, reason);
  50. }
  51. sendMessageToSw(msg) {
  52. return new Promise((resolve, reject) => {
  53. const channel = new MessageChannel();
  54. channel.port1.onmessage = function(event) {
  55. if (event.data === undefined) {
  56. reject('bad response from serviceWorker');
  57. } else if (event.data.error !== undefined) {
  58. reject(event.data.error);
  59. } else {
  60. resolve(event.data);
  61. }
  62. };
  63. navigator.serviceWorker.controller.postMessage(msg, [channel.port2]);
  64. });
  65. }
  66. async downloadBlob(noSave = false) {
  67. this.state = 'downloading';
  68. this.downloadRequest = await downloadFile(
  69. this.fileInfo.id,
  70. this.keychain,
  71. p => {
  72. this.progress = [p, this.fileInfo.size];
  73. this.emit('progress');
  74. }
  75. );
  76. try {
  77. const ciphertext = await this.downloadRequest.result;
  78. this.downloadRequest = null;
  79. this.msg = 'decryptingFile';
  80. this.state = 'decrypting';
  81. this.emit('decrypting');
  82. let size = this.fileInfo.size;
  83. let plainStream = this.keychain.decryptStream(blobStream(ciphertext));
  84. if (this.fileInfo.type === 'send-archive') {
  85. const zip = new Zip(this.fileInfo.manifest, plainStream);
  86. plainStream = zip.stream;
  87. size = zip.size;
  88. }
  89. const plaintext = await streamToArrayBuffer(plainStream, size);
  90. if (!noSave) {
  91. await saveFile({
  92. plaintext,
  93. name: decodeURIComponent(this.fileInfo.name),
  94. type: this.fileInfo.type
  95. });
  96. }
  97. this.msg = 'downloadFinish';
  98. this.emit('complete');
  99. this.state = 'complete';
  100. } catch (e) {
  101. this.downloadRequest = null;
  102. throw e;
  103. }
  104. }
  105. async downloadStream(noSave = false) {
  106. const start = Date.now();
  107. const onprogress = p => {
  108. this.progress = [p, this.fileInfo.size];
  109. this.emit('progress');
  110. };
  111. this.downloadRequest = {
  112. cancel: () => {
  113. this.sendMessageToSw({ request: 'cancel', id: this.fileInfo.id });
  114. }
  115. };
  116. try {
  117. this.state = 'downloading';
  118. const info = {
  119. request: 'init',
  120. id: this.fileInfo.id,
  121. filename: this.fileInfo.name,
  122. type: this.fileInfo.type,
  123. manifest: this.fileInfo.manifest,
  124. key: this.fileInfo.secretKey,
  125. requiresPassword: this.fileInfo.requiresPassword,
  126. password: this.fileInfo.password,
  127. url: this.fileInfo.url,
  128. size: this.fileInfo.size,
  129. nonce: this.keychain.nonce,
  130. noSave
  131. };
  132. await this.sendMessageToSw(info);
  133. onprogress(0);
  134. if (noSave) {
  135. const res = await fetch(getApiUrl(`/api/download/${this.fileInfo.id}`));
  136. if (res.status !== 200) {
  137. throw new Error(res.status);
  138. }
  139. } else {
  140. const downloadPath = `/api/download/${this.fileInfo.id}`;
  141. let downloadUrl = getApiUrl(downloadPath);
  142. if (downloadUrl === downloadPath) {
  143. downloadUrl = `${location.protocol}//${location.host}${downloadPath}`;
  144. }
  145. const a = document.createElement('a');
  146. a.href = downloadUrl;
  147. document.body.appendChild(a);
  148. a.click();
  149. }
  150. let prog = 0;
  151. let hangs = 0;
  152. while (prog < this.fileInfo.size) {
  153. const msg = await this.sendMessageToSw({
  154. request: 'progress',
  155. id: this.fileInfo.id
  156. });
  157. if (msg.progress === prog) {
  158. hangs++;
  159. } else {
  160. hangs = 0;
  161. }
  162. if (hangs > 30) {
  163. // TODO: On Chrome we don't get a cancel
  164. // signal so one is indistinguishable from
  165. // a hang. We may be able to detect
  166. // which end is hung in the service worker
  167. // to improve on this.
  168. const e = new Error('hung download');
  169. e.duration = Date.now() - start;
  170. e.size = this.fileInfo.size;
  171. e.progress = prog;
  172. throw e;
  173. }
  174. prog = msg.progress;
  175. onprogress(prog);
  176. await delay(1000);
  177. }
  178. this.downloadRequest = null;
  179. this.msg = 'downloadFinish';
  180. this.emit('complete');
  181. this.state = 'complete';
  182. } catch (e) {
  183. this.downloadRequest = null;
  184. if (e === 'cancelled' || e.message === '400') {
  185. throw new Error(0);
  186. }
  187. throw e;
  188. }
  189. }
  190. download(options) {
  191. if (options.stream) {
  192. return this.downloadStream(options.noSave);
  193. }
  194. return this.downloadBlob(options.noSave);
  195. }
  196. }
  197. async function saveFile(file) {
  198. return new Promise(function(resolve, reject) {
  199. const dataView = new DataView(file.plaintext);
  200. const blob = new Blob([dataView], { type: file.type });
  201. if (navigator.msSaveBlob) {
  202. navigator.msSaveBlob(blob, file.name);
  203. return resolve();
  204. } else {
  205. const downloadUrl = URL.createObjectURL(blob);
  206. const a = document.createElement('a');
  207. a.href = downloadUrl;
  208. a.download = file.name;
  209. document.body.appendChild(a);
  210. a.click();
  211. URL.revokeObjectURL(downloadUrl);
  212. setTimeout(resolve, 100);
  213. }
  214. });
  215. }