@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Support HTML5 / Javascript chunked file uploads

Summary:
Ref T7149. This adds chunking support to drag-and-drop uploads. It never activates right now unless you hack things up, since the chunk engine is still hard-coded as disabled.

The overall approach is the same as `arc upload` in D12061, with some slight changes to the API return values to avoid a few extra HTTP calls.

Test Plan:
- Enabled chunk engine.
- Uploaded some READMEs in a bunch of tiny 32 byte chunks.
- Worked out of the box in Safari, Chrome, Firefox.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T7149

Differential Revision: https://secure.phabricator.com/D12066

+485 -106
+36 -36
resources/celerity/map.php
··· 8 8 return array( 9 9 'names' => array( 10 10 'core.pkg.css' => 'efdeeb14', 11 - 'core.pkg.js' => 'deae6907', 11 + 'core.pkg.js' => '31bc6546', 12 12 'darkconsole.pkg.js' => '8ab24e01', 13 13 'differential.pkg.css' => '1940be3f', 14 - 'differential.pkg.js' => '53c1ccc2', 14 + 'differential.pkg.js' => 'be1e5f9b', 15 15 'diffusion.pkg.css' => '591664fa', 16 16 'diffusion.pkg.js' => 'bfc0737b', 17 17 'maniphest.pkg.css' => '68d4dd3d', ··· 438 438 'rsrc/js/application/uiexample/gesture-example.js' => '558829c2', 439 439 'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5', 440 440 'rsrc/js/core/Busy.js' => '6453c869', 441 - 'rsrc/js/core/DragAndDropFileUpload.js' => '8c49f386', 441 + 'rsrc/js/core/DragAndDropFileUpload.js' => 'fd6ace61', 442 442 'rsrc/js/core/DraggableList.js' => 'a16ec1c6', 443 - 'rsrc/js/core/FileUpload.js' => 'a4ae61bf', 443 + 'rsrc/js/core/FileUpload.js' => '477359c8', 444 444 'rsrc/js/core/Hovercard.js' => '7e8468ae', 445 445 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', 446 446 'rsrc/js/core/KeyboardShortcutManager.js' => 'c1700f6f', ··· 458 458 'rsrc/js/core/behavior-crop.js' => 'fa0f4fc2', 459 459 'rsrc/js/core/behavior-dark-console.js' => '08883e8b', 460 460 'rsrc/js/core/behavior-device.js' => '03d6ed07', 461 - 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '92eb531d', 461 + 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '6d49590e', 462 462 'rsrc/js/core/behavior-error-log.js' => '6882e80a', 463 463 'rsrc/js/core/behavior-fancy-datepicker.js' => 'c51ae228', 464 464 'rsrc/js/core/behavior-file-tree.js' => '88236f00', 465 465 'rsrc/js/core/behavior-form.js' => '5c54cbf3', 466 466 'rsrc/js/core/behavior-gesture.js' => '3ab51e2c', 467 - 'rsrc/js/core/behavior-global-drag-and-drop.js' => '8c584f17', 467 + 'rsrc/js/core/behavior-global-drag-and-drop.js' => 'bbdf75ca', 468 468 'rsrc/js/core/behavior-high-security-warning.js' => '8fc1c918', 469 469 'rsrc/js/core/behavior-history-install.js' => '7ee2b591', 470 470 'rsrc/js/core/behavior-hovercard.js' => 'f36e01af', ··· 549 549 'javelin-behavior-aphlict-status' => 'ea681761', 550 550 'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884', 551 551 'javelin-behavior-aphront-crop' => 'fa0f4fc2', 552 - 'javelin-behavior-aphront-drag-and-drop-textarea' => '92eb531d', 552 + 'javelin-behavior-aphront-drag-and-drop-textarea' => '6d49590e', 553 553 'javelin-behavior-aphront-form-disable-on-submit' => '5c54cbf3', 554 554 'javelin-behavior-aphront-more' => 'a80d0378', 555 555 'javelin-behavior-audio-source' => '59b251eb', ··· 588 588 'javelin-behavior-durable-column' => 'a3ba7034', 589 589 'javelin-behavior-error-log' => '6882e80a', 590 590 'javelin-behavior-fancy-datepicker' => 'c51ae228', 591 - 'javelin-behavior-global-drag-and-drop' => '8c584f17', 591 + 'javelin-behavior-global-drag-and-drop' => 'bbdf75ca', 592 592 'javelin-behavior-herald-rule-editor' => '7ebaeed3', 593 593 'javelin-behavior-high-security-warning' => '8fc1c918', 594 594 'javelin-behavior-history-install' => '7ee2b591', ··· 719 719 'phabricator-core-css' => '86bfbe8c', 720 720 'phabricator-countdown-css' => '86b7b0a0', 721 721 'phabricator-dashboard-css' => '17937d22', 722 - 'phabricator-drag-and-drop-file-upload' => '8c49f386', 722 + 'phabricator-drag-and-drop-file-upload' => 'fd6ace61', 723 723 'phabricator-draggable-list' => 'a16ec1c6', 724 724 'phabricator-fatal-config-template-css' => '8e6c6fcd', 725 725 'phabricator-feed-css' => 'b513b5f4', 726 - 'phabricator-file-upload' => 'a4ae61bf', 726 + 'phabricator-file-upload' => '477359c8', 727 727 'phabricator-filetree-view-css' => 'fccf9f82', 728 728 'phabricator-flag-css' => '5337623f', 729 729 'phabricator-hovercard' => '7e8468ae', ··· 1122 1122 'javelin-dom', 1123 1123 'javelin-workflow', 1124 1124 ), 1125 + '477359c8' => array( 1126 + 'javelin-install', 1127 + 'javelin-dom', 1128 + 'phabricator-notification', 1129 + ), 1125 1130 47830651 => array( 1126 1131 'javelin-behavior', 1127 1132 'javelin-dom', ··· 1272 1277 'javelin-dom', 1273 1278 'javelin-typeahead', 1274 1279 'javelin-uri', 1280 + ), 1281 + '6d49590e' => array( 1282 + 'javelin-behavior', 1283 + 'javelin-dom', 1284 + 'phabricator-drag-and-drop-file-upload', 1285 + 'phabricator-textareautils', 1275 1286 ), 1276 1287 '6e2de6f2' => array( 1277 1288 'multirow-row-manager', ··· 1508 1519 'javelin-request', 1509 1520 'javelin-typeahead-source', 1510 1521 ), 1511 - '8c49f386' => array( 1512 - 'javelin-install', 1513 - 'javelin-util', 1514 - 'javelin-request', 1515 - 'javelin-dom', 1516 - 'javelin-uri', 1517 - 'phabricator-file-upload', 1518 - ), 1519 - '8c584f17' => array( 1520 - 'javelin-behavior', 1521 - 'javelin-dom', 1522 - 'javelin-uri', 1523 - 'javelin-mask', 1524 - 'phabricator-drag-and-drop-file-upload', 1525 - ), 1526 1522 '8ce821c5' => array( 1527 1523 'phabricator-notification', 1528 1524 'javelin-stratcom', ··· 1546 1542 'javelin-uri', 1547 1543 'phabricator-notification', 1548 1544 ), 1549 - '92eb531d' => array( 1550 - 'javelin-behavior', 1551 - 'javelin-dom', 1552 - 'phabricator-drag-and-drop-file-upload', 1553 - 'phabricator-textareautils', 1554 - ), 1555 1545 '9414ff18' => array( 1556 1546 'javelin-behavior', 1557 1547 'javelin-resource', ··· 1638 1628 'javelin-vector', 1639 1629 'differential-inline-comment-editor', 1640 1630 ), 1641 - 'a4ae61bf' => array( 1642 - 'javelin-install', 1643 - 'javelin-dom', 1644 - 'phabricator-notification', 1645 - ), 1646 1631 'a80d0378' => array( 1647 1632 'javelin-behavior', 1648 1633 'javelin-stratcom', ··· 1717 1702 'javelin-behavior', 1718 1703 'javelin-stratcom', 1719 1704 'javelin-dom', 1705 + ), 1706 + 'bbdf75ca' => array( 1707 + 'javelin-behavior', 1708 + 'javelin-dom', 1709 + 'javelin-uri', 1710 + 'javelin-mask', 1711 + 'phabricator-drag-and-drop-file-upload', 1720 1712 ), 1721 1713 'bd4c8dca' => array( 1722 1714 'javelin-install', ··· 2006 1998 'javelin-behavior', 2007 1999 'javelin-dom', 2008 2000 'phortune-credit-card-form', 2001 + ), 2002 + 'fd6ace61' => array( 2003 + 'javelin-install', 2004 + 'javelin-util', 2005 + 'javelin-request', 2006 + 'javelin-dom', 2007 + 'javelin-uri', 2008 + 'phabricator-file-upload', 2009 2009 ), 2010 2010 'fe287620' => array( 2011 2011 'javelin-install',
+10
src/applications/files/conduit/FileAllocateConduitAPIMethod.php
··· 121 121 } 122 122 123 123 // None of the storage engines can accept this file. 124 + if (PhabricatorFileStorageEngine::loadWritableEngines()) { 125 + $error = pht( 126 + 'Unable to upload file: this file is too large for any '. 127 + 'configured storage engine.'); 128 + } else { 129 + $error = pht( 130 + 'Unable to upload file: the server is not configured with any '. 131 + 'writable storage engines.'); 132 + } 124 133 125 134 return array( 126 135 'upload' => false, 127 136 'filePHID' => null, 137 + 'error' => $error, 128 138 ); 129 139 } 130 140
+91 -9
src/applications/files/controller/PhabricatorFileDropUploadController.php
··· 13 13 // NOTE: Throws if valid CSRF token is not present in the request. 14 14 $request->validateCSRF(); 15 15 16 - $data = PhabricatorStartup::getRawInput(); 17 16 $name = $request->getStr('name'); 18 - 17 + $file_phid = $request->getStr('phid'); 19 18 // If there's no explicit view policy, make it very restrictive by default. 20 19 // This is the correct policy for files dropped onto objects during 21 20 // creation, comment and edit flows. 22 - 23 21 $view_policy = $request->getStr('viewPolicy'); 24 22 if (!$view_policy) { 25 23 $view_policy = $viewer->getPHID(); 26 24 } 27 25 26 + $is_chunks = $request->getBool('querychunks'); 27 + if ($is_chunks) { 28 + $params = array( 29 + 'filePHID' => $file_phid, 30 + ); 31 + 32 + $result = id(new ConduitCall('file.querychunks', $params)) 33 + ->setUser($viewer) 34 + ->execute(); 35 + 36 + return id(new AphrontAjaxResponse())->setContent($result); 37 + } 38 + 39 + $is_allocate = $request->getBool('allocate'); 40 + if ($is_allocate) { 41 + $params = array( 42 + 'name' => $name, 43 + 'contentLength' => $request->getInt('length'), 44 + 'viewPolicy' => $view_policy, 45 + 46 + // TODO: Remove. 47 + // 'forceChunking' => true, 48 + ); 49 + 50 + $result = id(new ConduitCall('file.allocate', $params)) 51 + ->setUser($viewer) 52 + ->execute(); 53 + 54 + $file_phid = $result['filePHID']; 55 + if ($file_phid) { 56 + $file = $this->loadFile($file_phid); 57 + $result += $this->getFileDictionary($file); 58 + } 59 + 60 + return id(new AphrontAjaxResponse())->setContent($result); 61 + } 62 + 63 + // Read the raw request data. We're either doing a chunk upload or a 64 + // vanilla upload, so we need it. 65 + $data = PhabricatorStartup::getRawInput(); 66 + 67 + 68 + $is_chunk_upload = $request->getBool('uploadchunk'); 69 + if ($is_chunk_upload) { 70 + $params = array( 71 + 'filePHID' => $file_phid, 72 + 'byteStart' => $request->getInt('byteStart'), 73 + 'data' => $data, 74 + ); 75 + 76 + $result = id(new ConduitCall('file.uploadchunk', $params)) 77 + ->setUser($viewer) 78 + ->execute(); 79 + 80 + $file = $this->loadFile($file_phid); 81 + if ($file->getIsPartial()) { 82 + $result = array(); 83 + } else { 84 + $result = array( 85 + 'complete' => true, 86 + ) + $this->getFileDictionary($file); 87 + } 88 + 89 + return id(new AphrontAjaxResponse())->setContent($result); 90 + } 91 + 28 92 $file = PhabricatorFile::newFromXHRUpload( 29 93 $data, 30 94 array( ··· 34 98 'isExplicitUpload' => true, 35 99 )); 36 100 37 - return id(new AphrontAjaxResponse())->setContent( 38 - array( 39 - 'id' => $file->getID(), 40 - 'phid' => $file->getPHID(), 41 - 'uri' => $file->getBestURI(), 42 - )); 101 + $result = $this->getFileDictionary($file); 102 + return id(new AphrontAjaxResponse())->setContent($result); 103 + } 104 + 105 + private function getFileDictionary(PhabricatorFile $file) { 106 + return array( 107 + 'id' => $file->getID(), 108 + 'phid' => $file->getPHID(), 109 + 'uri' => $file->getBestURI(), 110 + ); 111 + } 112 + 113 + private function loadFile($file_phid) { 114 + $viewer = $this->getViewer(); 115 + 116 + $file = id(new PhabricatorFileQuery()) 117 + ->setViewer($viewer) 118 + ->withPHIDs(array($file_phid)) 119 + ->executeOne(); 120 + if (!$file) { 121 + throw new Exception(pht('Failed to load file.')); 122 + } 123 + 124 + return $file; 43 125 } 44 126 45 127 }
+1 -1
src/applications/files/engine/PhabricatorChunkedFileStorageEngine.php
··· 162 162 return false; 163 163 } 164 164 165 - private function getChunkSize() { 165 + public function getChunkSize() { 166 166 // TODO: This is an artificially small size to make it easier to 167 167 // test chunking. 168 168 return 32;
+36
src/applications/files/engine/PhabricatorFileStorageEngine.php
··· 255 255 return $writable; 256 256 } 257 257 258 + 259 + /** 260 + * Return the largest file size which can be uploaded without chunking. 261 + * 262 + * Files smaller than this will always upload in one request, so clients 263 + * can safely skip the allocation step. 264 + * 265 + * @return int|null Byte size, or `null` if there is no chunk support. 266 + */ 267 + public static function getChunkThreshold() { 268 + $engines = self::loadWritableEngines(); 269 + 270 + $min = null; 271 + foreach ($engines as $engine) { 272 + if (!$engine->isChunkEngine()) { 273 + continue; 274 + } 275 + 276 + if (!$min) { 277 + $min = $engine; 278 + continue; 279 + } 280 + 281 + if ($min->getChunkSize() > $engine->getChunkSize()) { 282 + $min = $engine->getChunkSize(); 283 + } 284 + } 285 + 286 + if (!$min) { 287 + return null; 288 + } 289 + 290 + return $engine->getChunkSize(); 291 + } 292 + 293 + 258 294 }
+6 -5
src/applications/files/view/PhabricatorGlobalUploadTargetView.php
··· 24 24 require_celerity_resource('global-drag-and-drop-css'); 25 25 26 26 Javelin::initBehavior('global-drag-and-drop', array( 27 - 'ifSupported' => $this->showIfSupportedID, 28 - 'instructions' => $instructions_id, 29 - 'uploadURI' => '/file/dropupload/', 30 - 'browseURI' => '/file/query/authored/', 31 - 'viewPolicy' => PhabricatorPolicies::getMostOpenPolicy(), 27 + 'ifSupported' => $this->showIfSupportedID, 28 + 'instructions' => $instructions_id, 29 + 'uploadURI' => '/file/dropupload/', 30 + 'browseURI' => '/file/query/authored/', 31 + 'viewPolicy' => PhabricatorPolicies::getMostOpenPolicy(), 32 + 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(), 32 33 )); 33 34 34 35 return phutil_tag(
+4 -3
src/view/form/control/PhabricatorRemarkupControl.php
··· 35 35 Javelin::initBehavior( 36 36 'aphront-drag-and-drop-textarea', 37 37 array( 38 - 'target' => $id, 39 - 'activatedClass' => 'aphront-textarea-drag-and-drop', 40 - 'uri' => '/file/dropupload/', 38 + 'target' => $id, 39 + 'activatedClass' => 'aphront-textarea-drag-and-drop', 40 + 'uri' => '/file/dropupload/', 41 + 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(), 41 42 )); 42 43 43 44 Javelin::initBehavior(
+203 -39
webroot/rsrc/js/core/DragAndDropFileUpload.js
··· 169 169 })); 170 170 } 171 171 }, 172 + 172 173 _sendRequest : function(spec) { 173 174 var file = new JX.PhabricatorFileUpload() 175 + .setRawFileObject(spec) 174 176 .setName(spec.name) 175 - .setTotalBytes(spec.size) 176 - .setStatus('uploading') 177 + .setTotalBytes(spec.size); 178 + 179 + var threshold = this.getChunkThreshold(); 180 + if (threshold && (file.getTotalBytes() > threshold)) { 181 + // This is a large file, so we'll go through allocation so we can 182 + // pick up support for resume and chunking. 183 + this._allocateFile(file); 184 + } else { 185 + // If this file is smaller than the chunk threshold, skip the round 186 + // trip for allocation and just upload it directly. 187 + this._sendDataRequest(file); 188 + } 189 + }, 190 + 191 + _allocateFile: function(file) { 192 + file 193 + .setStatus('allocate') 177 194 .update(); 178 195 179 - this.invoke('willUpload', file); 196 + var alloc_uri = this._getUploadURI(file) 197 + .setQueryParam('allocate', 1); 198 + 199 + new JX.Workflow(alloc_uri) 200 + .setHandler(JX.bind(this, this._didAllocateFile, file)) 201 + .start(); 202 + }, 180 203 181 - var up_uri = JX.$U(this.getURI()) 204 + _getUploadURI: function(file) { 205 + var uri = JX.$U(this.getURI()) 182 206 .setQueryParam('name', file.getName()) 183 - .setQueryParam('__upload__', 1); 207 + .setQueryParam('length', file.getTotalBytes()); 184 208 185 209 if (this.getViewPolicy()) { 186 - up_uri.setQueryParam('viewPolicy', this.getViewPolicy()); 210 + uri.setQueryParam('viewPolicy', this.getViewPolicy()); 187 211 } 188 212 189 - up_uri = up_uri.toString(); 213 + if (file.getAllocatedPHID()) { 214 + uri.setQueryParam('phid', file.getAllocatedPHID()); 215 + } 190 216 191 - var onupload = JX.bind(this, function(r) { 192 - if (r.error) { 193 - file 194 - .setStatus('error') 195 - .setError(r.error) 196 - .update(); 217 + return uri; 218 + }, 197 219 198 - this.invoke('didError', file); 220 + _didAllocateFile: function(file, r) { 221 + var phid = r.phid; 222 + var upload = r.upload; 223 + 224 + if (!upload) { 225 + if (phid) { 226 + this._completeUpload(file, r); 227 + } else { 228 + this._failUpload(file, r); 229 + } 230 + return; 231 + } else { 232 + if (phid) { 233 + // Start or resume a chunked upload. 234 + file.setAllocatedPHID(phid); 235 + this._loadChunks(file); 199 236 } else { 200 - file 201 - .setID(r.id) 202 - .setPHID(r.phid) 203 - .setURI(r.uri) 204 - .setMarkup(r.html) 205 - .setStatus('done') 206 - .update(); 237 + // Proceed with non-chunked upload. 238 + this._sendDataRequest(file); 239 + } 240 + } 241 + }, 207 242 208 - this.invoke('didUpload', file); 243 + _loadChunks: function(file) { 244 + file 245 + .setStatus('chunks') 246 + .update(); 247 + 248 + var chunks_uri = this._getUploadURI(file) 249 + .setQueryParam('querychunks', 1); 250 + 251 + new JX.Workflow(chunks_uri) 252 + .setHandler(JX.bind(this, this._didLoadChunks, file)) 253 + .start(); 254 + }, 255 + 256 + _didLoadChunks: function(file, r) { 257 + file.setChunks(r); 258 + this._uploadNextChunk(file); 259 + }, 260 + 261 + _uploadNextChunk: function(file) { 262 + var chunks = file.getChunks(); 263 + var chunk; 264 + for (var ii = 0; ii < chunks.length; ii++) { 265 + chunk = chunks[ii]; 266 + if (!chunk.complete) { 267 + this._readChunk( 268 + file, 269 + chunk, 270 + JX.bind(this, this._didReadChunk, file, chunk)); 271 + break; 209 272 } 273 + } 274 + }, 275 + 276 + _readChunk: function(file, chunk, callback) { 277 + var reader = new FileReader(); 278 + var blob = file.getRawFileObject().slice(chunk.byteStart, chunk.byteEnd); 279 + 280 + reader.onload = function() { 281 + callback(reader.result); 282 + }; 283 + 284 + reader.onerror = function() { 285 + this._failUpload(file, {error: reader.error.message}); 286 + }; 287 + 288 + reader.readAsBinaryString(blob); 289 + }, 290 + 291 + _didReadChunk: function(file, chunk, data) { 292 + file 293 + .setStatus('upload') 294 + .update(); 295 + 296 + var chunkup_uri = this._getUploadURI(file) 297 + .setQueryParam('uploadchunk', 1) 298 + .setQueryParam('__upload__', 1) 299 + .setQueryParam('byteStart', chunk.byteStart) 300 + .toString(); 301 + 302 + var callback = JX.bind(this, this._didUploadChunk, file, chunk); 303 + 304 + var req = new JX.Request(chunkup_uri, callback); 305 + 306 + var seen_bytes = 0; 307 + var onprogress = JX.bind(this, function(progress) { 308 + file 309 + .addUploadedBytes(progress.loaded - seen_bytes) 310 + .update(); 311 + 312 + seen_bytes = progress.loaded; 313 + this.invoke('progress', file); 210 314 }); 211 315 212 - var req = new JX.Request(up_uri, onupload); 316 + req.listen('error', JX.bind(this, this._onUploadError, req, file)); 317 + req.listen('uploadprogress', onprogress); 318 + 319 + req 320 + .setRawData(data) 321 + .send(); 322 + }, 213 323 214 - var onerror = JX.bind(this, function(error) { 215 - file.setStatus('error'); 324 + _didUploadChunk: function(file, chunk, r) { 325 + file.didCompleteChunk(chunk); 216 326 217 - if (error) { 218 - file.setError(error.code + ': ' + error.info); 327 + if (r.complete) { 328 + this._completeUpload(file, r); 329 + } else { 330 + this._uploadNextChunk(file); 331 + } 332 + }, 333 + 334 + _sendDataRequest: function(file) { 335 + file 336 + .setStatus('uploading') 337 + .update(); 338 + 339 + this.invoke('willUpload', file); 340 + 341 + var up_uri = this._getUploadURI(file) 342 + .setQueryParam('__upload__', 1) 343 + .toString(); 344 + 345 + var onupload = JX.bind(this, function(r) { 346 + if (r.error) { 347 + this._failUpload(file, r); 219 348 } else { 220 - var xhr = req.getTransport(); 221 - if (xhr.responseText) { 222 - file.setError('Server responded: ' + xhr.responseText); 223 - } 349 + this._completeUpload(file, r); 224 350 } 351 + }); 225 352 226 - file.update(); 227 - this.invoke('didError', file); 228 - }); 353 + var req = new JX.Request(up_uri, onupload); 229 354 230 355 var onprogress = JX.bind(this, function(progress) { 231 356 file ··· 236 361 this.invoke('progress', file); 237 362 }); 238 363 239 - req.listen('error', onerror); 364 + req.listen('error', JX.bind(this, this._onUploadError, req, file)); 240 365 req.listen('uploadprogress', onprogress); 241 366 242 367 req 243 - .setRawData(spec) 368 + .setRawData(file.getRawFileObject()) 244 369 .send(); 370 + }, 371 + 372 + _completeUpload: function(file, r) { 373 + file 374 + .setID(r.id) 375 + .setPHID(r.phid) 376 + .setURI(r.uri) 377 + .setMarkup(r.html) 378 + .setStatus('done') 379 + .update(); 380 + 381 + this.invoke('didUpload', file); 382 + }, 383 + 384 + _failUpload: function(file, r) { 385 + file 386 + .setStatus('error') 387 + .setError(r.error) 388 + .update(); 389 + 390 + this.invoke('didError', file); 391 + }, 392 + 393 + _onUploadError: function(file, req, error) { 394 + file.setStatus('error'); 395 + 396 + if (error) { 397 + file.setError(error.code + ': ' + error.info); 398 + } else { 399 + var xhr = req.getTransport(); 400 + if (xhr.responseText) { 401 + file.setError('Server responded: ' + xhr.responseText); 402 + } 403 + } 404 + 405 + file.update(); 406 + this.invoke('didError', file); 245 407 } 408 + 246 409 }, 247 410 properties: { 248 - URI : null, 249 - activatedClass : null, 250 - viewPolicy : null 411 + URI: null, 412 + activatedClass: null, 413 + viewPolicy: null, 414 + chunkThreshold: null 251 415 } 252 416 });
+94 -11
webroot/rsrc/js/core/FileUpload.js
··· 13 13 }, 14 14 15 15 properties : { 16 - name : null, 17 - totalBytes : null, 18 - uploadedBytes : null, 19 - ID : null, 20 - PHID : null, 21 - URI : null, 22 - status : null, 23 - markup : null, 24 - error : null 16 + name: null, 17 + totalBytes: null, 18 + uploadedBytes: null, 19 + rawFileObject: null, 20 + allocatedPHID: null, 21 + ID: null, 22 + PHID: null, 23 + URI: null, 24 + status: null, 25 + markup: null, 26 + error: null 25 27 }, 26 28 27 29 members : { 28 30 _notification : null, 31 + _chunks: null, 32 + _isResume: false, 33 + 34 + addUploadedBytes: function(bytes) { 35 + var uploaded = this.getUploadedBytes(); 36 + this.setUploadedBytes(uploaded + bytes); 37 + return this; 38 + }, 39 + 40 + setChunks: function(chunks) { 41 + var chunk; 42 + for (var ii = 0; ii < chunks.length; ii++) { 43 + chunk = chunks[ii]; 44 + if (chunk.complete) { 45 + this.addUploadedBytes(chunk.byteEnd - chunk.byteStart); 46 + this._isResume = true; 47 + } 48 + } 49 + 50 + this._chunks = chunks; 51 + 52 + return this; 53 + }, 54 + 55 + getChunks: function() { 56 + return this._chunks; 57 + }, 58 + 59 + getRemainingChunks: function() { 60 + var chunks = this.getChunks(); 61 + 62 + var result = []; 63 + for (var ii = 0; ii < chunks.length; ii++) { 64 + if (!chunks[ii].complete) { 65 + result.push(chunks[ii]); 66 + } 67 + } 68 + 69 + return result; 70 + }, 71 + 72 + didCompleteChunk: function(chunk) { 73 + var chunks = this.getRemainingChunks(); 74 + for (var ii = 0; ii < chunks.length; ii++) { 75 + if (chunks[ii].byteStart == chunk.byteStart) { 76 + if (chunks[ii].byteEnd == chunk.byteEnd) { 77 + if (!chunks[ii].complete) { 78 + chunks[ii].complete = true; 79 + } 80 + break; 81 + } 82 + } 83 + } 84 + 85 + return this; 86 + }, 29 87 30 88 update : function() { 31 89 if (!this._notification) { ··· 37 95 .show(); 38 96 39 97 var content; 98 + 99 + // TODO: This stuff needs some work for translations. 100 + 40 101 switch (this.getStatus()) { 41 102 case 'done': 42 103 var link = JX.$N('a', {href: this.getURI()}, 'F' + this.getID()); ··· 68 129 .alterClassName('jx-notification-error', true); 69 130 this._notification = null; 70 131 break; 132 + case 'allocate': 133 + content = 'Allocating "' + this.getName() + '"...'; 134 + this._notification 135 + .setContent(content); 136 + break; 137 + case 'chunks': 138 + content = 'Loading chunks for "' + this.getName() + '"...'; 139 + this._notification 140 + .setContent(content); 141 + break; 71 142 default: 72 143 var info = ''; 73 144 if (this.getTotalBytes()) { 74 145 var p = this._renderPercentComplete(); 75 146 var f = this._renderFileSize(); 76 - info = ' (' + p + ' of ' + f + ')'; 147 + info = p + ' of ' + f; 77 148 } 78 149 79 - info = 'Uploading "' + this.getName() + '"' + info + '...'; 150 + var head; 151 + if (this._isResume) { 152 + head = 'Resuming:'; 153 + } else if (this._chunks) { 154 + head = 'Uploading chunks:'; 155 + } else { 156 + head = 'Uploading:'; 157 + } 158 + 159 + info = [ 160 + JX.$N('strong', {}, this.getName()), 161 + JX.$N('br'), 162 + head + ' ' + info]; 80 163 81 164 this._notification 82 165 .setContent(info);
+2 -1
webroot/rsrc/js/core/behavior-drag-and-drop-textarea.js
··· 27 27 28 28 if (JX.PhabricatorDragAndDropFileUpload.isSupported()) { 29 29 var drop = new JX.PhabricatorDragAndDropFileUpload(target) 30 - .setURI(config.uri); 30 + .setURI(config.uri) 31 + .setChunkThreshold(config.chunkThreshold); 31 32 drop.listen('didBeginDrag', function() { 32 33 JX.DOM.alterClass(target, config.activatedClass, true); 33 34 });
+2 -1
webroot/rsrc/js/core/behavior-global-drag-and-drop.js
··· 23 23 var page = JX.$('phabricator-standard-page'); 24 24 var drop = new JX.PhabricatorDragAndDropFileUpload(page) 25 25 .setURI(config.uploadURI) 26 - .setViewPolicy(config.viewPolicy); 26 + .setViewPolicy(config.viewPolicy) 27 + .setChunkThreshold(config.chunkThreshold); 27 28 28 29 drop.listen('didBeginDrag', function() { 29 30 JX.Mask.show('global-upload-mask');