<?php
/**
* Copyright 2012-2014 Rackspace US, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace OpenCloud\ObjectStore\Upload;
use DirectoryIterator;
use Guzzle\Http\EntityBody;
use OpenCloud\Common\Collection\ResourceIterator;
use OpenCloud\Common\Exceptions\InvalidArgumentError;
use OpenCloud\ObjectStore\Resource\Container;
/**
* DirectorySync upload class, in charge of creating, replacing and delete data objects on the API. The goal of
* this execution is to sync local directories with remote CloudFiles containers so that they are consistent.
*
* @package OpenCloud\ObjectStore\Upload
*/
class DirectorySync
{
/** @var string The path to the directory you're syncing. */
private $basePath;
/** @var ResourceIterator A collection of remote files in Swift. */
private $remoteFiles;
/** @var AbstractContainer The Container object you are syncing. */
private $container;
/** @var string */
private $targetDir;
/**
* Basic factory method to instantiate a new DirectorySync object with all the appropriate properties.
*
* @param string $path The local path
* @param Container $container The container you're syncing
* @param string $targetDir The path (or pseudo-directory) that the files will be nested in
*
* @return DirectorySync
*/
public static function factory($path, Container $container, $targetDir = null)
{
$transfer = new self();
$transfer->setBasePath($path);
$transfer->setContainer($container);
$transfer->setRemoteFiles($container->objectList());
$transfer->setTargetDir($targetDir);
return $transfer;
}
/**
* @param $path
* @throws \OpenCloud\Common\Exceptions\InvalidArgumentError
*/
public function setBasePath($path)
{
if (!file_exists($path)) {
throw new InvalidArgumentError(sprintf('%s does not exist', $path));
}
$this->basePath = $path;
}
/**
* @param ResourceIterator $remoteFiles
*/
public function setRemoteFiles(ResourceIterator $remoteFiles)
{
$this->remoteFiles = $remoteFiles;
}
/**
* @param Container $container
*/
public function setContainer(Container $container)
{
$this->container = $container;
}
/**
* @param string $dir The target path that all files will be nested in. By default, the files will not be nested.
*/
public function setTargetDir($dir)
{
$this->targetDir = rtrim($dir, '/');
}
/**
* Execute the sync process. This will collect all the remote files from the API and do a comparison. There are
* four scenarios that need to be dealt with:
*
* - Exists locally, exists remotely (identical checksum) = no action
* - Exists locally, exists remotely (diff checksum) = local overwrites remote
* - Exists locally, not exists remotely = local is written to remote
* - Not exists locally, exists remotely = remote file is deleted
*/
public function execute()
{
$localFiles = $this->traversePath($this->basePath);
$this->remoteFiles->rewind();
$this->remoteFiles->populateAll();
$entities = array();
$requests = array();
$deletePaths = array();
// Handle PUT requests (create/update files)
foreach ($localFiles as $filename) {
$remoteFilename = $this->targetDir ? $this->targetDir . '/' . $filename : $filename;
$callback = $this->getCallback($remoteFilename);
$filePath = rtrim($this->basePath, '/') . '/' . $filename;
if (!is_readable($filePath)) {
continue;
}
$entities[] = $entityBody = EntityBody::factory(fopen($filePath, 'r+'));
if (false !== ($remoteFile = $this->remoteFiles->search($callback))) {
// if different, upload updated version
if ($remoteFile->getEtag() != $entityBody->getContentMd5()) {
$requests[] = $this->container->getClient()->put(
$remoteFile->getUrl(),
$remoteFile->getMetadata()->toArray(),
$entityBody
);
}
} else {
// upload new file
$url = clone $this->container->getUrl();
$url->addPath($remoteFilename);
$requests[] = $this->container->getClient()->put($url, array(), $entityBody);
}
}
// Handle DELETE requests
foreach ($this->remoteFiles as $remoteFile) {
$remoteName = $remoteFile->getName();
if (!in_array($remoteName, $localFiles)) {
$deletePaths[] = sprintf('/%s/%s', $this->container->getName(), $remoteName);
}
}
// send update/create requests
if (count($requests)) {
$this->container->getClient()->send($requests);
}
// bulk delete
if (count($deletePaths)) {
$this->container->getService()->bulkDelete($deletePaths);
}
// close all streams
if (count($entities)) {
foreach ($entities as $entity) {
$entity->close();
}
}
}
/**
* Given a path, traverse it recursively for nested files.
*
* @param $path
* @return array
*/
private function traversePath($path)
{
$filenames = array();
$directory = new DirectoryIterator($path);
foreach ($directory as $file) {
if ($file->isDot()) {
continue;
}
if ($file->isDir()) {
$filenames = array_merge($filenames, $this->traversePath($file->getPathname()));
} else {
$filenames[] = $this->trimFilename($file);
}
}
return $filenames;
}
/**
* Given a path, trim away leading slashes and strip the base path.
*
* @param $file
* @return string
*/
private function trimFilename($file)
{
return ltrim(str_replace($this->basePath, '', $file->getPathname()), '/');
}
/**
* Get the callback used to do a search function on the remote iterator.
*
* @param $name The name of the file we're looking for.
* @return callable
*/
private function getCallback($name)
{
$name = trim($name, '/');
return function ($remoteFile) use ($name) {
if ($remoteFile->getName() == $name) {
return true;
}
return false;
};
}
}