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: $callback = $this->getCallback($filename);
116: $filePath = rtrim($this->basePath, '/') . '/' . $filename;
117:
118: if (!is_readable($filePath)) {
119: continue;
120: }
121:
122: $entities[] = $entityBody = EntityBody::factory(fopen($filePath, 'r+'));
123:
124: if (false !== ($remoteFile = $this->remoteFiles->search($callback))) {
125: // if different, upload updated version
126: if ($remoteFile->getEtag() != $entityBody->getContentMd5()) {
127: $requests[] = $this->container->getClient()->put(
128: $remoteFile->getUrl(),
129: $remoteFile->getMetadata()->toArray(),
130: $entityBody
131: );
132: }
133: } else {
134: // upload new file
135: $url = clone $this->container->getUrl();
136: $url->addPath($filename);
137:
138: $requests[] = $this->container->getClient()->put($url, array(), $entityBody);
139: }
140: }
141:
142: // Handle DELETE requests
143: foreach ($this->remoteFiles as $remoteFile) {
144: $remoteName = $remoteFile->getName();
145: if (!in_array($remoteName, $localFiles)) {
146: $deletePaths[] = sprintf('/%s/%s', $this->container->getName(), $remoteName);
147: }
148: }
149:
150: // send update/create requests
151: if (count($requests)) {
152: $this->container->getClient()->send($requests);
153: }
154:
155: // bulk delete
156: if (count($deletePaths)) {
157: $this->container->getService()->bulkDelete($deletePaths);
158: }
159:
160: // close all streams
161: if (count($entities)) {
162: foreach ($entities as $entity) {
163: $entity->close();
164: }
165: }
166: }
167:
168: /**
169: * Given a path, traverse it recursively for nested files.
170: *
171: * @param $path
172: * @return array
173: */
174: private function traversePath($path)
175: {
176: $filenames = array();
177:
178: $directory = new DirectoryIterator($path);
179:
180: foreach ($directory as $file) {
181: if ($file->isDot()) {
182: continue;
183: }
184: if ($file->isDir()) {
185: $filenames = array_merge($filenames, $this->traversePath($file->getPathname()));
186: } else {
187: $filenames[] = $this->trimFilename($file);
188: }
189: }
190:
191: return $filenames;
192: }
193:
194: /**
195: * Given a path, trim away leading slashes and strip the base path.
196: *
197: * @param $file
198: * @return string
199: */
200: private function trimFilename($file)
201: {
202: return ltrim(str_replace($this->basePath, '', $file->getPathname()), '/');
203: }
204:
205: /**
206: * Get the callback used to do a search function on the remote iterator.
207: *
208: * @param $name The name of the file we're looking for.
209: * @return callable
210: */
211: private function getCallback($name)
212: {
213: $name = trim($name, '/');
214:
215: return function ($remoteFile) use ($name) {
216: if ($remoteFile->getName() == $name) {
217: return true;
218: }
219:
220: return false;
221: };
222: }
223: }
224: