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