Skip to content

Commit 135280b

Browse files
author
epriestley
committed
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
1 parent aa4adf3 commit 135280b

File tree

11 files changed

+486
-107
lines changed

11 files changed

+486
-107
lines changed

resources/celerity/map.php

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
return array(
99
'names' => array(
1010
'core.pkg.css' => 'efdeeb14',
11-
'core.pkg.js' => 'deae6907',
11+
'core.pkg.js' => '31bc6546',
1212
'darkconsole.pkg.js' => '8ab24e01',
1313
'differential.pkg.css' => '1940be3f',
14-
'differential.pkg.js' => '53c1ccc2',
14+
'differential.pkg.js' => 'be1e5f9b',
1515
'diffusion.pkg.css' => '591664fa',
1616
'diffusion.pkg.js' => 'bfc0737b',
1717
'maniphest.pkg.css' => '68d4dd3d',
@@ -438,9 +438,9 @@
438438
'rsrc/js/application/uiexample/gesture-example.js' => '558829c2',
439439
'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5',
440440
'rsrc/js/core/Busy.js' => '6453c869',
441-
'rsrc/js/core/DragAndDropFileUpload.js' => '8c49f386',
441+
'rsrc/js/core/DragAndDropFileUpload.js' => 'fd6ace61',
442442
'rsrc/js/core/DraggableList.js' => 'a16ec1c6',
443-
'rsrc/js/core/FileUpload.js' => 'a4ae61bf',
443+
'rsrc/js/core/FileUpload.js' => '477359c8',
444444
'rsrc/js/core/Hovercard.js' => '7e8468ae',
445445
'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2',
446446
'rsrc/js/core/KeyboardShortcutManager.js' => 'c1700f6f',
@@ -458,13 +458,13 @@
458458
'rsrc/js/core/behavior-crop.js' => 'fa0f4fc2',
459459
'rsrc/js/core/behavior-dark-console.js' => '08883e8b',
460460
'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',
462462
'rsrc/js/core/behavior-error-log.js' => '6882e80a',
463463
'rsrc/js/core/behavior-fancy-datepicker.js' => 'c51ae228',
464464
'rsrc/js/core/behavior-file-tree.js' => '88236f00',
465465
'rsrc/js/core/behavior-form.js' => '5c54cbf3',
466466
'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',
468468
'rsrc/js/core/behavior-high-security-warning.js' => '8fc1c918',
469469
'rsrc/js/core/behavior-history-install.js' => '7ee2b591',
470470
'rsrc/js/core/behavior-hovercard.js' => 'f36e01af',
@@ -549,7 +549,7 @@
549549
'javelin-behavior-aphlict-status' => 'ea681761',
550550
'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884',
551551
'javelin-behavior-aphront-crop' => 'fa0f4fc2',
552-
'javelin-behavior-aphront-drag-and-drop-textarea' => '92eb531d',
552+
'javelin-behavior-aphront-drag-and-drop-textarea' => '6d49590e',
553553
'javelin-behavior-aphront-form-disable-on-submit' => '5c54cbf3',
554554
'javelin-behavior-aphront-more' => 'a80d0378',
555555
'javelin-behavior-audio-source' => '59b251eb',
@@ -588,7 +588,7 @@
588588
'javelin-behavior-durable-column' => 'a3ba7034',
589589
'javelin-behavior-error-log' => '6882e80a',
590590
'javelin-behavior-fancy-datepicker' => 'c51ae228',
591-
'javelin-behavior-global-drag-and-drop' => '8c584f17',
591+
'javelin-behavior-global-drag-and-drop' => 'bbdf75ca',
592592
'javelin-behavior-herald-rule-editor' => '7ebaeed3',
593593
'javelin-behavior-high-security-warning' => '8fc1c918',
594594
'javelin-behavior-history-install' => '7ee2b591',
@@ -719,11 +719,11 @@
719719
'phabricator-core-css' => '86bfbe8c',
720720
'phabricator-countdown-css' => '86b7b0a0',
721721
'phabricator-dashboard-css' => '17937d22',
722-
'phabricator-drag-and-drop-file-upload' => '8c49f386',
722+
'phabricator-drag-and-drop-file-upload' => 'fd6ace61',
723723
'phabricator-draggable-list' => 'a16ec1c6',
724724
'phabricator-fatal-config-template-css' => '8e6c6fcd',
725725
'phabricator-feed-css' => 'b513b5f4',
726-
'phabricator-file-upload' => 'a4ae61bf',
726+
'phabricator-file-upload' => '477359c8',
727727
'phabricator-filetree-view-css' => 'fccf9f82',
728728
'phabricator-flag-css' => '5337623f',
729729
'phabricator-hovercard' => '7e8468ae',
@@ -1122,6 +1122,11 @@
11221122
'javelin-dom',
11231123
'javelin-workflow',
11241124
),
1125+
'477359c8' => array(
1126+
'javelin-install',
1127+
'javelin-dom',
1128+
'phabricator-notification',
1129+
),
11251130
47830651 => array(
11261131
'javelin-behavior',
11271132
'javelin-dom',
@@ -1273,6 +1278,12 @@
12731278
'javelin-typeahead',
12741279
'javelin-uri',
12751280
),
1281+
'6d49590e' => array(
1282+
'javelin-behavior',
1283+
'javelin-dom',
1284+
'phabricator-drag-and-drop-file-upload',
1285+
'phabricator-textareautils',
1286+
),
12761287
'6e2de6f2' => array(
12771288
'multirow-row-manager',
12781289
'javelin-install',
@@ -1508,21 +1519,6 @@
15081519
'javelin-request',
15091520
'javelin-typeahead-source',
15101521
),
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-
),
15261522
'8ce821c5' => array(
15271523
'phabricator-notification',
15281524
'javelin-stratcom',
@@ -1546,12 +1542,6 @@
15461542
'javelin-uri',
15471543
'phabricator-notification',
15481544
),
1549-
'92eb531d' => array(
1550-
'javelin-behavior',
1551-
'javelin-dom',
1552-
'phabricator-drag-and-drop-file-upload',
1553-
'phabricator-textareautils',
1554-
),
15551545
'9414ff18' => array(
15561546
'javelin-behavior',
15571547
'javelin-resource',
@@ -1638,11 +1628,6 @@
16381628
'javelin-vector',
16391629
'differential-inline-comment-editor',
16401630
),
1641-
'a4ae61bf' => array(
1642-
'javelin-install',
1643-
'javelin-dom',
1644-
'phabricator-notification',
1645-
),
16461631
'a80d0378' => array(
16471632
'javelin-behavior',
16481633
'javelin-stratcom',
@@ -1718,6 +1703,13 @@
17181703
'javelin-stratcom',
17191704
'javelin-dom',
17201705
),
1706+
'bbdf75ca' => array(
1707+
'javelin-behavior',
1708+
'javelin-dom',
1709+
'javelin-uri',
1710+
'javelin-mask',
1711+
'phabricator-drag-and-drop-file-upload',
1712+
),
17211713
'bd4c8dca' => array(
17221714
'javelin-install',
17231715
'javelin-util',
@@ -2007,6 +1999,14 @@
20071999
'javelin-dom',
20082000
'phortune-credit-card-form',
20092001
),
2002+
'fd6ace61' => array(
2003+
'javelin-install',
2004+
'javelin-util',
2005+
'javelin-request',
2006+
'javelin-dom',
2007+
'javelin-uri',
2008+
'phabricator-file-upload',
2009+
),
20102010
'fe287620' => array(
20112011
'javelin-install',
20122012
'javelin-dom',

src/applications/files/conduit/FileAllocateConduitAPIMethod.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,20 @@ protected function execute(ConduitAPIRequest $request) {
121121
}
122122

123123
// 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+
}
124133

125134
return array(
126135
'upload' => false,
127136
'filePHID' => null,
137+
'error' => $error,
128138
);
129139
}
130140

src/applications/files/controller/PhabricatorFileDropUploadController.php

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,82 @@ public function processRequest() {
1313
// NOTE: Throws if valid CSRF token is not present in the request.
1414
$request->validateCSRF();
1515

16-
$data = PhabricatorStartup::getRawInput();
1716
$name = $request->getStr('name');
18-
17+
$file_phid = $request->getStr('phid');
1918
// If there's no explicit view policy, make it very restrictive by default.
2019
// This is the correct policy for files dropped onto objects during
2120
// creation, comment and edit flows.
22-
2321
$view_policy = $request->getStr('viewPolicy');
2422
if (!$view_policy) {
2523
$view_policy = $viewer->getPHID();
2624
}
2725

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+
2892
$file = PhabricatorFile::newFromXHRUpload(
2993
$data,
3094
array(
@@ -34,12 +98,30 @@ public function processRequest() {
3498
'isExplicitUpload' => true,
3599
));
36100

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;
43125
}
44126

45127
}

src/applications/files/engine/PhabricatorChunkedFileStorageEngine.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ private function getWritableEngine() {
162162
return false;
163163
}
164164

165-
private function getChunkSize() {
165+
public function getChunkSize() {
166166
// TODO: This is an artificially small size to make it easier to
167167
// test chunking.
168168
return 32;

src/applications/files/engine/PhabricatorFileStorageEngine.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,4 +255,40 @@ public static function loadWritableEngines() {
255255
return $writable;
256256
}
257257

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+
258294
}

src/applications/files/view/PhabricatorGlobalUploadTargetView.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ public function render() {
2424
require_celerity_resource('global-drag-and-drop-css');
2525

2626
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(),
3233
));
3334

3435
return phutil_tag(

src/view/form/control/PhabricatorRemarkupControl.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ protected function renderInput() {
3535
Javelin::initBehavior(
3636
'aphront-drag-and-drop-textarea',
3737
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(),
4142
));
4243

4344
Javelin::initBehavior(

0 commit comments

Comments
 (0)