Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14013770
D12066.id29034.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
26 KB
Referenced Files
None
Subscribers
None
D12066.id29034.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D12066: Support HTML5 / Javascript chunked file uploads
Attached
Detach File
Event Timeline
Log In to Comment