      1: <?php
      2: /**
      3:  * Copyright 2012-2014 Rackspace US, Inc.
      4:  *
      5:  * Licensed under the Apache License, Version 2.0 (the "License");
      6:  * you may not use this file except in compliance with the License.
      7:  * You may obtain a copy of the License at
      8:  *
      9:  * http://www.apache.org/licenses/LICENSE-2.0
     10:  *
     11:  * Unless required by applicable law or agreed to in writing, software
     12:  * distributed under the License is distributed on an "AS IS" BASIS,
     13:  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14:  * See the License for the specific language governing permissions and
     15:  * limitations under the License.
     16:  */
     17:
     18: namespace OpenCloud\ObjectStore\Upload;
     19:
     20: use DirectoryIterator;
     21: use Guzzle\Http\EntityBody;
     22: use OpenCloud\Common\Collection\ResourceIterator;
     23: use OpenCloud\Common\Exceptions\InvalidArgumentError;
     24: use OpenCloud\ObjectStore\Resource\Container;
     25:
     26: /**
     27:  * DirectorySync upload class, in charge of creating, replacing and delete data objects on the API. The goal of
     28:  * this execution is to sync local directories with remote CloudFiles containers so that they are consistent.
     29:  *
     30:  * @package OpenCloud\ObjectStore\Upload
     31:  */
     32: class DirectorySync
     33: {
     34:     /**
     35:      * @var string The path to the directory you're syncing.
     36:      */
     37:     private $basePath;
     38:     /**
     39:      * @var ResourceIterator A collection of remote files in Swift.
     40:      */
     41:     private $remoteFiles;
     42:     /**
     43:      * @var AbstractContainer The Container object you are syncing.
     44:      */
     45:     private $container;
     46:
     47:     /**
     48:      * Basic factory method to instantiate a new DirectorySync object with all the appropriate properties.
     49:      *
     50:      * @param           $path      The local path
     51:      * @param Container $container The container you're syncing
     52:      * @return DirectorySync
     53:      */
     54:     public static function factory($path, Container $container)
     55:     {
     56:         $transfer = new self();
     57:         $transfer->setBasePath($path);
     58:         $transfer->setContainer($container);
     59:         $transfer->setRemoteFiles($container->objectList());
     60:
     61:         return $transfer;
     62:     }
     63:
     64:     /**
     65:      * @param $path
     66:      * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError
     67:      */
     68:     public function setBasePath($path)
     69:     {
     70:         if (!file_exists($path)) {
     71:             throw new InvalidArgumentError(sprintf('%s does not exist', $path));
     72:         }
     73:
     74:         $this->basePath = $path;
     75:     }
     76:
     77:     /**
     78:      * @param ResourceIterator $remoteFiles
     79:      */
     80:     public function setRemoteFiles(ResourceIterator $remoteFiles)
     81:     {
     82:         $this->remoteFiles = $remoteFiles;
     83:     }
     84:
     85:     /**
     86:      * @param Container $container
     87:      */
     88:     public function setContainer(Container $container)
     89:     {
     90:         $this->container = $container;
     91:     }
     92:
     93:     /**
     94:      * Execute the sync process. This will collect all the remote files from the API and do a comparison. There are
     95:      * four scenarios that need to be dealt with:
     96:      *
     97:      * - Exists locally, exists remotely (identical checksum) = no action
     98:      * - Exists locally, exists remotely (diff checksum) = local overwrites remote
     99:      * - Exists locally, not exists remotely = local is written to remote
    100:      * - Not exists locally, exists remotely = remote file is deleted
    101:      */
    102:     public function execute()
    103:     {
    104:         $localFiles = $this->traversePath($this->basePath);
    105:
    106:         $this->remoteFiles->rewind();
    107:         $this->remoteFiles->populateAll();
    108:
    109:         $entities = array();
    110:         $requests = array();
    111:         $deletePaths = array();
    112:
    113:         // Handle PUT requests (create/update files)
    114:         foreach ($localFiles as $filename) {
    115:
    116:             $callback = $this->getCallback($filename);
    117:             $filePath = rtrim($this->basePath, '/') . '/' . $filename;
    118:
    119:             if (!is_readable($filePath)) {
    120:                 continue;
    121:             }
    122:
    123:             $entities[] = $entityBody = EntityBody::factory(fopen($filePath, 'r+'));
    124:
    125:             if (false !== ($remoteFile = $this->remoteFiles->search($callback))) {
    126:                 // if different, upload updated version
    127:                 if ($remoteFile->getEtag() != $entityBody->getContentMd5()) {
    128:                     $requests[] = $this->container->getClient()->put(
    129:                         $remoteFile->getUrl(),
    130:                         $remoteFile->getMetadata()->toArray(),
    131:                         $entityBody
    132:                     );
    133:                 }
    134:             } else {
    135:                 // upload new file
    136:                 $url = clone $this->container->getUrl();
    137:                 $url->addPath($filename);
    138:
    139:                 $requests[] = $this->container->getClient()->put($url, array(), $entityBody);
    140:             }
    141:         }
    142:
    143:         // Handle DELETE requests
    144:         foreach ($this->remoteFiles as $remoteFile) {
    145:             $remoteName = $remoteFile->getName();
    146:             if (!in_array($remoteName, $localFiles)) {
    147:                 $deletePaths[] = sprintf('/%s/%s', $this->container->getName(), $remoteName);
    148:             }
    149:         }
    150:
    151:         // send update/create requests
    152:         if (count($requests)) {
    153:             $this->container->getClient()->send($requests);
    154:         }
    155:
    156:         // bulk delete
    157:         if (count($deletePaths)) {
    158:             $this->container->getService()->bulkDelete($deletePaths);
    159:         }
    160:
    161:         // close all streams
    162:         if (count($entities)) {
    163:             foreach ($entities as $entity) {
    164:                 $entity->close();
    165:             }
    166:         }
    167:     }
    168:
    169:     /**
    170:      * Given a path, traverse it recursively for nested files.
    171:      *
    172:      * @param $path
    173:      * @return array
    174:      */
    175:     private function traversePath($path)
    176:     {
    177:         $filenames = array();
    178:
    179:         $directory = new DirectoryIterator($path);
    180:
    181:         foreach ($directory as $file) {
    182:             if ($file->isDot()) {
    183:                 continue;
    184:             }
    185:             if ($file->isDir()) {
    186:                 $filenames = array_merge($filenames, $this->traversePath($file->getPathname()));
    187:             } else {
    188:                 $filenames[] = $this->trimFilename($file);
    189:             }
    190:         }
    191:
    192:         return $filenames;
    193:     }
    194:
    195:     /**
    196:      * Given a path, trim away leading slashes and strip the base path.
    197:      *
    198:      * @param $file
    199:      * @return string
    200:      */
    201:     private function trimFilename($file)
    202:     {
    203:         return ltrim(str_replace($this->basePath, '', $file->getPathname()), '/');
    204:     }
    205:
    206:     /**
    207:      * Get the callback used to do a search function on the remote iterator.
    208:      *
    209:      * @param $name     The name of the file we're looking for.
    210:      * @return callable
    211:      */
    212:     private function getCallback($name)
    213:     {
    214:         $name = trim($name, '/');
    215:
    216:         return function ($remoteFile) use ($name) {
    217:             if ($remoteFile->getName() == $name) {
    218:                 return true;
    219:             }
    220:
    221:             return false;
    222:         };
    223:     }
    224: }
    225: