@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement support for creating and updating fragments from ZIPs

Summary:
This implements support for creating and updating fragments from ZIP files. It allows you to upload a ZIP via the Files application, create a fragment from it, and have it recursively imported into Phragment. Updating that folder with another ZIP will recursively create, update and delete files as appropriate.

The logic for creating and updating fragments from files has also been centralized into the PhragmentFragment class. Directories are also now supported; a directory fragment is simply a fragment that has no patches; thus a directory fragment can be converted to a file fragment by uploading a first patch for it.

Test Plan: Uploaded ZIP files through the interface and saw all of the fragments get created and updated as expected.

Reviewers: epriestley, #blessed_reviewers

Reviewed By: epriestley

CC: Korvin, epriestley, aran

Maniphest Tasks: T4205

Differential Revision: https://secure.phabricator.com/D7729

+270 -55
+2
resources/sql/patches/20131206.phragmentnull.sql
··· 1 + ALTER TABLE {$NAMESPACE}_phragment.phragment_fragment 2 + MODIFY latestVersionPHID VARCHAR(64) NULL;
+12 -8
src/applications/phragment/controller/PhragmentBrowseController.php
··· 57 57 $item = id(new PHUIObjectItemView()); 58 58 $item->setHeader($fragment->getName()); 59 59 $item->setHref($this->getApplicationURI('/browse/'.$fragment->getPath())); 60 - $item->addAttribute(pht( 61 - 'Last Updated %s', 62 - phabricator_datetime( 63 - $fragment->getLatestVersion()->getDateCreated(), 64 - $viewer))); 65 - $item->addAttribute(pht( 66 - 'Latest Version %s', 67 - $fragment->getLatestVersion()->getSequence())); 60 + if (!$fragment->isDirectory()) { 61 + $item->addAttribute(pht( 62 + 'Last Updated %s', 63 + phabricator_datetime( 64 + $fragment->getLatestVersion()->getDateCreated(), 65 + $viewer))); 66 + $item->addAttribute(pht( 67 + 'Latest Version %s', 68 + $fragment->getLatestVersion()->getSequence())); 69 + } else { 70 + $item->addAttribute('Directory'); 71 + } 68 72 $list->addItem($item); 69 73 } 70 74
+36 -15
src/applications/phragment/controller/PhragmentController.php
··· 66 66 67 67 $this->loadHandles($phids); 68 68 69 - $file = id(new PhabricatorFileQuery()) 70 - ->setViewer($viewer) 71 - ->withPHIDs(array($fragment->getLatestVersion()->getFilePHID())) 72 - ->executeOne(); 69 + $file = null; 73 70 $file_uri = null; 74 - if ($file !== null) { 75 - $file_uri = $file->getBestURI(); 71 + if (!$fragment->isDirectory()) { 72 + $file = id(new PhabricatorFileQuery()) 73 + ->setViewer($viewer) 74 + ->withPHIDs(array($fragment->getLatestVersion()->getFilePHID())) 75 + ->executeOne(); 76 + if ($file !== null) { 77 + $file_uri = $file->getBestURI(); 78 + } 76 79 } 77 80 78 81 $header = id(new PHUIHeaderView()) ··· 96 99 ->setHref($this->getApplicationURI("zip/".$fragment->getPath())) 97 100 ->setDisabled(false) // TODO: Policy 98 101 ->setIcon('zip')); 99 - $actions->addAction( 100 - id(new PhabricatorActionView()) 101 - ->setName(pht('Update Fragment')) 102 - ->setHref($this->getApplicationURI("update/".$fragment->getPath())) 103 - ->setDisabled(false) // TODO: Policy 104 - ->setIcon('edit')); 102 + if (!$fragment->isDirectory()) { 103 + $actions->addAction( 104 + id(new PhabricatorActionView()) 105 + ->setName(pht('Update Fragment')) 106 + ->setHref($this->getApplicationURI("update/".$fragment->getPath())) 107 + ->setDisabled(false) // TODO: Policy 108 + ->setIcon('edit')); 109 + } else { 110 + $actions->addAction( 111 + id(new PhabricatorActionView()) 112 + ->setName(pht('Convert to File')) 113 + ->setHref($this->getApplicationURI("update/".$fragment->getPath())) 114 + ->setDisabled(false) // TODO: Policy 115 + ->setIcon('edit')); 116 + } 105 117 if ($is_history_view) { 106 118 $actions->addAction( 107 119 id(new PhabricatorActionView()) ··· 121 133 ->setObject($fragment) 122 134 ->setActionList($actions); 123 135 124 - $properties->addProperty( 125 - pht('Latest Version'), 126 - $this->renderHandlesForPHIDs(array($fragment->getLatestVersionPHID()))); 136 + if (!$fragment->isDirectory()) { 137 + $properties->addProperty( 138 + pht('Type'), 139 + pht('File')); 140 + $properties->addProperty( 141 + pht('Latest Version'), 142 + $this->renderHandlesForPHIDs(array($fragment->getLatestVersionPHID()))); 143 + } else { 144 + $properties->addProperty( 145 + pht('Type'), 146 + pht('Directory')); 147 + } 127 148 128 149 return id(new PHUIObjectBoxView()) 129 150 ->setHeader($header)
+6 -15
src/applications/phragment/controller/PhragmentCreateController.php
··· 54 54 $depth = $parent->getDepth() + 1; 55 55 } 56 56 57 - $version = id(new PhragmentFragmentVersion()); 58 - $version->setSequence(0); 59 - $version->setFragmentPHID(''); // Can't set this yet... 60 - $version->setFilePHID($file->getPHID()); 61 - $version->save(); 62 - 63 - $fragment->setPath(trim($parent_path.'/'.$v_name, '/')); 64 - $fragment->setDepth($depth); 65 - $fragment->setLatestVersionPHID($version->getPHID()); 66 - $fragment->setViewPolicy($v_viewpolicy); 67 - $fragment->setEditPolicy($v_editpolicy); 68 - $fragment->save(); 69 - 70 - $version->setFragmentPHID($fragment->getPHID()); 71 - $version->save(); 57 + PhragmentFragment::createFromFile( 58 + $viewer, 59 + $file, 60 + trim($parent_path.'/'.$v_name, '/'), 61 + $v_viewpolicy, 62 + $v_editpolicy); 72 63 73 64 return id(new AphrontRedirectResponse()) 74 65 ->setURI('/phragment/browse/'.trim($parent_path.'/'.$v_name, '/'));
+8 -16
src/applications/phragment/controller/PhragmentUpdateController.php
··· 31 31 } 32 32 33 33 if (!count($errors)) { 34 - $existing = id(new PhragmentFragmentVersionQuery()) 35 - ->setViewer($viewer) 36 - ->withFragmentPHIDs(array($fragment->getPHID())) 37 - ->execute(); 38 - $sequence = count($existing); 39 - 40 - $fragment->openTransaction(); 41 - $version = id(new PhragmentFragmentVersion()); 42 - $version->setSequence($sequence); 43 - $version->setFragmentPHID($fragment->getPHID()); 44 - $version->setFilePHID($file->getPHID()); 45 - $version->save(); 46 - 47 - $fragment->setLatestVersionPHID($version->getPHID()); 48 - $fragment->save(); 49 - $fragment->saveTransaction(); 34 + // If the file is a ZIP archive (has application/zip mimetype) 35 + // then we extract the zip and apply versions for each of the 36 + // individual fragments, creating and deleting files as needed. 37 + if ($file->getMimeType() === "application/zip") { 38 + $fragment->updateFromZIP($viewer, $file); 39 + } else { 40 + $fragment->updateFromFile($viewer, $file); 41 + } 50 42 51 43 return id(new AphrontRedirectResponse()) 52 44 ->setURI('/phragment/browse/'.$fragment->getPath());
-1
src/applications/phragment/query/PhragmentFragmentQuery.php
··· 115 115 foreach ($page as $key => $fragment) { 116 116 $version_phid = $fragment->getLatestVersionPHID(); 117 117 if (empty($versions[$version_phid])) { 118 - unset($page[$key]); 119 118 continue; 120 119 } 121 120 $fragment->attachLatestVersion($versions[$version_phid]);
+202
src/applications/phragment/storage/PhragmentFragment.php
··· 38 38 return $this->file = $file; 39 39 } 40 40 41 + public function isDirectory() { 42 + return $this->latestVersionPHID === null; 43 + } 44 + 41 45 public function getLatestVersion() { 46 + if ($this->latestVersionPHID === null) { 47 + return null; 48 + } 42 49 return $this->assertAttached($this->latestVersion); 43 50 } 44 51 45 52 public function attachLatestVersion(PhragmentFragmentVersion $version) { 46 53 return $this->latestVersion = $version; 47 54 } 55 + 56 + 57 + /* -( Updating ) --------------------------------------------------------- */ 58 + 59 + 60 + /** 61 + * Create a new fragment from a file. 62 + */ 63 + public static function createFromFile( 64 + PhabricatorUser $viewer, 65 + PhabricatorFile $file = null, 66 + $path, 67 + $view_policy, 68 + $edit_policy) { 69 + 70 + $fragment = id(new PhragmentFragment()); 71 + $fragment->setPath($path); 72 + $fragment->setDepth(count(explode('/', $path))); 73 + $fragment->setLatestVersionPHID(null); 74 + $fragment->setViewPolicy($view_policy); 75 + $fragment->setEditPolicy($edit_policy); 76 + $fragment->save(); 77 + 78 + // Directory fragments have no versions associated with them, so we 79 + // just return the fragment at this point. 80 + if ($file === null) { 81 + return $fragment; 82 + } 83 + 84 + if ($file->getMimeType() === "application/zip") { 85 + $fragment->updateFromZIP($viewer, $file); 86 + } else { 87 + $fragment->updateFromFile($viewer, $file); 88 + } 89 + 90 + return $fragment; 91 + } 92 + 93 + 94 + /** 95 + * Set the specified file as the next version for the fragment. 96 + */ 97 + public function updateFromFile( 98 + PhabricatorUser $viewer, 99 + PhabricatorFile $file) { 100 + 101 + $existing = id(new PhragmentFragmentVersionQuery()) 102 + ->setViewer($viewer) 103 + ->withFragmentPHIDs(array($this->getPHID())) 104 + ->execute(); 105 + $sequence = count($existing); 106 + 107 + $this->openTransaction(); 108 + $version = id(new PhragmentFragmentVersion()); 109 + $version->setSequence($sequence); 110 + $version->setFragmentPHID($this->getPHID()); 111 + $version->setFilePHID($file->getPHID()); 112 + $version->save(); 113 + 114 + $this->setLatestVersionPHID($version->getPHID()); 115 + $this->save(); 116 + $this->saveTransaction(); 117 + } 118 + 119 + /** 120 + * Apply the specified ZIP archive onto the fragment, removing 121 + * and creating fragments as needed. 122 + */ 123 + public function updateFromZIP( 124 + PhabricatorUser $viewer, 125 + PhabricatorFile $file) { 126 + 127 + if ($file->getMimeType() !== "application/zip") { 128 + throw new Exception("File must have mimetype 'application/zip'"); 129 + } 130 + 131 + // First apply the ZIP as normal. 132 + $this->updateFromFile($viewer, $file); 133 + 134 + // Ensure we have ZIP support. 135 + $zip = null; 136 + try { 137 + $zip = new ZipArchive(); 138 + } catch (Exception $e) { 139 + // The server doesn't have php5-zip, so we can't do recursive updates. 140 + return; 141 + } 142 + 143 + $temp = new TempFile(); 144 + Filesystem::writeFile($temp, $file->loadFileData()); 145 + if (!$zip->open($temp)) { 146 + throw new Exception("Unable to open ZIP"); 147 + } 148 + 149 + // Get all of the paths and their data from the ZIP. 150 + $mappings = array(); 151 + for ($i = 0; $i < $zip->numFiles; $i++) { 152 + $path = trim($zip->getNameIndex($i), '/'); 153 + $stream = $zip->getStream($path); 154 + $data = null; 155 + // If the stream is false, then it is a directory entry. We leave 156 + // $data set to null for directories so we know not to create a 157 + // version entry for them. 158 + if ($stream !== false) { 159 + $data = stream_get_contents($stream); 160 + fclose($stream); 161 + } 162 + $mappings[$path] = $data; 163 + } 164 + 165 + // Adjust the paths relative to this fragment so we can look existing 166 + // fragments up in the DB. 167 + $base_path = $this->getPath(); 168 + $paths = array(); 169 + foreach ($mappings as $p => $data) { 170 + $paths[] = $base_path.'/'.$p; 171 + } 172 + 173 + // FIXME: What happens when a child exists, but the current user 174 + // can't see it. We're going to create a new child with the exact 175 + // same path and then bad things will happen. 176 + $children = id(new PhragmentFragmentQuery()) 177 + ->setViewer($viewer) 178 + ->needLatestVersion(true) 179 + ->withPaths($paths) 180 + ->execute(); 181 + $children = mpull($children, null, 'getPath'); 182 + 183 + // Iterate over the existing fragments. 184 + foreach ($children as $full_path => $child) { 185 + $path = substr($full_path, strlen($base_path) + 1); 186 + if (array_key_exists($path, $mappings)) { 187 + if ($child->isDirectory() && $mappings[$path] === null) { 188 + // Don't create a version entry for a directory 189 + // (unless it's been converted into a file). 190 + continue; 191 + } 192 + 193 + // The file is being updated. 194 + $file = PhabricatorFile::newFromFileData( 195 + $mappings[$path], 196 + array('name' => basename($path))); 197 + $child->updateFromFile($viewer, $file); 198 + } else { 199 + // The file is being deleted. 200 + $child->deleteFile($viewer); 201 + } 202 + } 203 + 204 + // Iterate over the mappings to find new files. 205 + foreach ($mappings as $path => $data) { 206 + if (!array_key_exists($base_path.'/'.$path, $children)) { 207 + // The file is being created. If the data is null, 208 + // then this is explicitly a directory being created. 209 + $file = null; 210 + if ($mappings[$path] !== null) { 211 + $file = PhabricatorFile::newFromFileData( 212 + $mappings[$path], 213 + array('name' => basename($path))); 214 + } 215 + PhragmentFragment::createFromFile( 216 + $viewer, 217 + $file, 218 + $base_path.'/'.$path, 219 + $this->getViewPolicy(), 220 + $this->getEditPolicy()); 221 + } 222 + } 223 + } 224 + 225 + /** 226 + * Delete the contents of the specified fragment. 227 + */ 228 + public function deleteFile(PhabricatorUser $viewer) { 229 + $existing = id(new PhragmentFragmentVersionQuery()) 230 + ->setViewer($viewer) 231 + ->withFragmentPHIDs(array($this->getPHID())) 232 + ->execute(); 233 + $sequence = count($existing); 234 + 235 + $this->openTransaction(); 236 + $version = id(new PhragmentFragmentVersion()); 237 + $version->setSequence($sequence); 238 + $version->setFragmentPHID($this->getPHID()); 239 + $version->setFilePHID(null); 240 + $version->save(); 241 + 242 + $this->setLatestVersionPHID($version->getPHID()); 243 + $this->save(); 244 + $this->saveTransaction(); 245 + } 246 + 247 + 248 + /* -( Policy Interface )--------------------------------------------------- */ 249 + 48 250 49 251 public function getCapabilities() { 50 252 return array(
+4
src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
··· 1820 1820 'type' => 'sql', 1821 1821 'name' => $this->getPatchPath('20131206.phragment.sql'), 1822 1822 ), 1823 + '20131206.phragmentnull.sql' => array( 1824 + 'type' => 'sql', 1825 + 'name' => $this->getPatchPath('20131206.phragmentnull.sql'), 1826 + ), 1823 1827 ); 1824 1828 } 1825 1829 }