PluginProbe
FV Player 8 / trunk
FV Player 8 vtrunk
trunk 8.0.18 8.0.19 8.0.20 8.0.21 8.0.25 8.0.27 8.1 8.1.3
fv-player / js / s3upload.js

s3upload.js in FV Player 8 trunk, at js/s3upload.js

495 lines 17.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 function S3MultiUpload( file, options ) {
2 let ajaxurl = false;
3 if ( window.fv_flowplayer_browser ) {
4 ajaxurl = window.fv_flowplayer_browser.ajaxurl;
5 } else if ( window.fv_player_s3_uploader ) {
6 ajaxurl = window.fv_player_s3_uploader.ajaxurl;
7 }
8
9 if ( ! ajaxurl ) {
10 console.error( 'S3MultiUpload: ajaxurl not found' );
11 return;
12 }
13
14 this.PART_SIZE = 100 * 1024 * 1024; // 100 MB per chunk
15 // this.PART_SIZE = 5 * 1024 * 1024 * 1024; // Minimum part size defined by aws s3 is 5 MB, maximum 5 GB
16 this.SERVER_LOC = ajaxurl + '?'; // Location of our server where we'll send all AWS commands and multipart instructions
17 this.completed = false;
18 this.file = file;
19
20 // Sanitize the filename to only allow letters, numbers, and hyphens
21 var sanitizedFilename = this.sanitizeFilename(this.file.name);
22
23 this.fileInfo = {
24 name: sanitizedFilename,
25 type: this.file.type,
26 size: this.file.size,
27 lastModifiedDate: this.file.lastModifiedDate
28 };
29 this.sendBackData = null;
30 this.uploadXHR = [];
31 // Progress monitoring
32 this.byterate = []
33 this.lastUploadedSize = []
34 this.lastUploadedTime = []
35 this.loaded = [];
36 this.total = [];
37 this.chunkRetries = {};
38 this.maxRetries = 4;
39 this.retryBackoffTimeout = 15000; // ms
40 this.completeErrors = 1;
41 this.validationChunkSize = 5 * 1024 * 1024;
42
43 // Nonces - will be set after validation
44 this.create_multiupload_nonce = null;
45 this.multiupload_send_part_nonce = null;
46 this.multiupload_abort_nonce = null;
47 this.multiupload_complete_nonce = null;
48
49 this.min_duration = options.min_duration;
50 this.min_duration_msg = options.min_duration_msg;
51 this.max_duration = options.max_duration;
52 this.max_duration_msg = options.max_duration_msg;
53 this.vertical_only = options.vertical_only;
54 this.vertical_only_msg = options.vertical_only_msg;
55 }
56
57 /**
58 * Sanitizes filename to only allow letters, numbers, and hyphens
59 * @param {string} filename The original filename
60 * @returns {string} The sanitized filename
61 */
62 S3MultiUpload.prototype.sanitizeFilename = function(filename) {
63 // Remove file extension first
64 var lastDotIndex = filename.lastIndexOf('.');
65 var name = filename.substring(0, lastDotIndex);
66 var extension = filename.substring(lastDotIndex);
67
68 // Sanitize the name part: keep only letters, numbers, and hyphens
69 // Replace any characters that are not letters, numbers, or hyphens with hyphens
70 // Also replace multiple consecutive hyphens with a single hyphen
71 var sanitizedName = name.replace(/[^a-zA-Z0-9-]/g, '-').replace(/-+/g, '-');
72
73 // Remove leading and trailing hyphens
74 sanitizedName = sanitizedName.replace(/^-+|-+$/g, '');
75
76 // If the sanitized name is empty, use 'file'
77 if (!sanitizedName) {
78 sanitizedName = 'file';
79 }
80
81 // Return the sanitized name with the original extension
82 return sanitizedName + extension;
83 };
84
85 /**
86 * Uploads the first 5MB of the file for validation
87 */
88 S3MultiUpload.prototype.validateFile = function() {
89 var self = this;
90
91 // Create a blob with the first 1MB of the file
92 var validationBlob = this.file.slice(0, this.validationChunkSize);
93
94 // Create FormData to send the file chunk
95 var formData = new FormData();
96 formData.append('action', 'validate_file_upload');
97 formData.append('file_chunk', validationBlob, this.fileInfo.name);
98 formData.append('file_info', JSON.stringify(this.fileInfo));
99 formData.append('nonce', window.fv_player_s3_uploader.validate_file_nonce || '');
100
101 jQuery.ajax({
102 url: self.SERVER_LOC,
103 type: 'POST',
104 data: formData,
105 processData: false,
106 contentType: false,
107 xhr: function() {
108 var xhr = new window.XMLHttpRequest();
109 xhr.upload.addEventListener('progress', function(e) {
110 if (e.lengthComputable) {
111 var percentComplete = (e.loaded / e.total) * 100;
112 self.onValidationProgress(percentComplete);
113 }
114 }, false);
115 return xhr;
116 }
117 }).done(function(data) {
118 if ( data.file_analysis ) {
119
120 if ( data.file_analysis.duration ) {
121 if ( self.min_duration && self.min_duration.value && data.file_analysis.duration < self.min_duration.value ) {
122 self.onValidationError( self.min_duration.msg );
123 return;
124 } else if ( self.max_duration && self.max_duration.value && data.file_analysis.duration > self.max_duration.value ) {
125 self.onValidationError( self.max_duration.msg );
126 return;
127 }
128
129 // Set the Betube video submission field
130 jQuery( 'body.wp-theme-betube [name=post_time]' ).val( data.file_analysis.duration );
131 }
132 if ( data.file_analysis.height ) {
133
134 // Set the Betube video submission field
135 jQuery( 'body.wp-theme-betube [name=post_quality]' ).val( data.file_analysis.height + 'p' );
136
137 if ( self.vertical_only && self.vertical_only.value && data.file_analysis.width > data.file_analysis.height ) {
138 self.onValidationError( self.vertical_only.msg );
139 return;
140 }
141 }
142 }
143
144 if (data.error) {
145 self.onValidationError(data.error);
146 } else {
147
148 // Store the nonces from validation response
149 self.create_multiupload_nonce = data.create_multiupload_nonce;
150 self.multiupload_send_part_nonce = data.multiupload_send_part_nonce;
151 self.multiupload_abort_nonce = data.multiupload_abort_nonce;
152 self.multiupload_complete_nonce = data.multiupload_complete_nonce;
153
154 self.onValidationSuccess(data);
155 self.createMultipartUpload();
156 }
157 }).fail( function(jqXHR, textStatus, errorThrown) {
158 // Try to extract as much info as possible from the jqXHR object
159 var errorMsg = 'Validation request failed: ' + textStatus;
160
161 // Check for HTTP status
162 if (jqXHR.status) {
163 errorMsg += ' (HTTP ' + jqXHR.status + ')';
164
165 // HTTP 413 Content Too Large
166 if ( 413 === parseInt( jqXHR.status ) ) {
167 errorMsg += ' - File size is too large.';
168 }
169 }
170
171 // Try to extract server response text
172 if (jqXHR.responseText) {
173 // Try to parse as JSON for any structured error message
174 try {
175 var responseJson = JSON.parse(jqXHR.responseText);
176 if (responseJson.error) {
177 errorMsg += ' - ' + responseJson.error;
178 } else if (responseJson.message) {
179 errorMsg += ' - ' + responseJson.message;
180 } else {
181 errorMsg += ' - ' + jqXHR.responseText;
182 }
183 } catch (e) {
184 // Not JSON, but check if there's HTML—strip tags before appending the response
185 var rawResponse = jqXHR.responseText;
186 if (typeof rawResponse === "string") {
187 // Create a temporary DOM element and extract text content to strip HTML tags
188 var tempDiv = document.createElement('div');
189 tempDiv.innerHTML = rawResponse;
190 rawResponse = tempDiv.textContent || tempDiv.innerText || "";
191 }
192
193 // If s3-ajax.php responded with "Error: action not set!", then it's likely because the entire request got broken due to running into the limit of upload size.
194 if ( 'Error: action not set!' === rawResponse ) {
195 errorMsg = ' - File size is too large.';
196 }
197
198 errorMsg += ' - ' + rawResponse;
199 }
200 }
201
202 // If there's an explicit error passed by jQuery as errorThrown, add it too
203 if (errorThrown) {
204 errorMsg += ' (' + errorThrown + ')';
205 }
206
207 self.onValidationError(errorMsg);
208 });
209 };
210
211 /**
212 * Creates the multipart upload
213 */
214 S3MultiUpload.prototype.createMultipartUpload = function() {
215 var self = this;
216
217 if( window.fv_player_media_browser && fv_player_media_browser.get_current_folder() === 'Home/' ) { // root folder
218 self.fileInfo.name = fv_player_media_browser.get_current_folder() + self.fileInfo.name
219 } else if ( window.fv_player_media_browser && fv_player_media_browser.get_current_folder() ) { // nested folder
220 self.fileInfo.name = fv_player_media_browser.get_current_folder() + '/' + self.fileInfo.name
221 } else {
222 // TODO: Make sure this cannot be changed by the user.
223 self.fileInfo.name = 'frontend/' + self.fileInfo.name
224 }
225
226 // TODO: Force some folder for user uploads
227
228 jQuery.post(self.SERVER_LOC, {
229 action: 'create_multiupload',
230 fileInfo: self.fileInfo,
231 nonce: this.create_multiupload_nonce
232 }).done(function(data) {
233 if( data.error ) {
234 self.onServerError('create', null, data.error, null);
235 } else {
236 self.sendBackData = data;
237 self.uploadParts();
238 }
239 }).fail(function(jqXHR, textStatus, errorThrown) {
240 self.onServerError('create', jqXHR, textStatus, errorThrown);
241 });
242 };
243
244 /**
245 * Call this function to start uploading to server
246 */
247 S3MultiUpload.prototype.start = function() {
248 this.validateFile();
249 };
250
251 /** private */
252 S3MultiUpload.prototype.uploadParts = function() {
253 var blobs = this.blobs = [], promises = [];
254 var start = 0;
255 var end, blob;
256 var partNum = 0;
257
258 while(start < this.file.size) {
259 end = Math.min(start + this.PART_SIZE, this.file.size);
260 var filePart = this.file.slice(start, end);
261 // this is to prevent push blob with 0Kb
262 if (filePart.size > 0)
263 blobs.push(filePart);
264 start = this.PART_SIZE * ++partNum;
265 }
266
267 for (var i = 0; i < blobs.length; i++) {
268 blob = blobs[i];
269 promises.push(this.uploadXHR[i]=jQuery.post(this.SERVER_LOC, {
270 action: 'multiupload_send_part',
271 sendBackData: this.sendBackData,
272 partNumber: i+1,
273 contentLength: blob.size,
274 nonce: this.multiupload_send_part_nonce
275 }));
276 }
277
278 jQuery.when.apply(null, promises)
279 .then(this.sendAll.bind(this), this.onServerError)
280 .done(this.onPrepareCompleted);
281 };
282
283 /**
284 * Sends all the created upload parts in a loop
285 */
286 S3MultiUpload.prototype.sendAll = function() {
287 var blobs = this.blobs;
288 var length = blobs.length;
289 if (length==1)
290 this.sendToS3(arguments[0], blobs[0], 0);
291 else for (var i = 0; i < length; i++) {
292 this.sendToS3(arguments[i][0], blobs[i], i);
293 }
294 };
295 /**
296 * Used to send each uploadPart
297 * @param array data parameters of the part
298 * @param blob blob data bytes
299 * @param integer index part index (base zero)
300 */
301 S3MultiUpload.prototype.sendToS3 = function(data, blob, index) {
302 var self = this;
303 var url = data['url'];
304 var size = blob.size;
305 var request = self.uploadXHR[index] = new XMLHttpRequest();
306 request.onreadystatechange = function() {
307 if (request.readyState === 4) { // 4 is DONE
308 // on abort, don't count that as an error - aborted is a manually added field, since status would be 0 whether
309 // we aborted manually or an Internet connection interrupt occured, so that's no use to us
310 if ( !request.aborted ) {
311 // self.uploadXHR[index] = null;
312 if (request.status !== 200) {
313 // check if we should retry this transfer of fail
314 if (!self.chunkRetries[url] || self.chunkRetries[url] < self.maxRetries) {
315 if (!self.chunkRetries[url]) {
316 self.chunkRetries[url] = 1;
317 } else {
318 self.chunkRetries[url]++;
319 }
320
321 //console.log('will retry ' + url + ' due to invalid request status: ' + request.status + ' (' + request.responseText + ')');
322 setTimeout(function () {
323 //console.log('starting retry #' + self.chunkRetries[ url ] + ' for ' + url );
324 self.sendToS3(data, blob, index);
325 }, self.retryBackoffTimeout * self.chunkRetries[url]);
326 } else {
327 self.updateProgress();
328 self.onS3UploadError(request);
329 }
330 return;
331 }
332 }
333 self.updateProgress();
334 }
335 };
336
337 request.upload.onprogress = function(e) {
338 if (e.lengthComputable) {
339 self.total[index] = size;
340 self.loaded[index] = e.loaded;
341 if (self.lastUploadedTime[index])
342 {
343 var time_diff=(new Date().getTime() - self.lastUploadedTime[index])/1000;
344 if (time_diff > 0.005) // 5 miliseconds has passed
345 {
346 var byterate=(self.loaded[index] - self.lastUploadedSize[index])/time_diff;
347 self.byterate[index] = byterate;
348 self.lastUploadedTime[index]=new Date().getTime();
349 self.lastUploadedSize[index]=self.loaded[index];
350 }
351 }
352 else
353 {
354 self.byterate[index] = 0;
355 self.lastUploadedTime[index]=new Date().getTime();
356 self.lastUploadedSize[index]=self.loaded[index];
357 }
358 // Only send update to user once, regardless of how many
359 // parallel XHRs we have (unless the first one is over).
360 if (index==0 || self.total[0]==self.loaded[0])
361 self.updateProgress();
362 }
363 };
364 request.open('PUT', url, true);
365 request.send(blob);
366 };
367
368 /**
369 * Abort multipart upload
370 */
371 S3MultiUpload.prototype.cancel = function() {
372 var self = this;
373 for (var i=0; i<this.uploadXHR.length; ++i) {
374 this.uploadXHR[i].aborted = true;
375 this.uploadXHR[i].abort();
376 }
377 jQuery.post(self.SERVER_LOC, {
378 action: 'multiupload_abort',
379 sendBackData: self.sendBackData,
380 nonce: this.multiupload_abort_nonce
381 }).done(function(data) {
382
383 });
384 };
385
386 /**
387 * Complete multipart upload
388 */
389 S3MultiUpload.prototype.completeMultipartUpload = function() {
390 var self = this;
391
392 if (this.completed) return;
393
394 self.completed = true; // prevent multiple calls to this function
395
396 jQuery.post(self.SERVER_LOC, {
397 action: 'multiupload_complete',
398 sendBackData: self.sendBackData,
399 nonce: this.multiupload_complete_nonce
400 }).done(function(data) {
401 self.onUploadCompleted(data);
402 self.completeErrors = 1;
403
404 }).fail(function(jqXHR, textStatus, errorThrown) {
405 // if we had an error, retry and only show error if at least 3 completion requests fail
406 if ( this.completeErrors++ > 3 ) {
407 self.onServerError('complete', jqXHR, textStatus, errorThrown);
408 self.completeErrors = 1;
409 self.completed = true;
410 } else {
411 setTimeout( function() {
412 self.completed = false;
413 self.completeMultipartUpload();
414 } , this.completeErrors * this.retryBackoffTimeout );
415 }
416 });
417 };
418
419 /**
420 * Track progress, propagate event, and check for completion
421 */
422 S3MultiUpload.prototype.updateProgress = function() {
423 var total=0;
424 var loaded=0;
425 var byterate=0.0;
426 var complete=1;
427 for (var i=0; i<this.total.length; ++i) {
428 loaded += +this.loaded[i] || 0;
429 total += this.total[i];
430 if (this.loaded[i]!=this.total[i])
431 {
432 // Only count byterate for active transfers
433 byterate += +this.byterate[i] || 0;
434 complete=0;
435 }
436 }
437 if (complete)
438 this.completeMultipartUpload();
439 total=this.fileInfo.size;
440 this.onProgressChanged(loaded, total, byterate);
441 };
442
443 // Overridable events:
444
445 /**
446 * Overrride this function to catch errors occured when communicating to your server
447 *
448 * @param {type} command Name of the command which failed,one of 'CreateMultipartUpload', 'SignUploadPart','CompleteMultipartUpload'
449 * @param {type} jqXHR jQuery XHR
450 * @param {type} textStatus resonse text status
451 * @param {type} errorThrown the error thrown by the server
452 */
453 S3MultiUpload.prototype.onServerError = function(command, jqXHR, textStatus, errorThrown) {};
454
455 /**
456 * Overrride this function to catch errors occured when uploading to S3
457 *
458 * @param XMLHttpRequest xhr the XMLHttpRequest object
459 */
460 S3MultiUpload.prototype.onS3UploadError = function(xhr) {};
461
462 /**
463 * Override this function to show user update progress
464 *
465 * @param {type} uploadedSize is the total uploaded bytes
466 * @param {type} totalSize the total size of the uploading file
467 * @param {type} speed bytes per second
468 */
469 S3MultiUpload.prototype.onProgressChanged = function(uploadedSize, totalSize, bitrate) {};
470
471 /**
472 * Override this method to execute something when upload finishes
473 *
474 */
475 S3MultiUpload.prototype.onUploadCompleted = function(serverData) {};
476 /**
477 * Override this method to execute something when part preparation is completed
478 *
479 */
480 S3MultiUpload.prototype.onPrepareCompleted = function() {};
481
482 /**
483 * Override this method to handle validation progress updates
484 *
485 * @param {number} percentComplete Percentage of validation upload completed (0-100)
486 */
487 S3MultiUpload.prototype.onValidationProgress = function(percentComplete) {};
488
489 /**
490 * Override this method to handle successful file validation
491 *
492 * @param {object} data Response data from validation server
493 */
494 S3MultiUpload.prototype.onValidationSuccess = function(data) {};
495