1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-29 18:22:41 +01:00
phorge-arcanist/src/upload/ArcanistFileUploader.php
epriestley 877e7b6388 Move Conduit file upload logic into a separate class
Summary:
Ref T8259. Currently, `arc upload` uses new logic but `arc diff` uses older logic internally. This prevents `arc diff` from uploading files larger than 4MB to newer servers.

Split the upload logic apart so the two upload pathways can share it. Callers now build a list of FileDataRefs and hand them to an Uploader for upload.

Test Plan:
Ran `arc upload` on:

  - One file.
  - Several files.
  - Small files.
  - Big files.
  - Directories.
  - Unreadable files.
  - Files full of random data.
  - The same file over and over again.
  - The same big file over and over again.
  - Artificially broke `file.allocate` and redid some of the simple cases (large/chunked aren't expected to work in this case).

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: joshuaspence, epriestley

Maniphest Tasks: T8259

Differential Revision: https://secure.phabricator.com/D13016
2015-05-27 10:25:53 -07:00

257 lines
6.1 KiB
PHP

<?php
/**
* Upload a list of @{class:ArcanistFileDataRef} objects over Conduit.
*
* // Create a new uploader.
* $uploader = id(new ArcanistFileUploader())
* ->setConduitClient($conduit);
*
* // Queue one or more files to be uploaded.
* $file = id(new ArcanistFileDataRef())
* ->setName('example.jpg')
* ->setPath('/path/to/example.jpg');
* $uploader->addFile($file);
*
* // Upload the files.
* $files = $uploader->uploadFiles();
*
* For details about building file references, see @{class:ArcanistFileDataRef}.
*
* @task config Configuring the Uploader
* @task add Adding Files
* @task upload Uploading Files
* @task internal Internals
*/
final class ArcanistFileUploader extends Phobject {
private $conduit;
private $files;
/* -( Configuring the Uploader )------------------------------------------- */
/**
* @task config
*/
public function setConduitClient(ConduitClient $conduit) {
$this->conduit = $conduit;
return $this;
}
/* -( Adding Files )------------------------------------------------------- */
/**
* @task add
*/
public function addFile(ArcanistFileDataRef $file) {
$this->files[] = $file;
return $this;
}
/* -( Uploading Files )---------------------------------------------------- */
/**
* @task upload
*/
public function uploadFiles() {
if (!$this->conduit) {
throw new PhutilInvalidStateException('setConduitClient');
}
$files = $this->files;
foreach ($files as $key => $file) {
try {
$file->willUpload();
} catch (Exception $ex) {
$file->didFail($ex->getMessage());
unset($files[$key]);
}
}
$conduit = $this->conduit;
$futures = array();
foreach ($files as $key => $file) {
$futures[$key] = $conduit->callMethod(
'file.allocate',
array(
'name' => $file->getName(),
'contentLength' => $file->getByteSize(),
'contentHash' => $file->getContentHash(),
));
}
$iterator = id(new FutureIterator($futures))->limit(4);
$chunks = array();
foreach ($iterator as $key => $future) {
try {
$result = $future->resolve();
} catch (Exception $ex) {
// The most likely cause for a failure here is that the server does
// not support `file.allocate`. In this case, we'll try the older
// upload method below.
continue;
}
$phid = $result['filePHID'];
$file = $files[$key];
// We don't need to upload any data. Figure out why not: this can either
// be because of an error (server can't accept the data) or because the
// server already has the data.
if (!$result['upload']) {
if (!$phid) {
$file->didFail(
pht(
'Unable to upload file: the server refused to accept file '.
'"%s". This usually means it is too large.',
$file->getName()));
} else {
// These server completed the upload by creating a reference to known
// file data. We don't need to transfer the actual data, and are all
// set.
$file->setPHID($phid);
}
unset($files[$key]);
continue;
}
// The server wants us to do an upload.
if ($phid) {
$chunks[$key] = array(
'file' => $file,
'phid' => $phid,
);
}
}
foreach ($chunks as $key => $chunk) {
$file = $chunk['file'];
$phid = $chunk['phid'];
try {
$this->uploadChunks($file, $phid);
$file->setPHID($phid);
} catch (Exception $ex) {
$file->didFail(
pht(
'Unable to upload file chunks: %s',
$ex->getMessage()));
}
unset($files[$key]);
}
foreach ($files as $key => $file) {
try {
$phid = $this->uploadData($file);
$file->setPHID($phid);
} catch (Exception $ex) {
$file->didFail(
pht(
'Unable to upload file data: %s',
$ex->getMessage()));
}
unset($files[$key]);
}
foreach ($this->files as $file) {
$file->didUpload();
}
return $this->files;
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
private function uploadChunks(ArcanistFileDataRef $file, $file_phid) {
$conduit = $this->conduit;
$chunks = $conduit->callMethodSynchronous(
'file.querychunks',
array(
'filePHID' => $file_phid,
));
$remaining = array();
foreach ($chunks as $chunk) {
if (!$chunk['complete']) {
$remaining[] = $chunk;
}
}
$done = (count($chunks) - count($remaining));
if ($done) {
$this->writeStatus(
pht(
'Resuming upload (%d of %d chunks remain).',
new PhutilNumber(count($remaining)),
new PhutilNumber(count($chunks))));
} else {
$this->writeStatus(
pht(
'Uploading chunks (%d chunks to upload).',
new PhutilNumber(count($remaining))));
}
$progress = new PhutilConsoleProgressBar();
$progress->setTotal(count($chunks));
for ($ii = 0; $ii < $done; $ii++) {
$progress->update(1);
}
$progress->draw();
// TODO: We could do these in parallel to improve upload performance.
foreach ($remaining as $chunk) {
$data = $file->readBytes($chunk['byteStart'], $chunk['byteEnd']);
$conduit->callMethodSynchronous(
'file.uploadchunk',
array(
'filePHID' => $file_phid,
'byteStart' => $chunk['byteStart'],
'dataEncoding' => 'base64',
'data' => base64_encode($data),
));
$progress->update(1);
}
}
/**
* @task internal
*/
private function uploadData(ArcanistFileDataRef $file) {
$conduit = $this->conduit;
$data = $file->readBytes(0, $file->getByteSize());
return $conduit->callMethodSynchronous(
'file.upload',
array(
'name' => $file->getName(),
'data_base64' => base64_encode($data),
));
}
/**
* @task internal
*/
private function writeStatus($message) {
echo $message."\n";
}
}