2013-12-07 02:43:49 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
final class PhragmentFragment extends PhragmentDAO
|
|
|
|
implements PhabricatorPolicyInterface {
|
|
|
|
|
|
|
|
protected $path;
|
|
|
|
protected $depth;
|
|
|
|
protected $latestVersionPHID;
|
|
|
|
protected $viewPolicy;
|
|
|
|
protected $editPolicy;
|
|
|
|
|
|
|
|
private $latestVersion = self::ATTACHABLE;
|
|
|
|
|
|
|
|
public function getConfiguration() {
|
|
|
|
return array(
|
|
|
|
self::CONFIG_AUX_PHID => true,
|
2014-10-01 16:52:26 +02:00
|
|
|
self::CONFIG_COLUMN_SCHEMA => array(
|
|
|
|
'path' => 'text255',
|
|
|
|
'depth' => 'uint32',
|
|
|
|
'latestVersionPHID' => 'phid?',
|
|
|
|
),
|
|
|
|
self::CONFIG_KEY_SCHEMA => array(
|
|
|
|
'key_path' => array(
|
|
|
|
'columns' => array('path'),
|
|
|
|
'unique' => true,
|
|
|
|
),
|
|
|
|
),
|
2013-12-07 02:43:49 +01:00
|
|
|
) + parent::getConfiguration();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function generatePHID() {
|
|
|
|
return PhabricatorPHID::generateNewPHID(
|
2014-07-24 00:05:46 +02:00
|
|
|
PhragmentFragmentPHIDType::TYPECONST);
|
2013-12-07 02:43:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public function getURI() {
|
2013-12-08 22:24:50 +01:00
|
|
|
return '/phragment/browse/'.$this->getPath();
|
2013-12-07 02:43:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public function getName() {
|
|
|
|
return basename($this->path);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getFile() {
|
|
|
|
return $this->assertAttached($this->file);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function attachFile(PhabricatorFile $file) {
|
|
|
|
return $this->file = $file;
|
|
|
|
}
|
|
|
|
|
2013-12-07 03:37:18 +01:00
|
|
|
public function isDirectory() {
|
|
|
|
return $this->latestVersionPHID === null;
|
|
|
|
}
|
|
|
|
|
2013-12-08 21:52:24 +01:00
|
|
|
public function isDeleted() {
|
|
|
|
return $this->getLatestVersion()->getFilePHID() === null;
|
|
|
|
}
|
|
|
|
|
2013-12-07 02:43:49 +01:00
|
|
|
public function getLatestVersion() {
|
2013-12-07 03:37:18 +01:00
|
|
|
if ($this->latestVersionPHID === null) {
|
|
|
|
return null;
|
|
|
|
}
|
2013-12-07 02:43:49 +01:00
|
|
|
return $this->assertAttached($this->latestVersion);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function attachLatestVersion(PhragmentFragmentVersion $version) {
|
|
|
|
return $this->latestVersion = $version;
|
|
|
|
}
|
|
|
|
|
2013-12-07 03:37:18 +01:00
|
|
|
|
|
|
|
/* -( Updating ) --------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a new fragment from a file.
|
|
|
|
*/
|
|
|
|
public static function createFromFile(
|
|
|
|
PhabricatorUser $viewer,
|
|
|
|
PhabricatorFile $file = null,
|
|
|
|
$path,
|
|
|
|
$view_policy,
|
|
|
|
$edit_policy) {
|
|
|
|
|
|
|
|
$fragment = id(new PhragmentFragment());
|
|
|
|
$fragment->setPath($path);
|
|
|
|
$fragment->setDepth(count(explode('/', $path)));
|
|
|
|
$fragment->setLatestVersionPHID(null);
|
|
|
|
$fragment->setViewPolicy($view_policy);
|
|
|
|
$fragment->setEditPolicy($edit_policy);
|
|
|
|
$fragment->save();
|
|
|
|
|
|
|
|
// Directory fragments have no versions associated with them, so we
|
|
|
|
// just return the fragment at this point.
|
|
|
|
if ($file === null) {
|
|
|
|
return $fragment;
|
|
|
|
}
|
|
|
|
|
2014-06-09 20:36:49 +02:00
|
|
|
if ($file->getMimeType() === 'application/zip') {
|
2013-12-07 03:37:18 +01:00
|
|
|
$fragment->updateFromZIP($viewer, $file);
|
|
|
|
} else {
|
|
|
|
$fragment->updateFromFile($viewer, $file);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $fragment;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the specified file as the next version for the fragment.
|
|
|
|
*/
|
|
|
|
public function updateFromFile(
|
|
|
|
PhabricatorUser $viewer,
|
|
|
|
PhabricatorFile $file) {
|
|
|
|
|
|
|
|
$existing = id(new PhragmentFragmentVersionQuery())
|
|
|
|
->setViewer($viewer)
|
|
|
|
->withFragmentPHIDs(array($this->getPHID()))
|
|
|
|
->execute();
|
|
|
|
$sequence = count($existing);
|
|
|
|
|
|
|
|
$this->openTransaction();
|
|
|
|
$version = id(new PhragmentFragmentVersion());
|
|
|
|
$version->setSequence($sequence);
|
|
|
|
$version->setFragmentPHID($this->getPHID());
|
|
|
|
$version->setFilePHID($file->getPHID());
|
|
|
|
$version->save();
|
|
|
|
|
|
|
|
$this->setLatestVersionPHID($version->getPHID());
|
|
|
|
$this->save();
|
|
|
|
$this->saveTransaction();
|
2013-12-13 04:42:12 +01:00
|
|
|
|
2014-09-04 21:51:33 +02:00
|
|
|
$file->attachToObject($version->getPHID());
|
2013-12-07 03:37:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Apply the specified ZIP archive onto the fragment, removing
|
|
|
|
* and creating fragments as needed.
|
|
|
|
*/
|
|
|
|
public function updateFromZIP(
|
|
|
|
PhabricatorUser $viewer,
|
|
|
|
PhabricatorFile $file) {
|
|
|
|
|
2014-06-09 20:36:49 +02:00
|
|
|
if ($file->getMimeType() !== 'application/zip') {
|
2013-12-07 03:37:18 +01:00
|
|
|
throw new Exception("File must have mimetype 'application/zip'");
|
|
|
|
}
|
|
|
|
|
|
|
|
// First apply the ZIP as normal.
|
|
|
|
$this->updateFromFile($viewer, $file);
|
|
|
|
|
|
|
|
// Ensure we have ZIP support.
|
|
|
|
$zip = null;
|
|
|
|
try {
|
|
|
|
$zip = new ZipArchive();
|
|
|
|
} catch (Exception $e) {
|
|
|
|
// The server doesn't have php5-zip, so we can't do recursive updates.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$temp = new TempFile();
|
|
|
|
Filesystem::writeFile($temp, $file->loadFileData());
|
|
|
|
if (!$zip->open($temp)) {
|
2014-06-09 20:36:49 +02:00
|
|
|
throw new Exception('Unable to open ZIP');
|
2013-12-07 03:37:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get all of the paths and their data from the ZIP.
|
|
|
|
$mappings = array();
|
|
|
|
for ($i = 0; $i < $zip->numFiles; $i++) {
|
|
|
|
$path = trim($zip->getNameIndex($i), '/');
|
|
|
|
$stream = $zip->getStream($path);
|
|
|
|
$data = null;
|
2014-07-10 00:12:48 +02:00
|
|
|
// If the stream is false, then it is a directory entry. We leave
|
2013-12-07 03:37:18 +01:00
|
|
|
// $data set to null for directories so we know not to create a
|
|
|
|
// version entry for them.
|
|
|
|
if ($stream !== false) {
|
|
|
|
$data = stream_get_contents($stream);
|
|
|
|
fclose($stream);
|
|
|
|
}
|
|
|
|
$mappings[$path] = $data;
|
|
|
|
}
|
|
|
|
|
2013-12-08 02:10:00 +01:00
|
|
|
// We need to detect any directories that are in the ZIP folder that
|
2014-07-10 00:12:48 +02:00
|
|
|
// aren't explicitly noted in the ZIP. This can happen if the file
|
2013-12-08 02:10:00 +01:00
|
|
|
// entries in the ZIP look like:
|
|
|
|
//
|
|
|
|
// * something/blah.png
|
|
|
|
// * something/other.png
|
|
|
|
// * test.png
|
|
|
|
//
|
|
|
|
// Where there is no explicit "something/" entry.
|
|
|
|
foreach ($mappings as $path_key => $data) {
|
|
|
|
if ($data === null) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
$directory = dirname($path_key);
|
2014-06-09 20:36:49 +02:00
|
|
|
while ($directory !== '.') {
|
2013-12-08 02:10:00 +01:00
|
|
|
if (!array_key_exists($directory, $mappings)) {
|
|
|
|
$mappings[$directory] = null;
|
|
|
|
}
|
|
|
|
if (dirname($directory) === $directory) {
|
|
|
|
// dirname() will not reduce this directory any further; to
|
|
|
|
// prevent infinite loop we just break out here.
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
$directory = dirname($directory);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-12-07 03:37:18 +01:00
|
|
|
// Adjust the paths relative to this fragment so we can look existing
|
|
|
|
// fragments up in the DB.
|
|
|
|
$base_path = $this->getPath();
|
|
|
|
$paths = array();
|
|
|
|
foreach ($mappings as $p => $data) {
|
|
|
|
$paths[] = $base_path.'/'.$p;
|
|
|
|
}
|
|
|
|
|
|
|
|
// FIXME: What happens when a child exists, but the current user
|
2014-07-10 00:12:48 +02:00
|
|
|
// can't see it. We're going to create a new child with the exact
|
2013-12-07 03:37:18 +01:00
|
|
|
// same path and then bad things will happen.
|
|
|
|
$children = id(new PhragmentFragmentQuery())
|
|
|
|
->setViewer($viewer)
|
|
|
|
->needLatestVersion(true)
|
2013-12-08 21:48:54 +01:00
|
|
|
->withLeadingPath($this->getPath().'/')
|
2013-12-07 03:37:18 +01:00
|
|
|
->execute();
|
|
|
|
$children = mpull($children, null, 'getPath');
|
|
|
|
|
|
|
|
// Iterate over the existing fragments.
|
|
|
|
foreach ($children as $full_path => $child) {
|
|
|
|
$path = substr($full_path, strlen($base_path) + 1);
|
|
|
|
if (array_key_exists($path, $mappings)) {
|
|
|
|
if ($child->isDirectory() && $mappings[$path] === null) {
|
|
|
|
// Don't create a version entry for a directory
|
|
|
|
// (unless it's been converted into a file).
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// The file is being updated.
|
|
|
|
$file = PhabricatorFile::newFromFileData(
|
|
|
|
$mappings[$path],
|
|
|
|
array('name' => basename($path)));
|
|
|
|
$child->updateFromFile($viewer, $file);
|
|
|
|
} else {
|
|
|
|
// The file is being deleted.
|
|
|
|
$child->deleteFile($viewer);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Iterate over the mappings to find new files.
|
|
|
|
foreach ($mappings as $path => $data) {
|
|
|
|
if (!array_key_exists($base_path.'/'.$path, $children)) {
|
2014-07-10 00:12:48 +02:00
|
|
|
// The file is being created. If the data is null,
|
2013-12-07 03:37:18 +01:00
|
|
|
// then this is explicitly a directory being created.
|
|
|
|
$file = null;
|
|
|
|
if ($mappings[$path] !== null) {
|
|
|
|
$file = PhabricatorFile::newFromFileData(
|
|
|
|
$mappings[$path],
|
|
|
|
array('name' => basename($path)));
|
|
|
|
}
|
|
|
|
PhragmentFragment::createFromFile(
|
|
|
|
$viewer,
|
|
|
|
$file,
|
|
|
|
$base_path.'/'.$path,
|
|
|
|
$this->getViewPolicy(),
|
|
|
|
$this->getEditPolicy());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete the contents of the specified fragment.
|
|
|
|
*/
|
|
|
|
public function deleteFile(PhabricatorUser $viewer) {
|
|
|
|
$existing = id(new PhragmentFragmentVersionQuery())
|
|
|
|
->setViewer($viewer)
|
|
|
|
->withFragmentPHIDs(array($this->getPHID()))
|
|
|
|
->execute();
|
|
|
|
$sequence = count($existing);
|
|
|
|
|
|
|
|
$this->openTransaction();
|
|
|
|
$version = id(new PhragmentFragmentVersion());
|
|
|
|
$version->setSequence($sequence);
|
|
|
|
$version->setFragmentPHID($this->getPHID());
|
|
|
|
$version->setFilePHID(null);
|
|
|
|
$version->save();
|
|
|
|
|
|
|
|
$this->setLatestVersionPHID($version->getPHID());
|
|
|
|
$this->save();
|
|
|
|
$this->saveTransaction();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
Provide `phragment.getstate` and `phragment.getpatch` Conduit methods
Summary:
This provides a `phragment.getstate` and a `phragment.getpatch` Conduit method.
`phragment.getstate` - This returns the current state of the fragment and all of it's children.
`phragment.getpatch` - This accepts a base path and a mapping of paths to hashes. The mapping is for the caller to specify the current state of the files it has. This returns a list of patches that the caller needs to apply to it's files to get to the latest version.
Test Plan:
Ran the following script in a folder which had content matching a fragment and it's children:
```
#!/bin/bash
STATE=""
for i in $(find ./ -type f); do
HASH=$(cat $i | sha1sum | awk '{ print $1 }')
BASE=${i:2}
STATE="$STATE,\"$BASE\":\"$HASH\""
done
STATE=${STATE:1}
STATE="{$STATE}"
echo '{"path":"tychaia3.zip","state":'$STATE'}' | arc --conduit-uri=http://phabricator.local/ call-conduit phragment.getpatch
```
and I got:
```
{"error":null,"errorMessage":null,"response":[]}
```
I updated one of the child fragments with a new file and ran the script again (patch has been omitted due to it's size):
```
{"error":null,"errorMessage":null,"response":[{"path":"Content\/TitleFont.xnb","hash_old":"4a927d7b90582e50cdd330de9f4b59b0cc5eb5c7","hash_new":"25867504642a3a403102274c68fbb9b430c1980f","patch":"..."}]}
```
Reviewers: epriestley, #blessed_reviewers
Reviewed By: epriestley
CC: Korvin, epriestley, aran, staticshock
Maniphest Tasks: T4205
Differential Revision: https://secure.phabricator.com/D7739
2013-12-11 01:19:23 +01:00
|
|
|
/* -( 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-12-07 03:37:18 +01:00
|
|
|
/* -( Policy Interface )--------------------------------------------------- */
|
|
|
|
|
|
|
|
|
2013-12-07 02:43:49 +01:00
|
|
|
public function getCapabilities() {
|
|
|
|
return array(
|
|
|
|
PhabricatorPolicyCapability::CAN_VIEW,
|
|
|
|
PhabricatorPolicyCapability::CAN_EDIT,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getPolicy($capability) {
|
|
|
|
switch ($capability) {
|
|
|
|
case PhabricatorPolicyCapability::CAN_VIEW:
|
|
|
|
return $this->getViewPolicy();
|
|
|
|
case PhabricatorPolicyCapability::CAN_EDIT:
|
|
|
|
return $this->getEditPolicy();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function describeAutomaticCapability($capability) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|