Index: src/__phutil_library_map__.php =================================================================== --- src/__phutil_library_map__.php +++ src/__phutil_library_map__.php @@ -207,6 +207,9 @@ 'ConduitAPI_phpast_Method' => 'applications/phpast/conduit/ConduitAPI_phpast_Method.php', 'ConduitAPI_phpast_getast_Method' => 'applications/phpast/conduit/ConduitAPI_phpast_getast_Method.php', 'ConduitAPI_phpast_version_Method' => 'applications/phpast/conduit/ConduitAPI_phpast_version_Method.php', + 'ConduitAPI_phragment_Method' => 'applications/phragment/conduit/ConduitAPI_phragment_Method.php', + 'ConduitAPI_phragment_getpatch_Method' => 'applications/phragment/conduit/ConduitAPI_phragment_getpatch_Method.php', + 'ConduitAPI_phragment_queryfragments_Method' => 'applications/phragment/conduit/ConduitAPI_phragment_queryfragments_Method.php', 'ConduitAPI_phriction_Method' => 'applications/phriction/conduit/ConduitAPI_phriction_Method.php', 'ConduitAPI_phriction_edit_Method' => 'applications/phriction/conduit/ConduitAPI_phriction_edit_Method.php', 'ConduitAPI_phriction_history_Method' => 'applications/phriction/conduit/ConduitAPI_phriction_history_Method.php', @@ -2588,6 +2591,9 @@ 'ConduitAPI_phpast_Method' => 'ConduitAPIMethod', 'ConduitAPI_phpast_getast_Method' => 'ConduitAPI_phpast_Method', 'ConduitAPI_phpast_version_Method' => 'ConduitAPI_phpast_Method', + 'ConduitAPI_phragment_Method' => 'ConduitAPIMethod', + 'ConduitAPI_phragment_getpatch_Method' => 'ConduitAPI_phragment_Method', + 'ConduitAPI_phragment_queryfragments_Method' => 'ConduitAPI_phragment_Method', 'ConduitAPI_phriction_Method' => 'ConduitAPIMethod', 'ConduitAPI_phriction_edit_Method' => 'ConduitAPI_phriction_Method', 'ConduitAPI_phriction_history_Method' => 'ConduitAPI_phriction_Method', Index: src/applications/files/query/PhabricatorFileQuery.php =================================================================== --- src/applications/files/query/PhabricatorFileQuery.php +++ src/applications/files/query/PhabricatorFileQuery.php @@ -13,6 +13,7 @@ private $transforms; private $dateCreatedAfter; private $dateCreatedBefore; + private $contentHashes; public function withIDs(array $ids) { $this->ids = $ids; @@ -39,6 +40,11 @@ return $this; } + public function withContentHashes(array $content_hashes) { + $this->contentHashes = $content_hashes; + return $this; + } + /** * Select files which are transformations of some other file. For example, * you can use this query to find previously generated thumbnails of an image @@ -228,6 +234,13 @@ $this->dateCreatedBefore); } + if ($this->contentHashes) { + $where[] = qsprintf( + $conn_r, + 'f.contentHash IN (%Ls)', + $this->contentHashes); + } + return $this->formatWhereClause($where); } Index: src/applications/phragment/conduit/ConduitAPI_phragment_Method.php =================================================================== --- /dev/null +++ src/applications/phragment/conduit/ConduitAPI_phragment_Method.php @@ -0,0 +1,13 @@ + 'required string', + 'state' => 'required dict', + ); + } + + public function defineReturnType() { + return 'nonempty dict'; + } + + public function defineErrorTypes() { + return array( + 'ERR_BAD_FRAGMENT' => 'No such fragment exists', + ); + } + + protected function execute(ConduitAPIRequest $request) { + $path = $request->getValue('path'); + $state = $request->getValue('state'); + // The state is an array mapping file paths to hashes. + + $patches = array(); + + // We need to get all of the mappings (like phragment.getstate) first + // so that we can detect deletions and creations of files. + $fragment = id(new PhragmentFragmentQuery()) + ->setViewer($request->getUser()) + ->withPaths(array($path)) + ->executeOne(); + if ($fragment === null) { + throw new ConduitException('ERR_BAD_FRAGMENT'); + } + + $mappings = $fragment->getFragmentMappings( + $request->getUser(), + $fragment->getPath()); + + $file_phids = mpull(mpull($mappings, 'getLatestVersion'), 'getFilePHID'); + $files = id(new PhabricatorFileQuery()) + ->setViewer($request->getUser()) + ->withPHIDs($file_phids) + ->execute(); + $files = mpull($files, null, 'getPHID'); + + // Scan all of the files that the caller currently has and iterate + // over that. + foreach ($state as $path => $hash) { + // If $mappings[$path] exists, then the user has the file and it's + // also a fragment. + if (array_key_exists($path, $mappings)) { + $file_phid = $mappings[$path]->getLatestVersion()->getFilePHID(); + if ($file_phid !== null) { + // If the file PHID is present, then we need to check the + // hashes to see if they are the same. + $hash_caller = strtolower($state[$path]); + $hash_current = $files[$file_phid]->getContentHash(); + if ($hash_caller === $hash_current) { + // The user's version is identical to our version, so + // there is no update needed. + } else { + // The hash differs, and the user needs to update. + $patches[] = array( + 'path' => $path, + 'fileOld' => null, + 'fileNew' => $files[$file_phid], + 'hashOld' => $hash_caller, + 'hashNew' => $hash_current, + 'patchURI' => null); + } + } else { + // We have a record of this as a file, but there is no file + // attached to the latest version, so we consider this to be + // a deletion. + $patches[] = array( + 'path' => $path, + 'fileOld' => null, + 'fileNew' => null, + 'hashOld' => $hash_caller, + 'hashNew' => PhragmentPatchUtil::EMPTY_HASH, + 'patchURI' => null); + } + } else { + // If $mappings[$path] does not exist, then the user has a file, + // and we have absolutely no record of it what-so-ever (we haven't + // even recorded a deletion). Assuming most applications will store + // some form of data near their own files, this is probably a data + // file relevant for the application that is not versioned, so we + // don't tell the client to do anything with it. + } + } + + // Check the remaining files that we know about but the caller has + // not reported. + foreach ($mappings as $path => $child) { + if (array_key_exists($path, $state)) { + // We have already evaluated this above. + } else { + $file_phid = $mappings[$path]->getLatestVersion()->getFilePHID(); + if ($file_phid !== null) { + // If the file PHID is present, then this is a new file that + // we know about, but the caller does not. We need to tell + // the caller to create the file. + $hash_current = $files[$file_phid]->getContentHash(); + $patches[] = array( + 'path' => $path, + 'fileOld' => null, + 'fileNew' => $files[$file_phid], + 'hashOld' => PhragmentPatchUtil::EMPTY_HASH, + 'hashNew' => $hash_current, + 'patchURI' => null); + } else { + // We have a record of deleting this file, and the caller hasn't + // reported it, so they've probably deleted it in a previous + // update. + } + } + } + + // Before we can calculate patches, we need to resolve the old versions + // of files so we can draw diffs on them. + $hashes = array(); + foreach ($patches as $patch) { + if ($patch["hashOld"] !== PhragmentPatchUtil::EMPTY_HASH) { + $hashes[] = $patch["hashOld"]; + } + } + $old_files = array(); + if (count($hashes) !== 0) { + $old_files = id(new PhabricatorFileQuery()) + ->setViewer($request->getUser()) + ->withContentHashes($hashes) + ->execute(); + } + $old_files = mpull($old_files, null, 'getContentHash'); + foreach ($patches as $key => $patch) { + if ($patch["hashOld"] !== PhragmentPatchUtil::EMPTY_HASH) { + if (array_key_exists($patch['hashOld'], $old_files)) { + $patches[$key]['fileOld'] = $old_files[$patch['hashOld']]; + } else { + // We either can't see or can't read the old file. + $patches[$key]['hashOld'] = PhragmentPatchUtil::EMPTY_HASH; + $patches[$key]['fileOld'] = null; + } + } + } + + // Now run through all of the patch entries, calculate the patches + // and return the results. + foreach ($patches as $key => $patch) { + $data = PhragmentPatchUtil::calculatePatch( + $patches[$key]['fileOld'], + $patches[$key]['fileNew']); + unset($patches[$key]['fileOld']); + unset($patches[$key]['fileNew']); + + $file = PhabricatorFile::buildFromFileDataOrHash( + $data, + array( + 'name' => 'patch.dmp', + 'ttl' => time() + 60 * 60 * 24, + )); + $patches[$key]['patchURI'] = $file->getDownloadURI(); + } + + return $patches; + } + +} Index: src/applications/phragment/conduit/ConduitAPI_phragment_queryfragments_Method.php =================================================================== --- /dev/null +++ src/applications/phragment/conduit/ConduitAPI_phragment_queryfragments_Method.php @@ -0,0 +1,83 @@ + 'required list', + ); + } + + public function defineReturnType() { + return 'nonempty dict'; + } + + public function defineErrorTypes() { + return array( + 'ERR_BAD_FRAGMENT' => 'No such fragment exists', + ); + } + + protected function execute(ConduitAPIRequest $request) { + $paths = $request->getValue('paths'); + + $fragments = id(new PhragmentFragmentQuery()) + ->setViewer($request->getUser()) + ->withPaths($paths) + ->execute(); + $fragments = mpull($fragments, null, 'getPath'); + foreach ($paths as $path) { + if (!array_key_exists($path, $fragments)) { + throw new ConduitException('ERR_BAD_FRAGMENT'); + } + } + + $results = array(); + foreach ($fragments as $path => $fragment) { + $mappings = $fragment->getFragmentMappings( + $request->getUser(), + $fragment->getPath()); + + $file_phids = mpull(mpull($mappings, 'getLatestVersion'), 'getFilePHID'); + $files = id(new PhabricatorFileQuery()) + ->setViewer($request->getUser()) + ->withPHIDs($file_phids) + ->execute(); + $files = mpull($files, null, 'getPHID'); + + $result = array(); + foreach ($mappings as $cpath => $child) { + $file_phid = $child->getLatestVersion()->getFilePHID(); + if (!isset($files[$file_phid])) { + // Skip any files we don't have permission to access. + continue; + } + + $file = $files[$file_phid]; + $cpath = substr($child->getPath(), strlen($fragment->getPath()) + 1); + $result[] = array( + 'phid' => $child->getPHID(), + 'phidVersion' => $child->getLatestVersionPHID(), + 'path' => $cpath, + 'hash' => $file->getContentHash(), + 'version' => $child->getLatestVersion()->getSequence(), + 'uri' => $file->getViewURI()); + } + $results[$path] = $result; + } + return $results; + } + +} Index: src/applications/phragment/controller/PhragmentZIPController.php =================================================================== --- src/applications/phragment/controller/PhragmentZIPController.php +++ src/applications/phragment/controller/PhragmentZIPController.php @@ -111,29 +111,18 @@ * Returns a list of mappings like array('some/path.txt' => 'file PHID'); */ private function getFragmentMappings(PhragmentFragment $current, $base_path) { - $children = id(new PhragmentFragmentQuery()) - ->setViewer($this->getRequest()->getUser()) - ->needLatestVersion(true) - ->withLeadingPath($current->getPath().'/') - ->withDepths(array($current->getDepth() + 1)) - ->execute(); - - if (count($children) === 0) { - $path = substr($current->getPath(), strlen($base_path) + 1); - if ($this->getVersion($current) === null) { - return array(); - } - return array($path => $this->getVersion($current)->getFilePHID()); - } else { - $mappings = array(); - foreach ($children as $child) { - $child_mappings = $this->getFragmentMappings($child, $base_path); - foreach ($child_mappings as $key => $value) { - $mappings[$key] = $value; - } + $mappings = $current->getFragmentMappings( + $this->getRequest()->getUser(), + $base_path); + + $result = array(); + foreach ($mappings as $path => $fragment) { + $version = $this->getVersion($fragment); + if ($version !== null) { + $result[$path] = $version->getFilePHID(); } - return $mappings; } + return $result; } private function getVersion($fragment) { Index: src/applications/phragment/storage/PhragmentFragment.php =================================================================== --- src/applications/phragment/storage/PhragmentFragment.php +++ src/applications/phragment/storage/PhragmentFragment.php @@ -276,6 +276,38 @@ } +/* -( Utility ) ---------------------------------------------------------- */ + + + public function getFragmentMappings( + PhabricatorUser $viewer, + $base_path) { + + $children = id(new PhragmentFragmentQuery()) + ->setViewer($viewer) + ->needLatestVersion(true) + ->withLeadingPath($this->getPath().'/') + ->withDepths(array($this->getDepth() + 1)) + ->execute(); + + if (count($children) === 0) { + $path = substr($this->getPath(), strlen($base_path) + 1); + return array($path => $this); + } else { + $mappings = array(); + foreach ($children as $child) { + $child_mappings = $child->getFragmentMappings( + $viewer, + $base_path); + foreach ($child_mappings as $key => $value) { + $mappings[$key] = $value; + } + } + return $mappings; + } + } + + /* -( Policy Interface )--------------------------------------------------- */