fileReceiver.js 6.5 KB

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