tessa 2.0 → 6.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -18
  3. data/app/assets/javascripts/tessa.esm.js +212 -173
  4. data/app/assets/javascripts/tessa.js +206 -167
  5. data/app/javascript/activestorage/file_checksum.ts +76 -0
  6. data/app/javascript/tessa/index.ts +264 -0
  7. data/app/javascript/tessa/types.ts +34 -0
  8. data/config/routes.rb +0 -1
  9. data/lib/tessa/simple_form/asset_input.rb +24 -25
  10. data/lib/tessa/version.rb +1 -1
  11. data/lib/tessa.rb +0 -80
  12. data/package.json +7 -2
  13. data/rollup.config.js +5 -5
  14. data/spec/dummy/app/models/single_asset_model.rb +1 -1
  15. data/spec/dummy/app/models/single_asset_model_form.rb +3 -5
  16. data/spec/dummy/bin/rails +2 -2
  17. data/spec/dummy/bin/rake +2 -2
  18. data/spec/dummy/bin/setup +14 -6
  19. data/spec/dummy/bin/yarn +9 -3
  20. data/spec/dummy/config/application.rb +11 -9
  21. data/spec/dummy/config/boot.rb +1 -1
  22. data/spec/dummy/config/environment.rb +1 -1
  23. data/spec/dummy/config/environments/development.rb +34 -5
  24. data/spec/dummy/config/environments/production.rb +49 -10
  25. data/spec/dummy/config/environments/test.rb +28 -12
  26. data/spec/dummy/config/initializers/backtrace_silencers.rb +4 -3
  27. data/spec/dummy/config/initializers/content_security_policy.rb +5 -0
  28. data/spec/dummy/config/initializers/filter_parameter_logging.rb +3 -1
  29. data/spec/dummy/config/initializers/new_framework_defaults_6_1.rb +67 -0
  30. data/spec/dummy/config/initializers/permissions_policy.rb +11 -0
  31. data/spec/dummy/config/initializers/wrap_parameters.rb +5 -0
  32. data/spec/dummy/config/locales/en.yml +1 -1
  33. data/spec/dummy/config/routes.rb +1 -1
  34. data/spec/dummy/config/storage.yml +31 -0
  35. data/spec/dummy/config.ru +2 -1
  36. data/spec/dummy/db/migrate/20230406194400_add_service_name_to_active_storage_blobs.active_storage.rb +22 -0
  37. data/spec/dummy/db/migrate/20230406194401_create_active_storage_variant_records.active_storage.rb +27 -0
  38. data/spec/dummy/db/schema.rb +15 -7
  39. data/tessa.gemspec +4 -5
  40. data/tsconfig.json +10 -0
  41. data/yarn.lock +74 -7
  42. metadata +36 -74
  43. data/app/javascript/activestorage/file_checksum.js +0 -53
  44. data/app/javascript/tessa/index.js.coffee +0 -183
  45. data/lib/tasks/tessa.rake +0 -18
  46. data/lib/tessa/active_storage/asset_wrapper.rb +0 -32
  47. data/lib/tessa/asset/failure.rb +0 -37
  48. data/lib/tessa/asset.rb +0 -47
  49. data/lib/tessa/asset_change.rb +0 -49
  50. data/lib/tessa/asset_change_set.rb +0 -49
  51. data/lib/tessa/config.rb +0 -16
  52. data/lib/tessa/controller_helpers.rb +0 -16
  53. data/lib/tessa/fake_connection.rb +0 -29
  54. data/lib/tessa/jobs/migrate_assets_job.rb +0 -222
  55. data/lib/tessa/model/dynamic_extensions.rb +0 -145
  56. data/lib/tessa/model/field.rb +0 -77
  57. data/lib/tessa/model.rb +0 -94
  58. data/lib/tessa/rack_upload_proxy.rb +0 -41
  59. data/lib/tessa/response_factory.rb +0 -15
  60. data/lib/tessa/upload/uploads_file.rb +0 -18
  61. data/lib/tessa/upload.rb +0 -31
  62. data/lib/tessa/view_helpers.rb +0 -23
  63. data/spec/dummy/app/models/multiple_asset_model.rb +0 -8
  64. data/spec/support/remote_call_macro.rb +0 -40
  65. data/spec/tessa/asset/failure_spec.rb +0 -48
  66. data/spec/tessa/asset_change_set_spec.rb +0 -198
  67. data/spec/tessa/asset_change_spec.rb +0 -86
  68. data/spec/tessa/asset_spec.rb +0 -196
  69. data/spec/tessa/config_spec.rb +0 -70
  70. data/spec/tessa/controller_helpers_spec.rb +0 -55
  71. data/spec/tessa/jobs/migrate_assets_job_spec.rb +0 -247
  72. data/spec/tessa/model_field_spec.rb +0 -72
  73. data/spec/tessa/model_spec.rb +0 -325
  74. data/spec/tessa/rack_upload_proxy_spec.rb +0 -83
  75. data/spec/tessa/upload/uploads_file_spec.rb +0 -72
  76. data/spec/tessa/upload_spec.rb +0 -125
  77. data/spec/tessa_spec.rb +0 -23
@@ -2,6 +2,34 @@
2
2
  typeof define === "function" && define.amd ? define(factory) : factory();
3
3
  })((function() {
4
4
  "use strict";
5
+ var extendStatics = function(d, b) {
6
+ extendStatics = Object.setPrototypeOf || {
7
+ __proto__: []
8
+ } instanceof Array && function(d, b) {
9
+ d.__proto__ = b;
10
+ } || function(d, b) {
11
+ for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p];
12
+ };
13
+ return extendStatics(d, b);
14
+ };
15
+ function __extends(d, b) {
16
+ if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
17
+ extendStatics(d, b);
18
+ function __() {
19
+ this.constructor = d;
20
+ }
21
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __);
22
+ }
23
+ var __assign = function() {
24
+ __assign = Object.assign || function __assign(t) {
25
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
26
+ s = arguments[i];
27
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
28
+ }
29
+ return t;
30
+ };
31
+ return __assign.apply(this, arguments);
32
+ };
5
33
  var sparkMd5 = {
6
34
  exports: {}
7
35
  };
@@ -412,249 +440,260 @@
412
440
  return SparkMD5;
413
441
  }));
414
442
  })(sparkMd5);
415
- var SparkMD5 = sparkMd5.exports;
416
- const fileSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
417
- class FileChecksum {
418
- static create(file, callback) {
419
- const instance = new FileChecksum(file);
420
- instance.create(callback);
421
- }
422
- constructor(file) {
443
+ var fileSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
444
+ var FileChecksum = function() {
445
+ function FileChecksum(file) {
423
446
  this.file = file;
424
447
  this.chunkSize = 2097152;
425
448
  this.chunkCount = Math.ceil(this.file.size / this.chunkSize);
426
449
  this.chunkIndex = 0;
427
450
  }
428
- create(callback) {
451
+ FileChecksum.create = function(file, callback) {
452
+ var instance = new FileChecksum(file);
453
+ instance.create(callback);
454
+ };
455
+ FileChecksum.prototype.create = function(callback) {
456
+ var _this = this;
429
457
  this.callback = callback;
430
- this.md5Buffer = new SparkMD5.ArrayBuffer;
458
+ this.md5Buffer = new sparkMd5.exports.ArrayBuffer;
431
459
  this.fileReader = new FileReader;
432
- this.fileReader.addEventListener("load", (event => this.fileReaderDidLoad(event)));
433
- this.fileReader.addEventListener("error", (event => this.fileReaderDidError(event)));
460
+ this.fileReader.addEventListener("load", (function(event) {
461
+ return _this.fileReaderDidLoad(event);
462
+ }));
463
+ this.fileReader.addEventListener("error", (function(event) {
464
+ return _this.fileReaderDidError(event);
465
+ }));
434
466
  this.readNextChunk();
435
- }
436
- fileReaderDidLoad(event) {
467
+ };
468
+ FileChecksum.prototype.fileReaderDidLoad = function(event) {
469
+ var _a;
470
+ if (!this.md5Buffer || !this.fileReader) {
471
+ throw new Error("FileChecksum: fileReaderDidLoad called before create");
472
+ }
473
+ if (!((_a = event.target) === null || _a === void 0 ? void 0 : _a.result)) {
474
+ return;
475
+ }
437
476
  this.md5Buffer.append(event.target.result);
438
477
  if (!this.readNextChunk()) {
439
- const binaryDigest = this.md5Buffer.end(true);
440
- const base64digest = btoa(binaryDigest);
441
- this.callback(null, base64digest);
478
+ var binaryDigest = this.md5Buffer.end(true);
479
+ var base64digest = btoa(binaryDigest);
480
+ if (this.callback) {
481
+ this.callback(null, base64digest);
482
+ }
483
+ }
484
+ };
485
+ FileChecksum.prototype.fileReaderDidError = function(event) {
486
+ if (this.callback) {
487
+ this.callback("Error reading ".concat(this.file.name));
488
+ }
489
+ };
490
+ FileChecksum.prototype.readNextChunk = function() {
491
+ if (!this.fileReader) {
492
+ throw new Error("FileChecksum: readNextChunk called before create");
442
493
  }
443
- }
444
- fileReaderDidError(event) {
445
- this.callback(`Error reading ${this.file.name}`);
446
- }
447
- readNextChunk() {
448
494
  if (this.chunkIndex < this.chunkCount || this.chunkIndex == 0 && this.chunkCount == 0) {
449
- const start = this.chunkIndex * this.chunkSize;
450
- const end = Math.min(start + this.chunkSize, this.file.size);
451
- const bytes = fileSlice.call(this.file, start, end);
495
+ var start = this.chunkIndex * this.chunkSize;
496
+ var end = Math.min(start + this.chunkSize, this.file.size);
497
+ var bytes = fileSlice.call(this.file, start, end);
452
498
  this.fileReader.readAsArrayBuffer(bytes);
453
499
  this.chunkIndex++;
454
500
  return true;
455
501
  } else {
456
502
  return false;
457
503
  }
458
- }
459
- }
460
- var $;
504
+ };
505
+ return FileChecksum;
506
+ }();
461
507
  window.WCC || (window.WCC = {});
462
- $ = window.jQuery;
463
- Dropzone.autoDiscover = false;
464
- window.WCC.Dropzone = class Dropzone extends window.Dropzone {
465
- uploadFile(file) {
466
- var handleError, headerName, headerValue, headers, progressObj, ref, response, updateProgress, xhr;
467
- xhr = new XMLHttpRequest;
508
+ var $ = window.jQuery;
509
+ window.Dropzone.autoDiscover = false;
510
+ var BaseDropzone = window.Dropzone;
511
+ var WCCDropzone = function(_super) {
512
+ __extends(WCCDropzone, _super);
513
+ function WCCDropzone() {
514
+ return _super !== null && _super.apply(this, arguments) || this;
515
+ }
516
+ WCCDropzone.prototype.uploadFile = function(file) {
517
+ var _this = this;
518
+ var _a;
519
+ var xhr = new XMLHttpRequest;
468
520
  file.xhr = xhr;
469
521
  xhr.open(file.uploadMethod, file.uploadURL, true);
470
- response = null;
471
- handleError = () => this._errorProcessing([ file ], response || this.options.dictResponseError.replace("{{statusCode}}", xhr.status), xhr);
472
- updateProgress = e => {
473
- var allFilesFinished, progress;
474
- if (e != null) {
522
+ var response = null;
523
+ var handleError = function() {
524
+ var _a;
525
+ _this._errorProcessing([ file ], response || ((_a = _this.options.dictResponseError) === null || _a === void 0 ? void 0 : _a.replace("{{statusCode}}", xhr.status.toString())), xhr);
526
+ };
527
+ var updateProgress = function(e) {
528
+ var _a;
529
+ var progress;
530
+ if (e) {
475
531
  progress = 100 * e.loaded / e.total;
476
- file.upload = {
532
+ file.upload = __assign(__assign({}, file.upload), {
477
533
  progress: progress,
478
534
  total: e.total,
479
535
  bytesSent: e.loaded
480
- };
536
+ });
481
537
  } else {
482
- allFilesFinished = true;
483
538
  progress = 100;
484
- if (!(file.upload.progress === 100 && file.upload.bytesSent === file.upload.total)) {
485
- allFilesFinished = false;
539
+ var allFilesFinished = false;
540
+ if (file.upload.progress == 100 && file.upload.bytesSent == file.upload.total) {
541
+ allFilesFinished = true;
486
542
  }
487
543
  file.upload.progress = progress;
488
- file.upload.bytesSent = file.upload.total;
544
+ file.upload.bytesSent = (_a = file.upload) === null || _a === void 0 ? void 0 : _a.total;
489
545
  if (allFilesFinished) {
490
546
  return;
491
547
  }
492
548
  }
493
- return this.emit("uploadprogress", file, progress, file.upload.bytesSent);
549
+ _this.emit("uploadprogress", file, progress, file.upload.bytesSent);
494
550
  };
495
- xhr.onload = e => {
496
- var ref;
497
- if (file.status === WCC.Dropzone.CANCELED) {
551
+ xhr.onload = function(e) {
552
+ if (file.status == WCCDropzone.CANCELED) {
498
553
  return;
499
554
  }
500
- if (xhr.readyState !== 4) {
555
+ if (xhr.readyState != 4) {
501
556
  return;
502
557
  }
503
558
  response = xhr.responseText;
504
- if (xhr.getResponseHeader("content-type") && ~xhr.getResponseHeader("content-type").indexOf("application/json")) {
559
+ if (xhr.getResponseHeader("content-type") && xhr.getResponseHeader("content-type").indexOf("application/json") >= 0) {
505
560
  try {
506
561
  response = JSON.parse(response);
507
- } catch (error1) {
508
- e = error1;
562
+ } catch (e) {
509
563
  response = "Invalid JSON response from server.";
510
564
  }
511
565
  }
512
566
  updateProgress();
513
- if (!(200 <= (ref = xhr.status) && ref < 300)) {
514
- return handleError();
515
- } else {
516
- return this._finished([ file ], response, e);
567
+ if (xhr.status < 200 || xhr.status >= 300) handleError(); else {
568
+ _this._finished([ file ], response, e);
517
569
  }
518
570
  };
519
- xhr.onerror = () => {
520
- if (file.status === WCC.Dropzone.CANCELED) {
571
+ xhr.onerror = function() {
572
+ if (file.status == WCCDropzone.CANCELED) {
521
573
  return;
522
574
  }
523
- return handleError();
575
+ handleError();
524
576
  };
525
- progressObj = (ref = xhr.upload) != null ? ref : xhr;
577
+ var progressObj = (_a = xhr.upload) !== null && _a !== void 0 ? _a : xhr;
526
578
  progressObj.onprogress = updateProgress;
527
- headers = {
579
+ var headers = {
528
580
  Accept: "application/json",
529
581
  "Cache-Control": "no-cache",
530
582
  "X-Requested-With": "XMLHttpRequest"
531
583
  };
532
584
  if (this.options.headers) {
533
- $.extend(headers, this.options.headers);
585
+ Object.assign(headers, this.options.headers);
534
586
  }
535
587
  if (file.uploadHeaders) {
536
- $.extend(headers, file.uploadHeaders);
588
+ Object.assign(headers, file.uploadHeaders);
537
589
  }
538
- for (headerName in headers) {
539
- headerValue = headers[headerName];
590
+ for (var _i = 0, _b = Object.entries(headers); _i < _b.length; _i++) {
591
+ var _c = _b[_i], headerName = _c[0], headerValue = _c[1];
540
592
  xhr.setRequestHeader(headerName, headerValue);
541
593
  }
542
594
  this.emit("sending", file, xhr);
543
- return xhr.send(file);
544
- }
545
- uploadFiles(files) {
546
- var file, l, len, results;
547
- results = [];
548
- for (l = 0, len = files.length; l < len; l++) {
549
- file = files[l];
550
- results.push(this.uploadFile(file));
595
+ xhr.send(file);
596
+ };
597
+ WCCDropzone.prototype.uploadFiles = function(files) {
598
+ for (var _i = 0, files_1 = files; _i < files_1.length; _i++) {
599
+ var file = files_1[_i];
600
+ this.uploadFile(file);
551
601
  }
552
- return results;
553
- }
554
- };
555
- WCC.Dropzone.uploadPendingWarning = "File uploads have not yet completed. If you submit the form now they will not be saved. Are you sure you want to continue?";
556
- WCC.Dropzone.prototype.defaultOptions.url = "UNUSED";
557
- WCC.Dropzone.prototype.defaultOptions.dictDefaultMessage = "Drop files or click to upload.";
558
- WCC.Dropzone.prototype.defaultOptions.accept = function(file, done) {
559
- var dz, postData, tessaParams;
560
- dz = $(file._removeLink).closest(".tessa-upload").first();
561
- tessaParams = dz.data("tessa-params") || {};
562
- postData = {
563
- name: file.name,
564
- size: file.size,
565
- mime_type: file.type
566
602
  };
567
- postData = $.extend(postData, tessaParams);
568
- return FileChecksum.create(file, (function(error, checksum) {
569
- if (error) {
570
- return done(error);
603
+ return WCCDropzone;
604
+ }(BaseDropzone);
605
+ var uploadPendingWarning = "File uploads have not yet completed. If you submit the form now they will not be saved. Are you sure you want to continue?";
606
+ function tessaInit() {
607
+ $(".tessa-upload").each((function(i, item) {
608
+ var $item = $(item);
609
+ var directUploadURL = $item.data("direct-upload-url") || "/rails/active_storage/direct_uploads";
610
+ var options = __assign({
611
+ maxFiles: 1,
612
+ addRemoveLinks: true,
613
+ url: "UNUSED",
614
+ dictDefaultMessage: "Drop files or click to upload.",
615
+ accept: createAcceptFn({
616
+ directUploadURL: directUploadURL
617
+ })
618
+ }, $item.data("dropzone-options"));
619
+ if ($item.hasClass("multiple")) {
620
+ options.maxFiles = undefined;
571
621
  }
572
- postData["checksum"] = checksum;
573
- return $.ajax("/tessa/uploads", {
574
- type: "POST",
575
- data: postData,
576
- success: function(response) {
577
- file.uploadURL = response.upload_url;
578
- file.uploadMethod = response.upload_method;
579
- file.uploadHeaders = response.upload_headers;
580
- file.assetID = response.asset_id;
581
- return done();
582
- },
583
- error: function(response) {
584
- return done("Failed to initiate the upload process!");
585
- }
586
- });
622
+ var dropzone = new WCCDropzone(item, options);
623
+ $item.find('input[type="hidden"]').each((function(i, input) {
624
+ var _a, _b;
625
+ var $input = $(input);
626
+ var mockFile = $input.data("meta");
627
+ if (!mockFile) {
628
+ return;
629
+ }
630
+ mockFile.accepted = true;
631
+ (_a = dropzone.options.addedfile) === null || _a === void 0 ? void 0 : _a.call(dropzone, mockFile);
632
+ (_b = dropzone.options.thumbnail) === null || _b === void 0 ? void 0 : _b.call(dropzone, mockFile, mockFile.url);
633
+ dropzone.emit("complete", mockFile);
634
+ dropzone.files.push(mockFile);
635
+ }));
636
+ var inputName = $item.data("input-name") || $item.find('input[type="hidden"]').attr("name");
637
+ dropzone.on("success", (function(file) {
638
+ $('input[name="'.concat(inputName, '"]')).val(file.signedID);
639
+ }));
640
+ dropzone.on("removedfile", (function(file) {
641
+ $item.find('input[name="'.concat(inputName, '"]')).val("");
642
+ }));
587
643
  }));
588
- };
589
- window.WCC.tessaInit = function(sel) {
590
- sel = sel || "form:has(.tessa-upload)";
591
- $(sel).each((function(i, form) {
592
- var $form;
593
- $form = $(form);
594
- return $form.bind("submit", (function(event) {
595
- var safeToSubmit;
596
- safeToSubmit = true;
644
+ $("form:has(.tessa-upload)").each((function(i, form) {
645
+ var $form = $(form);
646
+ $form.on("submit", (function(event) {
647
+ var safeToSubmit = true;
597
648
  $form.find(".tessa-upload").each((function(j, dropzoneElement) {
598
- return $(dropzoneElement.dropzone.files).each((function(k, file) {
599
- if (file.status != null && file.status !== WCC.Dropzone.SUCCESS) {
600
- return safeToSubmit = false;
649
+ dropzoneElement.dropzone.files.forEach((function(file) {
650
+ if (file.status && file.status != WCCDropzone.SUCCESS) {
651
+ safeToSubmit = false;
601
652
  }
602
653
  }));
603
654
  }));
604
- if (!safeToSubmit && !confirm(WCC.Dropzone.uploadPendingWarning)) {
655
+ if (!safeToSubmit && !confirm(uploadPendingWarning)) {
605
656
  return false;
606
657
  }
607
658
  }));
608
659
  }));
609
- return $(".tessa-upload", sel).each((function(i, item) {
610
- var $item, args, dropzone, inputPrefix, updateAction;
611
- $item = $(item);
612
- args = {
613
- maxFiles: 1,
614
- addRemoveLinks: true
660
+ }
661
+ function createAcceptFn(_a) {
662
+ var directUploadURL = _a.directUploadURL;
663
+ return function(file, done) {
664
+ var postData = {
665
+ blob: {
666
+ filename: file.name,
667
+ byte_size: file.size,
668
+ content_type: file.type,
669
+ checksum: ""
670
+ }
615
671
  };
616
- $.extend(args, $item.data("dropzone-options"));
617
- if ($item.hasClass("multiple")) {
618
- args.maxFiles = null;
619
- }
620
- inputPrefix = $item.data("asset-field-prefix");
621
- dropzone = new WCC.Dropzone(item, args);
622
- $item.find('input[type="hidden"]').each((function(j, input) {
623
- var $input, mockFile;
624
- $input = $(input);
625
- mockFile = $input.data("meta");
626
- mockFile.accepted = true;
627
- dropzone.options.addedfile.call(dropzone, mockFile);
628
- dropzone.options.thumbnail.call(dropzone, mockFile, mockFile.url);
629
- dropzone.emit("complete", mockFile);
630
- return dropzone.files.push(mockFile);
631
- }));
632
- updateAction = function(file) {
633
- var actionInput, inputID;
634
- if (file.assetID == null) {
635
- return;
672
+ FileChecksum.create(file, (function(error, checksum) {
673
+ if (error) {
674
+ return done(error);
636
675
  }
637
- inputID = `#tessa_asset_action_${file.assetID}`;
638
- actionInput = $(inputID);
639
- if (!actionInput.length) {
640
- actionInput = $('<input type="hidden">').attr({
641
- id: inputID,
642
- name: `${inputPrefix}[${file.assetID}][action]`
643
- }).appendTo(item);
676
+ if (!checksum) {
677
+ return done("Failed to generate checksum for file '".concat(file.name, "'"));
644
678
  }
645
- return actionInput.val(file.action);
646
- };
647
- dropzone.on("success", (function(file) {
648
- file.action = "add";
649
- return updateAction(file);
650
- }));
651
- return dropzone.on("removedfile", (function(file) {
652
- file.action = "remove";
653
- return updateAction(file);
679
+ postData.blob["checksum"] = checksum;
680
+ $.ajax(directUploadURL, {
681
+ type: "POST",
682
+ data: postData,
683
+ success: function(response) {
684
+ file.uploadURL = response.direct_upload.url;
685
+ file.uploadMethod = "PUT";
686
+ file.uploadHeaders = response.direct_upload.headers;
687
+ file.signedID = response.signed_id;
688
+ done();
689
+ },
690
+ error: function(response) {
691
+ console.error(response);
692
+ done("Failed to initiate the upload process!");
693
+ }
694
+ });
654
695
  }));
655
- }));
656
- };
657
- $((function() {
658
- return window.WCC.tessaInit();
659
- }));
696
+ };
697
+ }
698
+ $(tessaInit);
660
699
  }));
@@ -0,0 +1,76 @@
1
+ import * as SparkMD5 from "spark-md5"
2
+
3
+ const fileSlice = File.prototype.slice || (File.prototype as any).mozSlice || (File.prototype as any).webkitSlice
4
+
5
+ type FileChecksumCallback = (error: string | null, checksum?: string) => void
6
+
7
+ export class FileChecksum {
8
+ static create(file: File, callback: FileChecksumCallback) {
9
+ const instance = new FileChecksum(file)
10
+ instance.create(callback)
11
+ }
12
+
13
+ private file: File
14
+ private chunkSize: number
15
+ private chunkCount: number
16
+ private chunkIndex: number
17
+ private md5Buffer?: SparkMD5.ArrayBuffer
18
+ private fileReader?: FileReader
19
+ private callback?: FileChecksumCallback
20
+
21
+ constructor(file: File) {
22
+ this.file = file
23
+ this.chunkSize = 2097152 // 2MB
24
+ this.chunkCount = Math.ceil(this.file.size / this.chunkSize)
25
+ this.chunkIndex = 0
26
+ }
27
+
28
+ create(callback: FileChecksumCallback) {
29
+ this.callback = callback
30
+ this.md5Buffer = new SparkMD5.ArrayBuffer
31
+ this.fileReader = new FileReader
32
+ this.fileReader.addEventListener("load", event => this.fileReaderDidLoad(event))
33
+ this.fileReader.addEventListener("error", event => this.fileReaderDidError(event))
34
+ this.readNextChunk()
35
+ }
36
+
37
+ private fileReaderDidLoad(event: ProgressEvent<FileReader>) {
38
+ if (!this.md5Buffer || !this.fileReader) {
39
+ throw new Error("FileChecksum: fileReaderDidLoad called before create")
40
+ }
41
+ if (!event.target?.result) { return }
42
+
43
+ this.md5Buffer.append(event.target.result as ArrayBuffer)
44
+
45
+ if (!this.readNextChunk()) {
46
+ const binaryDigest = this.md5Buffer.end(true)
47
+ const base64digest = btoa(binaryDigest)
48
+ if (this.callback) {
49
+ this.callback(null, base64digest)
50
+ }
51
+ }
52
+ }
53
+
54
+ private fileReaderDidError(event: ProgressEvent<FileReader>) {
55
+ if (this.callback) {
56
+ this.callback(`Error reading ${this.file.name}`)
57
+ }
58
+ }
59
+
60
+ private readNextChunk() {
61
+ if (!this.fileReader) {
62
+ throw new Error("FileChecksum: readNextChunk called before create")
63
+ }
64
+
65
+ if (this.chunkIndex < this.chunkCount || (this.chunkIndex == 0 && this.chunkCount == 0)) {
66
+ const start = this.chunkIndex * this.chunkSize
67
+ const end = Math.min(start + this.chunkSize, this.file.size)
68
+ const bytes = fileSlice.call(this.file, start, end)
69
+ this.fileReader.readAsArrayBuffer(bytes)
70
+ this.chunkIndex++
71
+ return true
72
+ } else {
73
+ return false
74
+ }
75
+ }
76
+ }