4
* Upload a list of @{class:ArcanistFileDataRef} objects over Conduit.
6
* // Create a new uploader.
7
* $uploader = id(new ArcanistFileUploader())
8
* ->setConduitClient($conduit);
10
* // Queue one or more files to be uploaded.
11
* $file = id(new ArcanistFileDataRef())
12
* ->setName('example.jpg')
13
* ->setPath('/path/to/example.jpg');
14
* $uploader->addFile($file);
16
* // Upload the files.
17
* $files = $uploader->uploadFiles();
19
* For details about building file references, see @{class:ArcanistFileDataRef}.
21
* @task config Configuring the Uploader
22
* @task add Adding Files
23
* @task upload Uploading Files
24
* @task internal Internals
26
final class ArcanistFileUploader extends Phobject {
32
/* -( Configuring the Uploader )------------------------------------------- */
36
* Provide a Conduit client to choose which server to upload files to.
38
* @param ConduitClient Configured client.
42
public function setConduitClient(ConduitClient $conduit) {
43
$this->conduit = $conduit;
48
/* -( Adding Files )------------------------------------------------------- */
52
* Add a file to the list of files to be uploaded.
54
* You can optionally provide an explicit key which will be used to identify
55
* the file. After adding files, upload them with @{method:uploadFiles}.
57
* @param ArcanistFileDataRef File data to upload.
58
* @param null|string Optional key to use to identify this file.
62
public function addFile(ArcanistFileDataRef $file, $key = null) {
65
$this->files[] = $file;
67
if (isset($this->files[$key])) {
70
'Two files were added with identical explicit keys ("%s"); each '.
71
'explicit key must be unique.',
74
$this->files[$key] = $file;
81
/* -( Uploading Files )---------------------------------------------------- */
85
* Upload files to the server.
87
* This transfers all files which have been queued with @{method:addFiles}
88
* over the Conduit link configured with @{method:setConduitClient}.
90
* This method returns a map of all file data references. If references were
91
* added with an explicit key when @{method:addFile} was called, the key is
92
* retained in the result map.
94
* On return, files are either populated with a PHID (indicating a successful
95
* upload) or a list of errors. See @{class:ArcanistFileDataRef} for
98
* @return map<string, ArcanistFileDataRef> Files with results populated.
101
public function uploadFiles() {
102
if (!$this->conduit) {
103
throw new PhutilInvalidStateException('setConduitClient');
106
$files = $this->files;
107
foreach ($files as $key => $file) {
110
} catch (Exception $ex) {
111
$file->didFail($ex->getMessage());
116
$conduit = $this->conduit;
118
foreach ($files as $key => $file) {
119
$futures[$key] = $conduit->callMethod(
122
'name' => $file->getName(),
123
'contentLength' => $file->getByteSize(),
124
'contentHash' => $file->getContentHash(),
128
$iterator = id(new FutureIterator($futures))->limit(4);
130
foreach ($iterator as $key => $future) {
132
$result = $future->resolve();
133
} catch (Exception $ex) {
134
// The most likely cause for a failure here is that the server does
135
// not support `file.allocate`. In this case, we'll try the older
136
// upload method below.
140
$phid = $result['filePHID'];
141
$file = $files[$key];
143
// We don't need to upload any data. Figure out why not: this can either
144
// be because of an error (server can't accept the data) or because the
145
// server already has the data.
146
if (!$result['upload']) {
150
'Unable to upload file: the server refused to accept file '.
151
'"%s". This usually means it is too large.',
154
// These server completed the upload by creating a reference to known
155
// file data. We don't need to transfer the actual data, and are all
157
$file->setPHID($phid);
163
// The server wants us to do an upload.
165
$chunks[$key] = array(
172
foreach ($chunks as $key => $chunk) {
173
$file = $chunk['file'];
174
$phid = $chunk['phid'];
176
$this->uploadChunks($file, $phid);
177
$file->setPHID($phid);
178
} catch (Exception $ex) {
181
'Unable to upload file chunks: %s',
187
foreach ($files as $key => $file) {
189
$phid = $this->uploadData($file);
190
$file->setPHID($phid);
191
} catch (Exception $ex) {
194
'Unable to upload file data: %s',
200
foreach ($this->files as $file) {
208
/* -( Internals )---------------------------------------------------------- */
212
* Upload missing chunks of a large file by calling `file.uploadchunk` over
217
private function uploadChunks(ArcanistFileDataRef $file, $file_phid) {
218
$conduit = $this->conduit;
220
$chunks = $conduit->callMethodSynchronous(
223
'filePHID' => $file_phid,
226
$remaining = array();
227
foreach ($chunks as $chunk) {
228
if (!$chunk['complete']) {
229
$remaining[] = $chunk;
233
$done = (count($chunks) - count($remaining));
238
'Resuming upload (%d of %d chunks remain).',
239
new PhutilNumber(count($remaining)),
240
new PhutilNumber(count($chunks))));
244
'Uploading chunks (%d chunks to upload).',
245
new PhutilNumber(count($remaining))));
248
$progress = new PhutilConsoleProgressBar();
249
$progress->setTotal(count($chunks));
251
for ($ii = 0; $ii < $done; $ii++) {
252
$progress->update(1);
257
// TODO: We could do these in parallel to improve upload performance.
258
foreach ($remaining as $chunk) {
259
$data = $file->readBytes($chunk['byteStart'], $chunk['byteEnd']);
261
$conduit->callMethodSynchronous(
264
'filePHID' => $file_phid,
265
'byteStart' => $chunk['byteStart'],
266
'dataEncoding' => 'base64',
267
'data' => base64_encode($data),
270
$progress->update(1);
276
* Upload an entire file by calling `file.upload` over Conduit.
280
private function uploadData(ArcanistFileDataRef $file) {
281
$conduit = $this->conduit;
283
$data = $file->readBytes(0, $file->getByteSize());
285
return $conduit->callMethodSynchronous(
288
'name' => $file->getName(),
289
'data_base64' => base64_encode($data),
295
* Write a status message.
299
private function writeStatus($message) {
300
fwrite(STDERR, $message."\n");