From 70281fd17ec689f44ab9c3ee1d4828db72b121a0 Mon Sep 17 00:00:00 2001 From: Matthias Mullie Date: Fri, 7 Jul 2017 17:11:25 +0200 Subject: [PATCH] Make chunkedUpload match upload behavior .upload will upload the entire file, then reject the promise if it has warnings. This also happened in chunkedUpload, but a warning could already be present after uploading only 1 chunk & that rejection would cause the entire chain of chunks to stop. Instead, it'll now keep uploading all chunks & then reject with the complete upload data. Also fixed how abort is dealt with when retrying because of network issues. Change-Id: I8ecef2271359a9505c8c5fa22883b0df55e9e914 --- resources/src/mediawiki/api/upload.js | 88 +++++++++++++++++++++------ 1 file changed, 69 insertions(+), 19 deletions(-) diff --git a/resources/src/mediawiki/api/upload.js b/resources/src/mediawiki/api/upload.js index 4a2895d822..29bd59aed9 100644 --- a/resources/src/mediawiki/api/upload.js +++ b/resources/src/mediawiki/api/upload.js @@ -392,7 +392,7 @@ * @return {jQuery.Promise} */ uploadChunk: function ( file, data, start, end, filekey, retries ) { - var upload, retry, + var upload, api = this, chunk = this.slice( file, start, end ); @@ -401,22 +401,6 @@ // In such case, it could be useful to try again: a network hickup // doesn't necessarily have to result in upload failure... retries = retries === undefined ? 1 : retries; - retry = function ( code, result ) { - var deferred = $.Deferred(), - callback = function () { - api.uploadChunk( file, data, start, end, filekey, retries - 1 ) - .then( deferred.resolve, deferred.reject ); - }; - - // Don't retry if the request failed because we aborted it (or - // if it's another kind of request failure) - if ( code !== 'http' || result.textStatus === 'abort' ) { - return deferred.reject( code, result ); - } - - setTimeout( callback, 1000 ); - return deferred.promise(); - }; data.filesize = file.size; data.chunk = chunk; @@ -431,8 +415,36 @@ upload = this.uploadWithFormData( file, data ); return upload.then( null, - // If the call fails, we may want to try again... - retries === 0 ? null : retry, + function ( code, result ) { + var retry; + + // uploadWithFormData will reject uploads with warnings, but + // these warnings could be "harmless" or recovered from + // (e.g. exists-normalized, when it'll be renamed later) + // In the case of (only) a warning, we still want to + // continue the chunked upload until it completes: then + // reject it - at least it's been fully uploaded by then and + // failure handlers have a complete result object (including + // possibly more warnings, e.g. duplicate) + // This matches .upload, which also completes the upload. + if ( result.upload && result.upload.warnings && code in result.upload.warnings ) { + if ( end === file.size ) { + // uploaded last chunk = reject with result data + return $.Deferred().reject( code, result ); + } else { + // still uploading chunks = resolve to keep going + return $.Deferred().resolve( result ); + } + } + + if ( retries === 0 ) { + return $.Deferred().reject( code, result ); + } + + // If the call flat out failed, we may want to try again... + retry = api.uploadChunk.bind( this, file, data, start, end, filekey, retries - 1 ); + return api.retry( code, result, retry ); + }, function ( fraction ) { // Since we're only uploading small parts of a file, we // need to adjust the reported progress to reflect where @@ -442,6 +454,44 @@ ).promise( { abort: upload.abort } ); }, + /** + * Launch the upload anew if it failed because of network issues. + * + * @private + * @param {string} code Error code + * @param {Object} result API result + * @param {Function} callable + * @return {jQuery.Promise} + */ + retry: function ( code, result, callable ) { + var uploadPromise, + retryTimer, + deferred = $.Deferred(), + // Wrap around the callable, so that once it completes, it'll + // resolve/reject the promise we'll return + retry = function () { + uploadPromise = callable(); + uploadPromise.then( deferred.resolve, deferred.reject ); + }; + + // Don't retry if the request failed because we aborted it (or if + // it's another kind of request failure) + if ( code !== 'http' || result.textStatus === 'abort' ) { + return deferred.reject( code, result ); + } + + retryTimer = setTimeout( retry, 1000 ); + return deferred.promise( { abort: function () { + // Clear the scheduled upload, or abort if already in flight + if ( retryTimer ) { + clearTimeout( retryTimer ); + } + if ( uploadPromise.abort ) { + uploadPromise.abort(); + } + } } ); + }, + /** * Slice a chunk out of a File object. * -- 2.20.1