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: