tessa 2.0 → 6.0.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
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
+ }