diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2501,6 +2501,7 @@ 'PhabricatorInvalidConfigSetupCheck' => 'applications/config/check/PhabricatorInvalidConfigSetupCheck.php', 'PhabricatorIteratedMD5PasswordHasher' => 'infrastructure/util/password/PhabricatorIteratedMD5PasswordHasher.php', 'PhabricatorIteratedMD5PasswordHasherTestCase' => 'infrastructure/util/password/__tests__/PhabricatorIteratedMD5PasswordHasherTestCase.php', + 'PhabricatorIteratorFileUploadSource' => 'applications/files/uploadsource/PhabricatorIteratorFileUploadSource.php', 'PhabricatorJIRAAuthProvider' => 'applications/auth/provider/PhabricatorJIRAAuthProvider.php', 'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/PhabricatorJavelinLinter.php', 'PhabricatorJiraIssueHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorJiraIssueHasObjectEdgeType.php', @@ -6947,6 +6948,7 @@ 'PhabricatorInvalidConfigSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorIteratedMD5PasswordHasher' => 'PhabricatorPasswordHasher', 'PhabricatorIteratedMD5PasswordHasherTestCase' => 'PhabricatorTestCase', + 'PhabricatorIteratorFileUploadSource' => 'PhabricatorFileUploadSource', 'PhabricatorJIRAAuthProvider' => 'PhabricatorOAuth1AuthProvider', 'PhabricatorJavelinLinter' => 'ArcanistLinter', 'PhabricatorJiraIssueHasObjectEdgeType' => 'PhabricatorEdgeType', diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -891,7 +891,12 @@ } $path = $this->getGitLFSRequestPath($repository); - if ($path == 'objects/batch') { + $matches = null; + + if (preg_match('(^upload/(.*)\z)', $path, $matches)) { + $oid = $matches[1]; + return $this->serveGitLFSUploadRequest($repository, $viewer, $oid); + } else if ($path == 'objects/batch') { return $this->serveGitLFSBatchRequest($repository, $viewer); } else { return DiffusionGitLFSResponse::newErrorResponse( @@ -947,7 +952,7 @@ if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setViewer($viewer) - ->withPHIDs(array($file_phids)) + ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); } else { @@ -960,6 +965,7 @@ $oid = idx($object, 'oid'); $size = idx($object, 'size'); $ref = idx($refs, $oid); + $error = null; // NOTE: If we already have a ref for this object, we only emit a // "download" action. The client should not upload the file again. @@ -968,9 +974,26 @@ if ($ref) { $file = idx($files, $ref->getFilePHID()); if ($file) { + // Git LFS may prompt users for authentication if the action does + // not provide an "Authorization" header and does not have a query + // parameter named "token". See here for discussion: + // + $no_authorization = 'Basic '.base64_encode('none'); + $get_uri = $file->getCDNURIWithToken(); $actions['download'] = array( 'href' => $get_uri, + 'header' => array( + 'Authorization' => $no_authorization, + ), + ); + } else { + $error = array( + 'code' => 404, + 'message' => pht( + 'Object "%s" was previously uploaded, but no longer exists '. + 'on this server.', + $oid), ); } } else if ($want_upload) { @@ -995,11 +1018,20 @@ ); } - $output[] = array( + $object = array( 'oid' => $oid, 'size' => $size, - 'actions' => $actions, ); + + if ($actions) { + $object['actions'] = $actions; + } + + if ($error) { + $object['error'] = $error; + } + + $output[] = $object; } $output = array( @@ -1010,6 +1042,69 @@ ->setContent($output); } + private function serveGitLFSUploadRequest( + PhabricatorRepository $repository, + PhabricatorUser $viewer, + $oid) { + + $ref = id(new PhabricatorRepositoryGitLFSRefQuery()) + ->setViewer($viewer) + ->withRepositoryPHIDs(array($repository->getPHID())) + ->withObjectHashes(array($oid)) + ->executeOne(); + if ($ref) { + return DiffusionGitLFSResponse::newErrorResponse( + 405, + pht( + 'Content for object "%s" is already known to this server. It can '. + 'not be uploaded again.', + $oid)); + } + + $request_stream = new AphrontRequestStream(); + $request_iterator = $request_stream->getIterator(); + $hashing_iterator = id(new PhutilHashingIterator($request_iterator)) + ->setAlgorithm('sha256'); + + $source = id(new PhabricatorIteratorFileUploadSource()) + ->setName('lfs-'.$oid) + ->setViewPolicy(PhabricatorPolicies::POLICY_NOONE) + ->setIterator($hashing_iterator); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $file = $source->uploadFile(); + unset($unguarded); + + $hash = $hashing_iterator->getHash(); + if ($hash !== $oid) { + return DiffusionGitLFSResponse::newErrorResponse( + 400, + pht( + 'Uploaded data is corrupt or invalid. Expected hash "%s", actual '. + 'hash "%s".', + $oid, + $hash)); + } + + $ref = id(new PhabricatorRepositoryGitLFSRef()) + ->setRepositoryPHID($repository->getPHID()) + ->setObjectHash($hash) + ->setByteSize($file->getByteSize()) + ->setAuthorPHID($viewer->getPHID()) + ->setFilePHID($file->getPHID()); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + // Attach the file to the repository to give users permission + // to access it. + $file->attachToObject($repository->getPHID()); + $ref->save(); + unset($unguarded); + + // This is just a plain HTTP 200 with no content, which is what `git lfs` + // expects. + return new DiffusionGitLFSResponse(); + } + private function newGitLFSHTTPAuthorization( PhabricatorRepository $repository, PhabricatorUser $viewer, diff --git a/src/applications/files/uploadsource/PhabricatorFileUploadSource.php b/src/applications/files/uploadsource/PhabricatorFileUploadSource.php --- a/src/applications/files/uploadsource/PhabricatorFileUploadSource.php +++ b/src/applications/files/uploadsource/PhabricatorFileUploadSource.php @@ -72,7 +72,9 @@ $data->rewind(); $this->didRewind = true; } else { - $data->next(); + if ($data->valid()) { + $data->next(); + } } if (!$data->valid()) { diff --git a/src/applications/files/uploadsource/PhabricatorIteratorFileUploadSource.php b/src/applications/files/uploadsource/PhabricatorIteratorFileUploadSource.php new file mode 100644 --- /dev/null +++ b/src/applications/files/uploadsource/PhabricatorIteratorFileUploadSource.php @@ -0,0 +1,25 @@ +iterator = $iterator; + return $this; + } + + public function getIterator() { + return $this->iterator; + } + + protected function newDataIterator() { + return $this->getIterator(); + } + + protected function getDataLength() { + return null; + } + +}