Page MenuHomePhabricator

D12066.id29034.diff
No OneTemporary

D12066.id29034.diff

diff --git a/resources/celerity/map.php b/resources/celerity/map.php
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -8,10 +8,10 @@
return array(
'names' => array(
'core.pkg.css' => 'efdeeb14',
- 'core.pkg.js' => '5f50c01b',
+ 'core.pkg.js' => 'b0d70fa0',
'darkconsole.pkg.js' => '8ab24e01',
'differential.pkg.css' => '1940be3f',
- 'differential.pkg.js' => '53c1ccc2',
+ 'differential.pkg.js' => 'be1e5f9b',
'diffusion.pkg.css' => '591664fa',
'diffusion.pkg.js' => 'bfc0737b',
'maniphest.pkg.css' => '68d4dd3d',
@@ -438,9 +438,9 @@
'rsrc/js/application/uiexample/gesture-example.js' => '558829c2',
'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5',
'rsrc/js/core/Busy.js' => '6453c869',
- 'rsrc/js/core/DragAndDropFileUpload.js' => '8c49f386',
+ 'rsrc/js/core/DragAndDropFileUpload.js' => 'fd6ace61',
'rsrc/js/core/DraggableList.js' => 'a16ec1c6',
- 'rsrc/js/core/FileUpload.js' => 'a4ae61bf',
+ 'rsrc/js/core/FileUpload.js' => '477359c8',
'rsrc/js/core/Hovercard.js' => '7e8468ae',
'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2',
'rsrc/js/core/KeyboardShortcutManager.js' => 'c1700f6f',
@@ -458,13 +458,13 @@
'rsrc/js/core/behavior-crop.js' => 'fa0f4fc2',
'rsrc/js/core/behavior-dark-console.js' => '08883e8b',
'rsrc/js/core/behavior-device.js' => '03d6ed07',
- 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '92eb531d',
+ 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '6d49590e',
'rsrc/js/core/behavior-error-log.js' => '6882e80a',
'rsrc/js/core/behavior-fancy-datepicker.js' => 'c51ae228',
'rsrc/js/core/behavior-file-tree.js' => '88236f00',
'rsrc/js/core/behavior-form.js' => '5c54cbf3',
'rsrc/js/core/behavior-gesture.js' => '3ab51e2c',
- 'rsrc/js/core/behavior-global-drag-and-drop.js' => '07f199d8',
+ 'rsrc/js/core/behavior-global-drag-and-drop.js' => '68a56fa5',
'rsrc/js/core/behavior-high-security-warning.js' => '8fc1c918',
'rsrc/js/core/behavior-history-install.js' => '7ee2b591',
'rsrc/js/core/behavior-hovercard.js' => 'f36e01af',
@@ -549,7 +549,7 @@
'javelin-behavior-aphlict-status' => 'ea681761',
'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884',
'javelin-behavior-aphront-crop' => 'fa0f4fc2',
- 'javelin-behavior-aphront-drag-and-drop-textarea' => '92eb531d',
+ 'javelin-behavior-aphront-drag-and-drop-textarea' => '6d49590e',
'javelin-behavior-aphront-form-disable-on-submit' => '5c54cbf3',
'javelin-behavior-aphront-more' => 'a80d0378',
'javelin-behavior-audio-source' => '59b251eb',
@@ -588,7 +588,7 @@
'javelin-behavior-durable-column' => '9142e483',
'javelin-behavior-error-log' => '6882e80a',
'javelin-behavior-fancy-datepicker' => 'c51ae228',
- 'javelin-behavior-global-drag-and-drop' => '07f199d8',
+ 'javelin-behavior-global-drag-and-drop' => '68a56fa5',
'javelin-behavior-herald-rule-editor' => '7ebaeed3',
'javelin-behavior-high-security-warning' => '8fc1c918',
'javelin-behavior-history-install' => '7ee2b591',
@@ -719,11 +719,11 @@
'phabricator-core-css' => '86bfbe8c',
'phabricator-countdown-css' => '86b7b0a0',
'phabricator-dashboard-css' => '17937d22',
- 'phabricator-drag-and-drop-file-upload' => '8c49f386',
+ 'phabricator-drag-and-drop-file-upload' => 'fd6ace61',
'phabricator-draggable-list' => 'a16ec1c6',
'phabricator-fatal-config-template-css' => '8e6c6fcd',
'phabricator-feed-css' => 'b513b5f4',
- 'phabricator-file-upload' => 'a4ae61bf',
+ 'phabricator-file-upload' => '477359c8',
'phabricator-filetree-view-css' => 'fccf9f82',
'phabricator-flag-css' => '5337623f',
'phabricator-hovercard' => '7e8468ae',
@@ -872,13 +872,6 @@
'javelin-stratcom',
'javelin-workflow',
),
- '07f199d8' => array(
- 'javelin-behavior',
- 'javelin-dom',
- 'javelin-uri',
- 'javelin-mask',
- 'phabricator-drag-and-drop-file-upload',
- ),
'08883e8b' => array(
'javelin-behavior',
'javelin-stratcom',
@@ -1129,6 +1122,11 @@
'javelin-dom',
'javelin-workflow',
),
+ '477359c8' => array(
+ 'javelin-install',
+ 'javelin-dom',
+ 'phabricator-notification',
+ ),
47830651 => array(
'javelin-behavior',
'javelin-dom',
@@ -1260,6 +1258,13 @@
'6882e80a' => array(
'javelin-dom',
),
+ '68a56fa5' => array(
+ 'javelin-behavior',
+ 'javelin-dom',
+ 'javelin-uri',
+ 'javelin-mask',
+ 'phabricator-drag-and-drop-file-upload',
+ ),
'69adf288' => array(
'javelin-install',
),
@@ -1280,6 +1285,12 @@
'javelin-typeahead',
'javelin-uri',
),
+ '6d49590e' => array(
+ 'javelin-behavior',
+ 'javelin-dom',
+ 'phabricator-drag-and-drop-file-upload',
+ 'phabricator-textareautils',
+ ),
'6e2de6f2' => array(
'multirow-row-manager',
'javelin-install',
@@ -1515,14 +1526,6 @@
'javelin-request',
'javelin-typeahead-source',
),
- '8c49f386' => array(
- 'javelin-install',
- 'javelin-util',
- 'javelin-request',
- 'javelin-dom',
- 'javelin-uri',
- 'phabricator-file-upload',
- ),
'8ce821c5' => array(
'phabricator-notification',
'javelin-stratcom',
@@ -1556,12 +1559,6 @@
'phabricator-keyboard-shortcut',
'conpherence-thread-manager',
),
- '92eb531d' => array(
- 'javelin-behavior',
- 'javelin-dom',
- 'phabricator-drag-and-drop-file-upload',
- 'phabricator-textareautils',
- ),
'9414ff18' => array(
'javelin-behavior',
'javelin-resource',
@@ -1638,11 +1635,6 @@
'javelin-vector',
'differential-inline-comment-editor',
),
- 'a4ae61bf' => array(
- 'javelin-install',
- 'javelin-dom',
- 'phabricator-notification',
- ),
'a80d0378' => array(
'javelin-behavior',
'javelin-stratcom',
@@ -2007,6 +1999,14 @@
'javelin-dom',
'phortune-credit-card-form',
),
+ 'fd6ace61' => array(
+ 'javelin-install',
+ 'javelin-util',
+ 'javelin-request',
+ 'javelin-dom',
+ 'javelin-uri',
+ 'phabricator-file-upload',
+ ),
'fe287620' => array(
'javelin-install',
'javelin-dom',
diff --git a/src/applications/files/conduit/FileAllocateConduitAPIMethod.php b/src/applications/files/conduit/FileAllocateConduitAPIMethod.php
--- a/src/applications/files/conduit/FileAllocateConduitAPIMethod.php
+++ b/src/applications/files/conduit/FileAllocateConduitAPIMethod.php
@@ -121,10 +121,20 @@
}
// None of the storage engines can accept this file.
+ if (PhabricatorFileStorageEngine::loadWritableEngines()) {
+ $error = pht(
+ 'Unable to upload file: this file is too large for any '.
+ 'configured storage engine.');
+ } else {
+ $error = pht(
+ 'Unable to upload file: the server is not configured with any '.
+ 'writable storage engines.');
+ }
return array(
'upload' => false,
'filePHID' => null,
+ 'error' => $error,
);
}
diff --git a/src/applications/files/controller/PhabricatorFileDropUploadController.php b/src/applications/files/controller/PhabricatorFileDropUploadController.php
--- a/src/applications/files/controller/PhabricatorFileDropUploadController.php
+++ b/src/applications/files/controller/PhabricatorFileDropUploadController.php
@@ -13,18 +13,82 @@
// NOTE: Throws if valid CSRF token is not present in the request.
$request->validateCSRF();
- $data = PhabricatorStartup::getRawInput();
$name = $request->getStr('name');
-
+ $file_phid = $request->getStr('phid');
// If there's no explicit view policy, make it very restrictive by default.
// This is the correct policy for files dropped onto objects during
// creation, comment and edit flows.
-
$view_policy = $request->getStr('viewPolicy');
if (!$view_policy) {
$view_policy = $viewer->getPHID();
}
+ $is_chunks = $request->getBool('querychunks');
+ if ($is_chunks) {
+ $params = array(
+ 'filePHID' => $file_phid,
+ );
+
+ $result = id(new ConduitCall('file.querychunks', $params))
+ ->setUser($viewer)
+ ->execute();
+
+ return id(new AphrontAjaxResponse())->setContent($result);
+ }
+
+ $is_allocate = $request->getBool('allocate');
+ if ($is_allocate) {
+ $params = array(
+ 'name' => $name,
+ 'contentLength' => $request->getInt('length'),
+ 'viewPolicy' => $view_policy,
+
+ // TODO: Remove.
+ // 'forceChunking' => true,
+ );
+
+ $result = id(new ConduitCall('file.allocate', $params))
+ ->setUser($viewer)
+ ->execute();
+
+ $file_phid = $result['filePHID'];
+ if ($file_phid) {
+ $file = $this->loadFile($file_phid);
+ $result += $this->getFileDictionary($file);
+ }
+
+ return id(new AphrontAjaxResponse())->setContent($result);
+ }
+
+ // Read the raw request data. We're either doing a chunk upload or a
+ // vanilla upload, so we need it.
+ $data = PhabricatorStartup::getRawInput();
+
+
+ $is_chunk_upload = $request->getBool('uploadchunk');
+ if ($is_chunk_upload) {
+ $params = array(
+ 'filePHID' => $file_phid,
+ 'byteStart' => $request->getInt('byteStart'),
+ 'data' => $data,
+ );
+
+ $result = id(new ConduitCall('file.uploadchunk', $params))
+ ->setUser($viewer)
+ ->execute();
+
+ $file = $this->loadFile($file_phid);
+ if ($file->getIsPartial()) {
+ $result = array();
+ } else {
+ $result = array(
+ 'complete' => true,
+ ) + $this->getFileDictionary($file);
+ }
+
+ return id(new AphrontAjaxResponse())->setContent($result);
+ }
+
$file = PhabricatorFile::newFromXHRUpload(
$data,
array(
@@ -34,12 +98,30 @@
'isExplicitUpload' => true,
));
- return id(new AphrontAjaxResponse())->setContent(
- array(
- 'id' => $file->getID(),
- 'phid' => $file->getPHID(),
- 'uri' => $file->getBestURI(),
- ));
+ $result = $this->getFileDictionary($file);
+ return id(new AphrontAjaxResponse())->setContent($result);
+ }
+
+ private function getFileDictionary(PhabricatorFile $file) {
+ return array(
+ 'id' => $file->getID(),
+ 'phid' => $file->getPHID(),
+ 'uri' => $file->getBestURI(),
+ );
+ }
+
+ private function loadFile($file_phid) {
+ $viewer = $this->getViewer();
+
+ $file = id(new PhabricatorFileQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($file_phid))
+ ->executeOne();
+ if (!$file) {
+ throw new Exception(pht('Failed to load file.'));
+ }
+
+ return $file;
}
}
diff --git a/src/applications/files/engine/PhabricatorChunkedFileStorageEngine.php b/src/applications/files/engine/PhabricatorChunkedFileStorageEngine.php
--- a/src/applications/files/engine/PhabricatorChunkedFileStorageEngine.php
+++ b/src/applications/files/engine/PhabricatorChunkedFileStorageEngine.php
@@ -162,7 +162,7 @@
return false;
}
- private function getChunkSize() {
+ public function getChunkSize() {
// TODO: This is an artificially small size to make it easier to
// test chunking.
return 32;
diff --git a/src/applications/files/engine/PhabricatorFileStorageEngine.php b/src/applications/files/engine/PhabricatorFileStorageEngine.php
--- a/src/applications/files/engine/PhabricatorFileStorageEngine.php
+++ b/src/applications/files/engine/PhabricatorFileStorageEngine.php
@@ -255,4 +255,40 @@
return $writable;
}
+
+ /**
+ * Return the largest file size which can be uploaded without chunking.
+ *
+ * Files smaller than this will always upload in one request, so clients
+ * can safely skip the allocation step.
+ *
+ * @return int|null Byte size, or `null` if there is no chunk support.
+ */
+ public static function getChunkThreshold() {
+ $engines = self::loadWritableEngines();
+
+ $min = null;
+ foreach ($engines as $engine) {
+ if (!$engine->isChunkEngine()) {
+ continue;
+ }
+
+ if (!$min) {
+ $min = $engine;
+ continue;
+ }
+
+ if ($min->getChunkSize() > $engine->getChunkSize()) {
+ $min = $engine->getChunkSize();
+ }
+ }
+
+ if (!$min) {
+ return null;
+ }
+
+ return $engine->getChunkSize();
+ }
+
+
}
diff --git a/src/applications/files/view/PhabricatorGlobalUploadTargetView.php b/src/applications/files/view/PhabricatorGlobalUploadTargetView.php
--- a/src/applications/files/view/PhabricatorGlobalUploadTargetView.php
+++ b/src/applications/files/view/PhabricatorGlobalUploadTargetView.php
@@ -24,11 +24,12 @@
require_celerity_resource('global-drag-and-drop-css');
Javelin::initBehavior('global-drag-and-drop', array(
- 'ifSupported' => $this->showIfSupportedID,
- 'instructions' => $instructions_id,
- 'uploadURI' => '/file/dropupload/',
- 'browseURI' => '/file/query/authored/',
- 'viewPolicy' => PhabricatorPolicies::getMostOpenPolicy(),
+ 'ifSupported' => $this->showIfSupportedID,
+ 'instructions' => $instructions_id,
+ 'uploadURI' => '/file/dropupload/',
+ 'browseURI' => '/file/query/authored/',
+ 'viewPolicy' => PhabricatorPolicies::getMostOpenPolicy(),
+ 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(),
));
return phutil_tag(
diff --git a/src/view/form/control/PhabricatorRemarkupControl.php b/src/view/form/control/PhabricatorRemarkupControl.php
--- a/src/view/form/control/PhabricatorRemarkupControl.php
+++ b/src/view/form/control/PhabricatorRemarkupControl.php
@@ -35,9 +35,10 @@
Javelin::initBehavior(
'aphront-drag-and-drop-textarea',
array(
- 'target' => $id,
- 'activatedClass' => 'aphront-textarea-drag-and-drop',
- 'uri' => '/file/dropupload/',
+ 'target' => $id,
+ 'activatedClass' => 'aphront-textarea-drag-and-drop',
+ 'uri' => '/file/dropupload/',
+ 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(),
));
Javelin::initBehavior(
diff --git a/webroot/rsrc/js/core/DragAndDropFileUpload.js b/webroot/rsrc/js/core/DragAndDropFileUpload.js
--- a/webroot/rsrc/js/core/DragAndDropFileUpload.js
+++ b/webroot/rsrc/js/core/DragAndDropFileUpload.js
@@ -169,64 +169,189 @@
}));
}
},
+
_sendRequest : function(spec) {
var file = new JX.PhabricatorFileUpload()
+ .setRawFileObject(spec)
.setName(spec.name)
- .setTotalBytes(spec.size)
- .setStatus('uploading')
+ .setTotalBytes(spec.size);
+
+ var threshold = this.getChunkThreshold();
+ if (threshold && (file.getTotalBytes() > threshold)) {
+ // This is a large file, so we'll go through allocation so we can
+ // pick up support for resume and chunking.
+ this._allocateFile(file);
+ } else {
+ // If this file is smaller than the chunk threshold, skip the round
+ // trip for allocation and just upload it directly.
+ this._sendDataRequest(file);
+ }
+ },
+
+ _allocateFile: function(file) {
+ file
+ .setStatus('allocate')
.update();
- this.invoke('willUpload', file);
+ var alloc_uri = this._getUploadURI(file)
+ .setQueryParam('allocate', 1);
+
+ new JX.Workflow(alloc_uri)
+ .setHandler(JX.bind(this, this._didAllocateFile, file))
+ .start();
+ },
- var up_uri = JX.$U(this.getURI())
+ _getUploadURI: function(file) {
+ var uri = JX.$U(this.getURI())
.setQueryParam('name', file.getName())
- .setQueryParam('__upload__', 1);
+ .setQueryParam('length', file.getTotalBytes());
if (this.getViewPolicy()) {
- up_uri.setQueryParam('viewPolicy', this.getViewPolicy());
+ uri.setQueryParam('viewPolicy', this.getViewPolicy());
}
- up_uri = up_uri.toString();
+ if (file.getAllocatedPHID()) {
+ uri.setQueryParam('phid', file.getAllocatedPHID());
+ }
- var onupload = JX.bind(this, function(r) {
- if (r.error) {
- file
- .setStatus('error')
- .setError(r.error)
- .update();
+ return uri;
+ },
+
+ _didAllocateFile: function(file, r) {
+ var phid = r.phid;
+ var upload = r.upload;
- this.invoke('didError', file);
+ if (!upload) {
+ if (phid) {
+ this._completeUpload(file, r);
+ } else {
+ this._failUpload(file, r);
+ }
+ return;
+ } else {
+ if (phid) {
+ // Start or resume a chunked upload.
+ file.setAllocatedPHID(phid);
+ this._loadChunks(file);
} else {
- file
- .setID(r.id)
- .setPHID(r.phid)
- .setURI(r.uri)
- .setMarkup(r.html)
- .setStatus('done')
- .update();
-
- this.invoke('didUpload', file);
+ // Proceed with non-chunked upload.
+ this._sendDataRequest(file);
+ }
+ }
+ },
+
+ _loadChunks: function(file) {
+ file
+ .setStatus('chunks')
+ .update();
+
+ var chunks_uri = this._getUploadURI(file)
+ .setQueryParam('querychunks', 1);
+
+ new JX.Workflow(chunks_uri)
+ .setHandler(JX.bind(this, this._didLoadChunks, file))
+ .start();
+ },
+
+ _didLoadChunks: function(file, r) {
+ file.setChunks(r);
+ this._uploadNextChunk(file);
+ },
+
+ _uploadNextChunk: function(file) {
+ var chunks = file.getChunks();
+ var chunk;
+ for (var ii = 0; ii < chunks.length; ii++) {
+ chunk = chunks[ii];
+ if (!chunk.complete) {
+ this._readChunk(
+ file,
+ chunk,
+ JX.bind(this, this._didReadChunk, file, chunk));
+ break;
}
+ }
+ },
+
+ _readChunk: function(file, chunk, callback) {
+ var reader = new FileReader();
+ var blob = file.getRawFileObject().slice(chunk.byteStart, chunk.byteEnd);
+
+ reader.onload = function() {
+ callback(reader.result);
+ };
+
+ reader.onerror = function() {
+ this._failUpload(file, {error: reader.error.message});
+ };
+
+ reader.readAsBinaryString(blob);
+ },
+
+ _didReadChunk: function(file, chunk, data) {
+ file
+ .setStatus('upload')
+ .update();
+
+ var chunkup_uri = this._getUploadURI(file)
+ .setQueryParam('uploadchunk', 1)
+ .setQueryParam('__upload__', 1)
+ .setQueryParam('byteStart', chunk.byteStart)
+ .toString();
+
+ var callback = JX.bind(this, this._didUploadChunk, file, chunk);
+
+ var req = new JX.Request(chunkup_uri, callback);
+
+ var seen_bytes = 0;
+ var onprogress = JX.bind(this, function(progress) {
+ file
+ .addUploadedBytes(progress.loaded - seen_bytes)
+ .update();
+
+ seen_bytes = progress.loaded;
+ this.invoke('progress', file);
});
- var req = new JX.Request(up_uri, onupload);
+ req.listen('error', JX.bind(this, this._onUploadError, req, file));
+ req.listen('uploadprogress', onprogress);
+
+ req
+ .setRawData(data)
+ .send();
+ },
- var onerror = JX.bind(this, function(error) {
- file.setStatus('error');
+ _didUploadChunk: function(file, chunk, r) {
+ file.didCompleteChunk(chunk);
- if (error) {
- file.setError(error.code + ': ' + error.info);
+ if (r.complete) {
+ this._completeUpload(file, r);
+ } else {
+ this._uploadNextChunk(file);
+ }
+ },
+
+ _sendDataRequest: function(file) {
+ file
+ .setStatus('uploading')
+ .update();
+
+ this.invoke('willUpload', file);
+
+ var up_uri = this._getUploadURI(file)
+ .setQueryParam('__upload__', 1)
+ .toString();
+
+ var onupload = JX.bind(this, function(r) {
+ if (r.error) {
+ this._failUpload(file, r);
} else {
- var xhr = req.getTransport();
- if (xhr.responseText) {
- file.setError('Server responded: ' + xhr.responseText);
- }
+ this._completeUpload(file, r);
}
-
- file.update();
- this.invoke('didError', file);
});
+ var req = new JX.Request(up_uri, onupload);
+
var onprogress = JX.bind(this, function(progress) {
file
.setTotalBytes(progress.total)
@@ -236,17 +361,56 @@
this.invoke('progress', file);
});
- req.listen('error', onerror);
+ req.listen('error', JX.bind(this, this._onUploadError, req, file));
req.listen('uploadprogress', onprogress);
req
- .setRawData(spec)
+ .setRawData(file.getRawFileObject())
.send();
+ },
+
+ _completeUpload: function(file, r) {
+ file
+ .setID(r.id)
+ .setPHID(r.phid)
+ .setURI(r.uri)
+ .setMarkup(r.html)
+ .setStatus('done')
+ .update();
+
+ this.invoke('didUpload', file);
+ },
+
+ _failUpload: function(file, r) {
+ file
+ .setStatus('error')
+ .setError(r.error)
+ .update();
+
+ this.invoke('didError', file);
+ },
+
+ _onUploadError: function(file, req, error) {
+ file.setStatus('error');
+
+ if (error) {
+ file.setError(error.code + ': ' + error.info);
+ } else {
+ var xhr = req.getTransport();
+ if (xhr.responseText) {
+ file.setError('Server responded: ' + xhr.responseText);
+ }
+ }
+
+ file.update();
+ this.invoke('didError', file);
}
+
},
properties: {
- URI : null,
- activatedClass : null,
- viewPolicy : null
+ URI: null,
+ activatedClass: null,
+ viewPolicy: null,
+ chunkThreshold: null
}
});
diff --git a/webroot/rsrc/js/core/FileUpload.js b/webroot/rsrc/js/core/FileUpload.js
--- a/webroot/rsrc/js/core/FileUpload.js
+++ b/webroot/rsrc/js/core/FileUpload.js
@@ -13,19 +13,77 @@
},
properties : {
- name : null,
- totalBytes : null,
- uploadedBytes : null,
- ID : null,
- PHID : null,
- URI : null,
- status : null,
- markup : null,
- error : null
+ name: null,
+ totalBytes: null,
+ uploadedBytes: null,
+ rawFileObject: null,
+ allocatedPHID: null,
+ ID: null,
+ PHID: null,
+ URI: null,
+ status: null,
+ markup: null,
+ error: null
},
members : {
_notification : null,
+ _chunks: null,
+ _isResume: false,
+
+ addUploadedBytes: function(bytes) {
+ var uploaded = this.getUploadedBytes();
+ this.setUploadedBytes(uploaded + bytes);
+ return this;
+ },
+
+ setChunks: function(chunks) {
+ var chunk;
+ for (var ii = 0; ii < chunks.length; ii++) {
+ chunk = chunks[ii];
+ if (chunk.complete) {
+ this.addUploadedBytes(chunk.byteEnd - chunk.byteStart);
+ this._isResume = true;
+ }
+ }
+
+ this._chunks = chunks;
+
+ return this;
+ },
+
+ getChunks: function() {
+ return this._chunks;
+ },
+
+ getRemainingChunks: function() {
+ var chunks = this.getChunks();
+
+ var result = [];
+ for (var ii = 0; ii < chunks.length; ii++) {
+ if (!chunks[ii].complete) {
+ result.push(chunks[ii]);
+ }
+ }
+
+ return result;
+ },
+
+ didCompleteChunk: function(chunk) {
+ var chunks = this.getRemainingChunks();
+ for (var ii = 0; ii < chunks.length; ii++) {
+ if (chunks[ii].byteStart == chunk.byteStart) {
+ if (chunks[ii].byteEnd == chunk.byteEnd) {
+ if (!chunks[ii].complete) {
+ chunks[ii].complete = true;
+ }
+ break;
+ }
+ }
+ }
+
+ return this;
+ },
update : function() {
if (!this._notification) {
@@ -37,6 +95,9 @@
.show();
var content;
+
+ // TODO: This stuff needs some work for translations.
+
switch (this.getStatus()) {
case 'done':
var link = JX.$N('a', {href: this.getURI()}, 'F' + this.getID());
@@ -68,15 +129,37 @@
.alterClassName('jx-notification-error', true);
this._notification = null;
break;
+ case 'allocate':
+ content = 'Allocating "' + this.getName() + '"...';
+ this._notification
+ .setContent(content);
+ break;
+ case 'chunks':
+ content = 'Loading chunks for "' + this.getName() + '"...';
+ this._notification
+ .setContent(content);
+ break;
default:
var info = '';
if (this.getTotalBytes()) {
var p = this._renderPercentComplete();
var f = this._renderFileSize();
- info = ' (' + p + ' of ' + f + ')';
+ info = p + ' of ' + f;
}
- info = 'Uploading "' + this.getName() + '"' + info + '...';
+ var head;
+ if (this._isResume) {
+ head = 'Resuming:';
+ } else if (this._chunks) {
+ head = 'Uploading chunks:';
+ } else {
+ head = 'Uploading:';
+ }
+
+ info = [
+ JX.$N('strong', {}, this.getName()),
+ JX.$N('br'),
+ head + ' ' + info];
this._notification
.setContent(info);
diff --git a/webroot/rsrc/js/core/behavior-drag-and-drop-textarea.js b/webroot/rsrc/js/core/behavior-drag-and-drop-textarea.js
--- a/webroot/rsrc/js/core/behavior-drag-and-drop-textarea.js
+++ b/webroot/rsrc/js/core/behavior-drag-and-drop-textarea.js
@@ -27,7 +27,8 @@
if (JX.PhabricatorDragAndDropFileUpload.isSupported()) {
var drop = new JX.PhabricatorDragAndDropFileUpload(target)
- .setURI(config.uri);
+ .setURI(config.uri)
+ .setChunkThreshold(config.chunkThreshold);
drop.listen('didBeginDrag', function() {
JX.DOM.alterClass(target, config.activatedClass, true);
});
diff --git a/webroot/rsrc/js/core/behavior-global-drag-and-drop.js b/webroot/rsrc/js/core/behavior-global-drag-and-drop.js
--- a/webroot/rsrc/js/core/behavior-global-drag-and-drop.js
+++ b/webroot/rsrc/js/core/behavior-global-drag-and-drop.js
@@ -22,7 +22,8 @@
var drop = new JX.PhabricatorDragAndDropFileUpload(document.documentElement)
.setURI(config.uploadURI)
- .setViewPolicy(config.viewPolicy);
+ .setViewPolicy(config.viewPolicy)
+ .setChunkThreshold(config.chunkThreshold);
drop.listen('didBeginDrag', function() {
JX.Mask.show();

File Metadata

Mime Type
text/plain
Expires
Sun, Nov 3, 12:27 PM (2 w, 1 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6732215
Default Alt Text
D12066.id29034.diff (26 KB)

Event Timeline