Volatile.php #1

  • //
  • guest/
  • perforce_software/
  • chronicle/
  • main/
  • library/
  • P4Cms/
  • Record/
  • Volatile.php
  • View
  • Commits
  • Open Download .zip Download (11 KB)
<?php
/**
 * Provides volatile (unsubmitted/unversioned) storage in Perforce.
 * This works by creating pending files and setting open attributes
 * on those files. This storage is useful for casual data that has
 * high turn-over or that we simply don't want to make permanent.
 *
 * Opened attributes can only be viewed by the client workspace that
 * set them. Therefore, this class introduces get/setClientMasquerade()
 * methods that cause it to masquerade as another client workspace
 * when executing Perforce commands.
 *
 * Volatile records have many limitations:
 *  - much simpler than regular records
 *  - no support for file content fields
 *  - no support for fetching multiple records
 *  - only string values are supported
 *  - no auto-generation of ids
 *  - no lazy-loading
 *
 * @copyright   2011 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
 */
class P4Cms_Record_Volatile extends P4Cms_Record_Connected
{
    const       CLIENT              = 'volatileClient';

    protected   $_clientMasquerade  = null;
    protected   $_depotFile         = null;
    protected   $_originalClient    = null;
    protected   $_originalHost      = null;
    protected   $_storageSubPath    = null;

    /**
     * Fetch a stored volatile record by id.
     *
     * @param   string                  $id             the id of the record to fetch
     * @param   P4Cms_Record_Adapter    $adapter        the storage adapter to use
     * @param   P4_Client|string|null   $masquerade     the client to masquerade as
     * @return  P4Cms_Record_Volatile   the populated volatile record
     * @throws  P4Cms_Record_NotFoundException          if no matching record can be found.
     */
    public static function fetch($id, $adapter, $masquerade = null)
    {
        $record = new static;
        $record->setId($id)
               ->setAdapter($adapter)
               ->setClientMasquerade($masquerade)
               ->_populate();

        return $record;
    }

    /**
     * Check for the existance of a volatile record by id.
     *
     * @param   string                  $id             the id of the record to lookup
     * @param   P4Cms_Record_Adapter    $adapter        the storage adapter to use
     * @param   P4_Client|string|null   $masquerade     the client to masquerade as
     * @return  bool                    true if the record exists; false otherwise.
     */
    public static function exists($id, $adapter, $masquerade = null)
    {
        try {
            static::fetch($id, $adapter, $masquerade);
            return true;
        } catch (P4Cms_Record_NotFoundException $e) {
            return false;
        }
    }

    /**
     * Set the client workspace to masquerade as.
     *
     * @param   P4_Client|string|null   $client     the client workspace to masquerade as
     * @return  P4Cms_Record_Volatile   provides fluent interface.
     */
    public function setClientMasquerade($client)
    {
        // upgrade client to a client object.
        $client = (!$client instanceof P4_Client && !is_null($client))
            ? P4_Client::fetch($client, $this->getAdapter()->getConnection())
            : $client;

        $this->_clientMasquerade = $client;

        return $this;
    }

    /**
     * Get the client workspace we are set to masquerade as.
     * If no masquerade client has been explicitly set we will
     * fetch/create one based on the presence of a 'volatileClient'
     * property on the storage adapter.
     *
     * @return  P4_Client|null  the client workspace to masquerade as.
     */
    public function getClientMasquerade()
    {
        // if we already have a client that we're masquerading as, return it.
        if ($this->_clientMasquerade) {
            return $this->_clientMasquerade;
        }

        // the adapter may specify a volatile client property (the site
        // branch object does this), if we don't have one early exit.
        $adapter = $this->getAdapter();
        if (!$adapter->hasProperty(static::CLIENT)) {
            return null;
        }

        $p4       = $adapter->getConnection();
        $clientId = $adapter->getProperty(static::CLIENT);

        // try to fetch the volatile client - if it does not exist, create one
        // based on the adapter's existing client (ie. same view/stream).
        try {
            $client = P4_Client::fetch($clientId, $p4);
        } catch (P4_Spec_NotFoundException $e) {
            $client = P4_Client::fetch($p4->getClient(), $p4);
            $client->setId($clientId)
                   ->setDescription("Chronicle generated 'volatile' record client.")
                   ->setRoot(P4_Environment::isWindows() ? "NUL" : "/dev/null")
                   ->touchUpView()
                   ->save();
        }

        $this->_clientMasquerade = $client;

        return $this->_clientMasquerade;
    }

    /**
     * Save the values in this volatile record to Perforce.
     * Note that the values are not submitted, only pended.
     *
     * @return  P4Cms_Record_Volatile   provides fluent interface.
     */
    public function save()
    {
        $file = $this->_getDepotFile();

        // masquerade as another client
        $this->_beginCharade();

        try {
            // ensure pending file is opened for add (or edit)
            // we use flush and -t/k to avoid touching files on disk.
            $this->_runFree('flush', $file);
            $this->_runFree('add',   array('-t',  'text', $file));
            $this->_runFree('edit',  array('-kt', 'text', $file));

            $params = array();
            foreach ($this->_values as $key => $value) {
                $params[] = "-n";
                $params[] = $key;
                $params[] = "-v";
                $params[] = (string) $value;
            }
            $params[] = $file;

            $this->getAdapter()->getConnection()->run('attribute', $params);
        } catch (Exception $e) {
            // look away!
        }

        // restore original client.
        $this->_endCharade();

        // if an exception occurred, throw it now.
        if (isset($e)) {
            throw $e;
        }

        return $this;
    }

    /**
     * Remove the volatile record. Since volatile records are never
     * submitted, this is actually a 'p4 revert' operation.
     *
     * @return  P4Cms_Record_Volatile   provides fluent interface.
     */
    public function delete()
    {
        $file = $this->_getDepotFile();

        // masquerade as another client
        $this->_beginCharade();

        try {
            $this->getAdapter()->getConnection()->run('revert', array('-k', $file));
        } catch (Exception $e) {
            // look away!
        }

        // restore original client.
        $this->_endCharade();

        // if an exception occurred, throw it now.
        if (isset($e)) {
            throw $e;
        }

        return $this;
    }

    /**
     * Set the id of this record.
     * Extended to clear depotFile anytime id changes.
     *
     * @param   mixed   $id             the value of the id of this record.
     * @return  P4Cms_Record_Volatile   provides a fluent interface
     */
    public function setId($id)
    {
        $this->_depotFile = null;
        return parent::setId($id);
    }

    /**
     * Load values into this record from Perforce.
     * Clobbers any existing in-memory values.
     *
     * @return  P4Cms_Record_Volatile   provides fluent interface.
     */
    protected function _populate()
    {
        $file = $this->_getDepotFile();

        // masquerade as another client
        $this->_beginCharade();

        try {
            $result = $this->getAdapter()->getConnection()->run('fstat', array('-Oa', $file));

            // extract information if no warings (ie. file exists).
            if (!$result->hasWarnings()) {
                $this->_values = array();
                foreach ((array) $result->getData(0) as $key => $value) {
                    $parts = explode('-', $key, 2);
                    if (count($parts) !== 2 || $parts[0] !== 'openattr') {
                        continue;
                    }

                    $this->_setValue($parts[1], $value);
                }
            } else {
                // warnings indicate no such record.
                $e = new P4Cms_Record_NotFoundException(
                    "Cannot fetch record: " . $this->getId() . ". No matching record."
                );
            }
        } catch (Exception $e) {
            // look away!
        }

        // restore original client.
        $this->_endCharade();

        // if an exception occurred, throw it now.
        if (isset($e)) {
            throw $e;
        }

        return $this;
    }

    /**
     * Run a Perforce command with no exceptions.
     *
     * @param   string          $command    the command to run.
     * @param   array|string    $params     optional - one or more arguments.
     * @return  bool|P4_Result  command result or false if it failed.
     */
    protected function _runFree($command, $params)
    {
        $adapter = $this->getAdapter();
        $p4      = $adapter->getConnection();

        try {
            return $p4->run($command, $params);
        } catch (P4_Exception $e) {
            return false;
        }
    }

    /**
     * Begin masquerading as another client/host.
     */
    protected function _beginCharade()
    {
        // nothing to do if not masquerading.
        $masquerade = $this->getClientMasquerade();
        if (!$masquerade) {
            return;
        }

        $adapter = $this->getAdapter();
        $p4      = $adapter->getConnection();

        // clobber the current client and host, but remember
        // them so we can restore them afterwards.
        $this->_originalClient = $p4->getClient();
        $this->_originalHost   = $p4->getHost();
        $p4->setClient($masquerade->getId() ?: $p4->getClient());
        $p4->setHost($masquerade->getHost() ?: $p4->getHost());
    }

    /**
     * Stop masquerading - restore original client/host.
     */
    protected function _endCharade()
    {
        // nothing to do if not masquerading.
        if (!$this->_originalClient) {
            return;
        }

        $adapter = $this->getAdapter();
        $p4      = $adapter->getConnection();

        $p4->setClient($this->_originalClient);
        $p4->setHost($this->_originalHost);

        $this->_originalClient = null;
        $this->_originalHost   = null;
    }

    /**
     * Get the (depot-syntax formatted) path to this record in Perforce.
     *
     * @return  string  the path to this record in depot-syntax.
     */
    protected function _getDepotFile()
    {
        // must have an id to get depot file.
        if (!$this->getId()) {
            throw new P4Cms_Record_Exception("Cannot get record file path without an id.");
        }

        // convert id to depot file syntax if we haven't already.
        if (!$this->_depotFile) {
            $adapter = $this->getAdapter();
            $subPath = trim($this->_storageSubPath, '/\\');
            $subPath = $subPath ? '/' . $subPath . '/' : '/';
            $file    = $adapter->getBasePath() . $subPath . $this->getId();
            $result  = $adapter->getConnection()->run('where', $file);

            $this->_depotFile = $result->getData(0, 'depotFile');
        }

        return $this->_depotFile;
    }
}
# Change User Description Committed
#1 16170 perforce_software Move Chronicle files to follow new path scheme for branching.
//guest/perforce_software/chronicle/library/P4Cms/Record/Volatile.php
#1 8972 Matt Attaway Initial add of the Chronicle source code