Skip to main content

max / makenotwork

4.1 KB · 115 lines History Blame Raw
1 /**
2 * Shared S3 upload utilities.
3 *
4 * Usage:
5 * const uploader = new S3Uploader({
6 * filenameEl: document.getElementById('upload-filename'),
7 * percentEl: document.getElementById('upload-percent'),
8 * progressBar: document.getElementById('progress-bar'),
9 * });
10 * uploader.upload(presignedUrl, file, s3Key, fallbackContentType)
11 * .then(s3Key => { ... })
12 * .catch(err => { ... });
13 * uploader.cancel();
14 */
15
16 function S3Uploader(opts) {
17 this.filenameEl = opts.filenameEl;
18 this.percentEl = opts.percentEl;
19 this.progressBar = opts.progressBar;
20 this.speedEl = opts.speedEl || null;
21 this._xhr = null;
22 }
23
24 /** Start an upload. Returns a Promise resolving to the s3Key. */
25 S3Uploader.prototype.upload = function(url, file, s3Key, fallbackContentType, cacheControl) {
26 var self = this;
27 var startTime = Date.now();
28
29 if (self.filenameEl) self.filenameEl.textContent = file.name;
30 if (self.percentEl) self.percentEl.textContent = '0%';
31 if (self.progressBar) self.progressBar.style.width = '0%';
32 if (self.speedEl) self.speedEl.textContent = '';
33
34 return new Promise(function(resolve, reject) {
35 var xhr = new XMLHttpRequest();
36 self._xhr = xhr;
37
38 xhr.upload.addEventListener('progress', function(e) {
39 if (e.lengthComputable) {
40 var percent = Math.round((e.loaded / e.total) * 100);
41 if (self.percentEl) self.percentEl.textContent = percent + '%';
42 if (self.progressBar) self.progressBar.style.width = percent + '%';
43 if (self.speedEl && e.loaded > 0) {
44 var elapsed = (Date.now() - startTime) / 1000;
45 if (elapsed > 0.5) {
46 var speed = e.loaded / elapsed;
47 var remaining = (e.total - e.loaded) / speed;
48 var speedStr = speed > 1024 * 1024
49 ? (speed / (1024 * 1024)).toFixed(1) + ' MB/s'
50 : (speed / 1024).toFixed(0) + ' KB/s';
51 var etaStr = remaining < 60
52 ? Math.ceil(remaining) + 's'
53 : Math.ceil(remaining / 60) + 'm ' + Math.ceil(remaining % 60) + 's';
54 self.speedEl.textContent = speedStr + ' \u2014 ' + etaStr + ' remaining';
55 }
56 }
57 }
58 });
59
60 xhr.addEventListener('load', function() {
61 self._xhr = null;
62 if (xhr.status >= 200 && xhr.status < 300) {
63 resolve(s3Key);
64 } else {
65 reject(new Error('Upload failed: ' + xhr.status));
66 }
67 });
68
69 xhr.addEventListener('error', function() {
70 self._xhr = null;
71 reject(new Error('Network error during upload'));
72 });
73
74 xhr.addEventListener('abort', function() {
75 self._xhr = null;
76 reject(new Error('Upload cancelled'));
77 });
78
79 xhr.open('PUT', url);
80 xhr.setRequestHeader('Content-Type', file.type || fallbackContentType || 'application/octet-stream');
81 if (cacheControl) xhr.setRequestHeader('Cache-Control', cacheControl);
82 xhr.send(file);
83 });
84 };
85
86 /** Abort any in-progress upload. */
87 S3Uploader.prototype.cancel = function() {
88 if (this._xhr) {
89 this._xhr.abort();
90 this._xhr = null;
91 }
92 };
93
94 /* Dropzone helpers — wire up drag/drop on an element */
95 function initDropzone(dropzoneEl, fileInputEl, onFile) {
96 dropzoneEl.addEventListener('dragover', function(e) {
97 e.preventDefault();
98 dropzoneEl.classList.add('dragover');
99 });
100 dropzoneEl.addEventListener('dragleave', function() {
101 dropzoneEl.classList.remove('dragover');
102 });
103 dropzoneEl.addEventListener('drop', function(e) {
104 e.preventDefault();
105 dropzoneEl.classList.remove('dragover');
106 if (e.dataTransfer.files[0]) onFile(e.dataTransfer.files[0]);
107 });
108 dropzoneEl.addEventListener('click', function() {
109 fileInputEl.click();
110 });
111 fileInputEl.addEventListener('change', function() {
112 if (fileInputEl.files[0]) onFile(fileInputEl.files[0]);
113 });
114 }
115