<?php
/**
* Perforce Swarm
*
* @copyright 2013 Perforce Software. All rights reserved.
* @license Please see LICENSE.txt in top-level folder of this distribution.
* @version <release>/<patch>
*/
namespace Record\File;
use P4\Connection\Exception\CommandException;
use P4\File\File;
use P4\Model\Connected\ConnectedAbstract;
/**
* Simplified handler for reading and writing files to a special depot storage location.
*/
class FileService extends ConnectedAbstract
{
protected $config;
/**
* Retrieve contents from a file in the depot
*
* @param string $filespec file location (either absolute depot path or relative to base_path)
* @return string the contents of the file
*/
public function read($filespec)
{
return $this->getFile($filespec)->getDepotContents();
}
/**
* Stream the contents to STDOUT
*
* @param string $filespec file location (either absolute depot path or relative to base_path)
* @return File a file instance
*/
public function stream($filespec)
{
return $this->getFile($filespec)->streamDepotContents();
}
/**
* Manipulate a file in the depot using an anonymous function. This is used for writing from strings and
* local files, and also for deleting from the depot.
*
* Example invocation:
*
* $this->manipulateFile(
* $filespec,
* function ($file) use ($filespec) {
* $file->delete();
* return "Deleted: " . $filespec;
* }
* );
*
* The returned string is used for the submit message when the changes are applied.
*
* @param string $filespec full or partial p4 filespec (partial filespecs will be absolutized)
* @param \Closure $callback anonymous function that accepts a $file parameter and performs some action on it.
* must return a string to use as a submit message.
*/
protected function manipulateFile($filespec, \Closure $callback)
{
$p4 = $this->getConnection();
$pool = $p4->getService('clients');
$pool->grab();
try {
$pool->reset(true);
$file = new File($p4);
$file->setFilespec($this->absolutize($filespec));
$message = $callback($file);
$file->submit($message);
} catch (\Exception $e) {
}
try {
$pool->clearFiles();
} catch (\Exception $clearFilesException) {
}
$pool->release();
// exceptions in the callback take priority over clearFiles exceptions
if (isset($e)) {
throw $e;
}
if (isset($clearFilesException)) {
throw $clearFilesException;
}
}
/**
* Write raw data to a file in the Depot
*
* @param string $filespec file location (either absolute depot path or relative to base_path)
* @param string $data the data to be written
*/
public function write($filespec, $data)
{
return $this->manipulateFile(
$filespec,
function ($file) use ($data, $filespec) {
$file->setLocalContents($data);
// use depot path to check for existence vs file location (could be local)
if (!File::exists($file->getFilespec())) {
$file->add();
return "Added: " . $filespec;
}
$file->sync(false, true);
$file->edit();
return "Edited: " . $filespec;
}
);
}
/**
* Copies the specified file to a local client workspace, then writes it to the depot.
*
* @param string $filespec file location (either absolute depot path or relative to base_path)
* @param string $location the location of the file on the local filesystem
* @param bool $move optional - move the file from $location to the active client (default: false)
*/
public function writeFromFile($filespec, $location, $move = false)
{
return $this->manipulateFile(
$filespec,
function ($file) use ($filespec, $location, $move) {
$localFilename = $file->getLocalFilename();
$file->createLocalPath();
if ($move && !@rename($location, $localFilename)) {
throw new \RuntimeException("Unable to move file: " . $location);
}
if (!$move && !@copy($location, $localFilename)) {
throw new \RuntimeException("Unable to copy file: " . $location);
}
if (!File::exists($file->getFilespec())) {
$file->add();
return "Added: " . $filespec;
}
$file->sync(false, true);
$file->edit();
return "Edited: " . $filespec;
}
);
}
/**
* Delete a file in the Depot
*
* @param string $filespec file location (either absolute depot path or relative to base_path)
*/
public function delete($filespec)
{
return $this->manipulateFile(
$filespec,
function ($file) use ($filespec) {
$file->delete();
return "Deleted: " . $filespec;
}
);
}
/**
* Get an instance of File from the Depot
*
* @param string $filespec file location (either absolute depot path or relative to base_path)
* @return File a file instance
* @throws P4\File\Exception\NotFoundException;
*/
public function getFile($filespec)
{
$path = $this->absolutize($filespec);
return File::fetch($path, $this->getConnection(), true);
}
/**
* Take a filespec and resolve it to a absolute location in the depot.
* If the filespec is already absolute, it will be returned as-is.
*
* @param string $filespec file location (either absolute depot path or relative to base_path)
* @return string the full path to the depot location of the file
*/
public function absolutize($filespec)
{
if (is_null($filespec) || $filespec === '' || !strlen(trim($filespec, '/'))) {
throw new \InvalidArgumentException('FileService::absolutize($filespec) requires a non-empty filespec');
}
if (substr($filespec, 0, 2) == '//') {
$path = $filespec;
} else {
$path = $this->getBasePath() . '/' . $filespec;
}
return $path;
}
/**
* Configure the file service.
*
* Expects an array:
*
* array(
* 'base_path' => '//.swarm'
* )
*
* @param $config array containing 'base_path' key for storage location
*/
public function setConfig($config)
{
$this->config = $config;
}
/**
* @return array the file service configuration array
*/
public function getConfig()
{
return (array) $this->config + array('base_path' => null);
}
/**
* Get the base location for writing files to.
*
* @return string the base depot path (defaults to "//.swarm" if not set)
*/
public function getBasePath()
{
if (!isset($this->config['base_path']) || strlen(trim($this->config['base_path'], '/')) == 0) {
throw new \Exception('Administrator must set $config[\'depot_storage\'][\'base_path\']');
}
return rtrim($this->config['base_path'], '/');
}
/**
* Check if given $path is writable
*
* @param string $path the path to check for writability
* @return bool whether $path is writable or not
*/
public function isWritable($path)
{
try {
$result = $this->getConnection()->run("protects", array("-m", $this->absolutize($path)));
} catch (CommandException $e) {
if (strpos($e->getMessage(), 'must refer to client')) {
return false;
}
if (strpos($e->getMessage(), 'Protections table is empty')) {
return true;
}
throw $e;
}
return in_array($result->getData(0, "permMax"), array('write', 'super', 'admin'));
}
}