Changeset View
Changeset View
Standalone View
Standalone View
src/workflow/ArcanistDownloadWorkflow.php
| <?php | <?php | ||||
| /** | final class ArcanistDownloadWorkflow | ||||
| * Download a file from Phabricator. | extends ArcanistArcWorkflow { | ||||
| */ | |||||
| final class ArcanistDownloadWorkflow extends ArcanistWorkflow { | |||||
| private $id; | |||||
| private $saveAs; | |||||
| private $show; | |||||
| public function getWorkflowName() { | public function getWorkflowName() { | ||||
| return 'download'; | return 'download'; | ||||
| } | } | ||||
| public function getCommandSynopses() { | public function getWorkflowInformation() { | ||||
| return phutil_console_format(<<<EOTEXT | $help = pht(<<<EOTEXT | ||||
| **download** __file__ [--as __name__] [--show] | Download a file to local disk. | ||||
| EOTEXT | EOTEXT | ||||
| ); | ); | ||||
| } | |||||
| public function getCommandHelp() { | |||||
| return phutil_console_format(<<<EOTEXT | |||||
| Supports: filesystems | |||||
| Download a file to local disk, e.g.: | |||||
| $ arc download F33 # Download file 'F33' | return $this->newWorkflowInformation() | ||||
| EOTEXT | ->setSynopsis(pht('Download a file to local disk.')) | ||||
| ); | ->addExample(pht('**download** [__options__] -- __file__')) | ||||
| ->setHelp($help); | |||||
| } | } | ||||
| public function getArguments() { | public function getWorkflowArguments() { | ||||
| return array( | return array( | ||||
| 'show' => array( | $this->newWorkflowArgument('as') | ||||
| 'conflicts' => array( | ->setParameter('path') | ||||
| 'as' => pht( | ->setHelp(pht('Save the file to a specific location.')), | ||||
| 'Use %s to direct the file to stdout, or %s to direct '. | $this->newWorkflowArgument('argv') | ||||
| 'it to a named location.', | ->setWildcard(true), | ||||
| '--show', | |||||
| '--as'), | |||||
| ), | |||||
| 'help' => pht('Write file to stdout instead of to disk.'), | |||||
| ), | |||||
| 'as' => array( | |||||
| 'param' => 'name', | |||||
| 'help' => pht( | |||||
| 'Save the file with a specific name rather than the default.'), | |||||
| ), | |||||
| '*' => 'argv', | |||||
| ); | ); | ||||
| } | } | ||||
| protected function didParseArguments() { | protected function didParseArguments() { | ||||
| $argv = $this->getArgument('argv'); | $argv = $this->getArgument('argv'); | ||||
| if (!$argv) { | if (!$argv) { | ||||
| throw new ArcanistUsageException(pht('Specify a file to download.')); | throw new ArcanistUsageException(pht('Specify a file to download.')); | ||||
| } | } | ||||
| if (count($argv) > 1) { | if (count($argv) > 1) { | ||||
| throw new ArcanistUsageException( | throw new ArcanistUsageException( | ||||
| pht('Specify exactly one file to download.')); | pht('Specify exactly one file to download.')); | ||||
| } | } | ||||
| $file = reset($argv); | $file = reset($argv); | ||||
| if (!preg_match('/^F?\d+$/', $file)) { | if (!preg_match('/^F?\d+$/', $file)) { | ||||
| throw new ArcanistUsageException( | throw new ArcanistUsageException( | ||||
| pht('Specify file by ID, e.g. %s.', 'F123')); | pht('Specify file by ID, e.g. %s.', 'F123')); | ||||
| } | } | ||||
| $this->id = (int)ltrim($file, 'F'); | $this->id = (int)ltrim($file, 'F'); | ||||
| $this->saveAs = $this->getArgument('as'); | $this->saveAs = $this->getArgument('as'); | ||||
| $this->show = $this->getArgument('show'); | $this->show = $this->getArgument('show'); | ||||
| } | } | ||||
| public function requiresAuthentication() { | |||||
| return true; | |||||
| } | |||||
| public function run() { | |||||
| $conduit = $this->getConduit(); | |||||
| $id = $this->id; | public function runWorkflow() { | ||||
| $display_name = 'F'.$id; | $file_symbols = $this->getArgument('argv'); | ||||
| $is_show = $this->show; | |||||
| $save_as = $this->saveAs; | |||||
| $path = null; | |||||
| try { | if (!$file_symbols) { | ||||
| $file = $conduit->callMethodSynchronous( | throw new PhutilArgumentUsageException( | ||||
| 'file.search', | pht( | ||||
| array( | 'Specify a file to download, like "F123".')); | ||||
| 'constraints' => array( | } | ||||
| 'ids' => array($id), | |||||
| ), | |||||
| )); | |||||
| $data = $file['data']; | if (count($file_symbols) > 1) { | ||||
| if (!$data) { | throw new PhutilArgumentUsageException( | ||||
| throw new ArcanistUsageException( | |||||
| pht( | pht( | ||||
| 'File "%s" is not a valid file, or not visible.', | 'Specify exactly one file to download.')); | ||||
| $display_name)); | |||||
| } | } | ||||
| $file = head($data); | $file_symbol = head($file_symbols); | ||||
| $data_uri = idxv($file, array('fields', 'dataURI')); | |||||
| if ($data_uri === null) { | $symbols = $this->getSymbolEngine(); | ||||
| throw new ArcanistUsageException( | $file_ref = $symbols->loadFileForSymbol($file_symbol); | ||||
| if (!$file_ref) { | |||||
| throw new PhutilArgumentUsageException( | |||||
| pht( | pht( | ||||
| 'File "%s" can not be downloaded.', | 'File "%s" does not exist, or you do not have permission to '. | ||||
| $display_name)); | 'view it.', | ||||
| $file_symbol)); | |||||
| } | } | ||||
| if ($is_show) { | $is_stdout = false; | ||||
| // Skip all the file path stuff if we're just going to echo the | $path = null; | ||||
| // file contents. | |||||
| } else { | |||||
| if ($save_as !== null) { | |||||
| $path = Filesystem::resolvePath($save_as); | |||||
| $try_unique = false; | $save_as = $this->getArgument('as'); | ||||
| } else { | if ($save_as === '-') { | ||||
| $path = idxv($file, array('fields', 'name'), $display_name); | $is_stdout = true; | ||||
| } else if ($save_as === null) { | |||||
| $path = $file_ref->getName(); | |||||
| $path = basename($path); | $path = basename($path); | ||||
| $path = Filesystem::resolvePath($path); | $path = Filesystem::resolvePath($path); | ||||
| $try_unique = true; | $try_unique = true; | ||||
| } else { | |||||
| $path = Filesystem::resolvePath($save_as); | |||||
| $try_unique = false; | |||||
| } | } | ||||
| $file_handle = null; | |||||
| if (!$is_stdout) { | |||||
| if ($try_unique) { | if ($try_unique) { | ||||
| $path = Filesystem::writeUniqueFile($path, ''); | $path = Filesystem::writeUniqueFile($path, ''); | ||||
| Filesystem::remove($path); | |||||
| } else { | } else { | ||||
| if (Filesystem::pathExists($path)) { | if (Filesystem::pathExists($path)) { | ||||
| throw new ArcanistUsageException( | throw new PhutilArgumentUsageException( | ||||
| pht( | pht( | ||||
| 'File "%s" already exists.', | 'File "%s" already exists.', | ||||
| $save_as)); | $path)); | ||||
| } | } | ||||
| Filesystem::writeFile($path, ''); | |||||
| } | } | ||||
| $display_path = Filesystem::readablePath($path); | |||||
| } | } | ||||
| $size = idxv($file, array('fields', 'size'), 0); | $display_path = Filesystem::readablePath($path); | ||||
| if ($is_show) { | $display_name = $file_ref->getName(); | ||||
| $file_handle = null; | if (!strlen($display_name)) { | ||||
| } else { | $display_name = $file_ref->getMonogram(); | ||||
| $file_handle = fopen($path, 'ab+'); | |||||
| if ($file_handle === false) { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'Failed to open file "%s" for writing.', | |||||
| $path)); | |||||
| } | } | ||||
| $this->writeInfo( | $expected_bytes = $file_ref->getSize(); | ||||
| $log = $this->getLogEngine(); | |||||
| if (!$is_stdout) { | |||||
| $log->writeStatus( | |||||
| pht('DATA'), | pht('DATA'), | ||||
| pht( | pht( | ||||
| 'Downloading "%s" (%s byte(s))...', | 'Downloading "%s" (%s byte(s)) to "%s"...', | ||||
| $display_name, | $display_name, | ||||
| new PhutilNumber($size))); | new PhutilNumber($expected_bytes), | ||||
| $display_path)); | |||||
| } | } | ||||
| $data_uri = $file_ref->getDataURI(); | |||||
| $future = new HTTPSFuture($data_uri); | $future = new HTTPSFuture($data_uri); | ||||
| if (!$is_stdout) { | |||||
| // For small files, don't bother drawing a progress bar. | // For small files, don't bother drawing a progress bar. | ||||
| $minimum_bar_bytes = (1024 * 1024 * 4); | $minimum_bar_bytes = (1024 * 1024 * 4); | ||||
| if ($expected_bytes > $minimum_bar_bytes) { | |||||
| $progress = id(new PhutilConsoleProgressSink()) | |||||
| ->setTotalWork($expected_bytes); | |||||
| if ($is_show || ($size < $minimum_bar_bytes)) { | $future->setProgressSink($progress); | ||||
| $bar = null; | |||||
| } else { | |||||
| $bar = id(new PhutilConsoleProgressBar()) | |||||
| ->setTotal($size); | |||||
| } | } | ||||
| // TODO: We should stream responses to disk, but cURL gives us the raw | // Compute a timeout based on the expected filesize. | ||||
| // HTTP response data and BaseHTTPFuture can not currently parse it in | $transfer_rate = 32 * 1024; | ||||
| // a stream-oriented way. Until this is resolved, buffer the file data | $timeout = (int)(120 + ($expected_bytes / $transfer_rate)); | ||||
| // in memory and write it to disk in one shot. | |||||
| list($status, $data) = $future->resolve(); | $future | ||||
| if ($status->getStatusCode() !== 200) { | ->setTimeout($timeout) | ||||
| throw new Exception( | ->setDownloadPath($path); | ||||
| pht( | |||||
| 'Got HTTP %d status response, expected HTTP 200.', | |||||
| $status->getStatusCode())); | |||||
| } | |||||
| if (strlen($data)) { | |||||
| if ($is_show) { | |||||
| echo $data; | |||||
| } else { | |||||
| $ok = fwrite($file_handle, $data); | |||||
| if ($ok === false) { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'Failed to write file data to "%s".', | |||||
| $path)); | |||||
| } | |||||
| } | |||||
| } | } | ||||
| if ($bar) { | try { | ||||
| $bar->update(strlen($data)); | list($data) = $future->resolvex(); | ||||
| } catch (Exception $ex) { | |||||
| Filesystem::removePath($path); | |||||
| throw $ex; | |||||
| } | } | ||||
| if ($bar) { | if ($is_stdout) { | ||||
| $bar->done(); | $file_bytes = strlen($data); | ||||
| } else { | |||||
| // TODO: This has various potential problems with clearstatcache() and | |||||
| // 32-bit systems, but just ignore them for now. | |||||
| $file_bytes = filesize($path); | |||||
| } | } | ||||
| if ($file_handle) { | if ($file_bytes !== $expected_bytes) { | ||||
| $ok = fclose($file_handle); | |||||
| if ($ok === false) { | |||||
| throw new Exception( | throw new Exception( | ||||
| pht( | pht( | ||||
| 'Failed to close file handle for "%s".', | 'Downloaded file size (%s bytes) does not match expected '. | ||||
| $path)); | 'file size (%s bytes). This download may be incomplete or '. | ||||
| } | 'corrupt.', | ||||
| new PhutilNumber($file_bytes), | |||||
| new PhutilNumber($expected_bytes))); | |||||
| } | } | ||||
| if (!$is_show) { | if ($is_stdout) { | ||||
| $this->writeOkay( | echo $data; | ||||
| } else { | |||||
| $log->writeStatus( | |||||
| pht('DONE'), | pht('DONE'), | ||||
| pht( | pht( | ||||
| 'Saved "%s" as "%s".', | 'Saved "%s" as "%s".', | ||||
| $display_name, | $display_name, | ||||
| $display_path)); | $display_path)); | ||||
| } | } | ||||
| return 0; | return 0; | ||||
| } catch (Exception $ex) { | |||||
| // If we created an empty file, clean it up. | |||||
| if (!$is_show) { | |||||
| if ($path !== null) { | |||||
| Filesystem::remove($path); | |||||
| } | |||||
| } | |||||
| // If we fail for any reason, fall back to the older mechanism using | |||||
| // "file.info" and "file.download". | |||||
| } | |||||
| $this->writeStatusMessage(pht('Getting file information...')."\n"); | |||||
| $info = $conduit->callMethodSynchronous( | |||||
| 'file.info', | |||||
| array( | |||||
| 'id' => $this->id, | |||||
| )); | |||||
| $desc = pht('(%s bytes)', new PhutilNumber($info['byteSize'])); | |||||
| if ($info['name']) { | |||||
| $desc = "'".$info['name']."' ".$desc; | |||||
| } | |||||
| $this->writeStatusMessage(pht('Downloading file %s...', $desc)."\n"); | |||||
| $data = $conduit->callMethodSynchronous( | |||||
| 'file.download', | |||||
| array( | |||||
| 'phid' => $info['phid'], | |||||
| )); | |||||
| $data = base64_decode($data); | |||||
| if ($this->show) { | |||||
| echo $data; | |||||
| } else { | |||||
| $path = Filesystem::writeUniqueFile( | |||||
| nonempty($this->saveAs, $info['name'], 'file'), | |||||
| $data); | |||||
| $this->writeStatusMessage(pht("Saved file as '%s'.", $path)."\n"); | |||||
| } | |||||
| return 0; | |||||
| } | } | ||||
| } | } | ||||