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;
19:
20: use Guzzle\Http\EntityBody;
21: use OpenCloud\Common\Constants\Header;
22: use OpenCloud\Common\Constants\Mime;
23: use OpenCloud\Common\Exceptions;
24: use OpenCloud\Common\Exceptions\InvalidArgumentError;
25: use OpenCloud\Common\Http\Client;
26: use OpenCloud\Common\Http\Message\Formatter;
27: use OpenCloud\Common\Log\Logger;
28: use OpenCloud\Common\Service\ServiceBuilder;
29: use OpenCloud\ObjectStore\Constants\UrlType;
30: use OpenCloud\ObjectStore\Resource\Container;
31: use OpenCloud\ObjectStore\Upload\ContainerMigration;
32:
33: /**
34: * The ObjectStore (Cloud Files) service.
35: */
36: class Service extends AbstractService
37: {
38: const DEFAULT_NAME = 'cloudFiles';
39: const DEFAULT_TYPE = 'object-store';
40: const BATCH_DELETE_MAX = 10000;
41:
42: /**
43: * This holds the associated CDN service (for Rackspace public cloud)
44: * or is NULL otherwise. The existence of an object here is
45: * indicative that the CDN service is available.
46: */
47: private $cdnService;
48:
49: public function __construct(Client $client, $type = null, $name = null, $region = null, $urlType = null)
50: {
51: parent::__construct($client, $type, $name, $region, $urlType);
52:
53: try {
54: $this->cdnService = ServiceBuilder::factory($client, 'OpenCloud\ObjectStore\CDNService', array(
55: 'region' => $region
56: ));
57: } catch (Exceptions\EndpointError $e) {
58: }
59: }
60:
61: /**
62: * Return the CDN version of the ObjectStore service.
63: *
64: * @return CDNService CDN version of the ObjectStore service
65: */
66: public function getCdnService()
67: {
68: return $this->cdnService;
69: }
70:
71: /**
72: * List all available containers.
73: *
74: * @param array $filter Array of filter options such as:
75: *
76: * * `limit`: number of results to limit the list to. Optional.
77: * * `marker`: name of container after which to start the list. Optional.
78: * * `end_marker`: name of container before which to end the list. Optional.
79: * @return \OpenCloud\Common\Collection\PaginatedIterator Iterator to list of containers
80: */
81: public function listContainers(array $filter = array())
82: {
83: $filter['format'] = 'json';
84: return $this->resourceList('Container', $this->getUrl(null, $filter), $this);
85: }
86:
87: /**
88: * Return a new or existing (if name is specified) container.
89: *
90: * @param \stdClass $data Data to initialize container. Optional.
91: * @return Container Container
92: */
93: public function getContainer($data = null)
94: {
95: return new Container($this, $data);
96: }
97:
98: /**
99: * Create a container for this service.
100: *
101: * @param string $name The name of the container
102: * @param array $metadata Additional (optional) metadata to associate with the container
103: * @return bool|Container Newly-created Container upon success; false, otherwise
104: */
105: public function createContainer($name, array $metadata = array())
106: {
107: $this->checkContainerName($name);
108:
109: $containerHeaders = Container::stockHeaders($metadata);
110:
111: $response = $this->getClient()
112: ->put($this->getUrl($name), $containerHeaders)
113: ->send();
114:
115: if ($response->getStatusCode() == 201) {
116: return Container::fromResponse($response, $this);
117: }
118:
119: return false;
120: }
121:
122: /**
123: * Check the validity of a potential container name.
124: *
125: * @param string $name Name of container
126: * @return bool True if container name is valid
127: * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError if container name is invalid
128: */
129: public function checkContainerName($name)
130: {
131: if (strlen($name) == 0) {
132: $error = 'Container name cannot be blank';
133: }
134:
135: if (strpos($name, '/') !== false) {
136: $error = 'Container name cannot contain "/"';
137: }
138:
139: if (strlen($name) > self::MAX_CONTAINER_NAME_LENGTH) {
140: $error = 'Container name is too long';
141: }
142:
143: if (isset($error)) {
144: throw new InvalidArgumentError($error);
145: }
146:
147: return true;
148: }
149:
150: /**
151: * Perform a bulk extraction, expanding an archive file. If the $path is an empty string, containers will be
152: * auto-created accordingly, and files in the archive that do not map to any container (files in the base directory)
153: * will be ignored. You can create up to 1,000 new containers per extraction request. Also note that only regular
154: * files will be uploaded. Empty directories, symlinks, and so on, will not be uploaded.
155: *
156: * @param string $path The path to the archive being extracted
157: * @param string|stream $archive The contents of the archive (either string or stream)
158: * @param string $archiveType The type of archive you're using {@see \OpenCloud\ObjectStore\Constants\UrlType}
159: * @return \Guzzle\Http\Message\Response HTTP response from API
160: * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError if specifed `$archiveType` is invalid
161: * @throws Exception\BulkOperationException if there are errors with the bulk extract
162: */
163: public function bulkExtract($path = '', $archive, $archiveType = UrlType::TAR_GZ)
164: {
165: $entity = EntityBody::factory($archive);
166:
167: $acceptableTypes = array(
168: UrlType::TAR,
169: UrlType::TAR_GZ,
170: UrlType::TAR_BZ2
171: );
172:
173: if (!in_array($archiveType, $acceptableTypes)) {
174: throw new InvalidArgumentError(sprintf(
175: 'The archive type must be one of the following: [%s]. You provided [%s].',
176: implode($acceptableTypes, ','),
177: print_r($archiveType, true)
178: ));
179: }
180:
181: $url = $this->getUrl()->addPath($path)->setQuery(array('extract-archive' => $archiveType));
182: $response = $this->getClient()->put($url, array(Header::CONTENT_TYPE => ''), $entity)->send();
183:
184: $body = Formatter::decode($response);
185:
186: if (!empty($body->Errors)) {
187: throw new Exception\BulkOperationException((array) $body->Errors);
188: }
189:
190: return $response;
191: }
192:
193: /**
194: * @deprecated Please use {@see batchDelete()} instead.
195: */
196: public function bulkDelete(array $paths)
197: {
198: $this->getLogger()->warning(Logger::deprecated(__METHOD__, '::batchDelete()'));
199:
200: return $this->executeBatchDeleteRequest($paths);
201: }
202:
203: /**
204: * Batch delete will delete an array of object paths. By default,
205: * the API will only accept a maximum of 10,000 object deletions
206: * per request - so for arrays that exceed this size, it is chunked
207: * and sent as individual requests.
208: *
209: * @param array $paths The objects you want to delete. Each path needs
210: * be formatted as `/{containerName}/{objectName}`. If
211: * you are deleting `object_1` and `object_2` from the
212: * `photos_container`, the array will be:
213: *
214: * array(
215: * '/photos_container/object_1',
216: * '/photos_container/object_2'
217: * )
218: *
219: * @return array[Guzzle\Http\Message\Response] HTTP responses from the API
220: * @throws Exception\BulkOperationException if the bulk delete operation fails
221: */
222: public function batchDelete(array $paths)
223: {
224: $chunks = array_chunk($paths, self::BATCH_DELETE_MAX);
225:
226: $responses = array();
227:
228: foreach ($chunks as $chunk) {
229: $responses[] = $this->executeBatchDeleteRequest($chunk);
230: }
231:
232: return $responses;
233: }
234:
235: /**
236: * Internal method for dispatching single batch delete requests.
237: *
238: * @param array $paths
239: * @return \Guzzle\Http\Message\Response
240: * @throws Exception\BulkOperationException
241: */
242: private function executeBatchDeleteRequest(array $paths)
243: {
244: $entity = EntityBody::factory(implode(PHP_EOL, $paths));
245:
246: $url = $this->getUrl()->setQuery(array('bulk-delete' => true));
247:
248: $response = $this->getClient()
249: ->delete($url, array(Header::CONTENT_TYPE => Mime::TEXT), $entity)
250: ->send();
251:
252: try {
253: $body = Formatter::decode($response);
254: if (!empty($body->Errors)) {
255: throw new Exception\BulkOperationException((array) $body->Errors);
256: }
257: } catch (Exceptions\JsonError $e) {
258: }
259:
260: return $response;
261: }
262:
263: /**
264: * Allows files to be transferred from one container to another.
265: *
266: * @param Container $old Where you're moving files from
267: * @param Container $new Where you're moving files to
268: * @param array $options Options to configure the migration. Optional. Available options are:
269: *
270: * * `read.batchLimit`: Number of files to read at a time from `$old` container. Optional; default = 1000.
271: * * `write.batchLimit`: Number of files to write at a time to `$new` container. Optional; default = 1000.
272: * * `read.pageLimit`: Number of filenames to read at a time from `$old` container. Optional; default = 10000.
273: * @return array[Guzzle\Http\Message\Response] HTTP responses from the API
274: */
275: public function migrateContainer(Container $old, Container $new, array $options = array())
276: {
277: $migration = ContainerMigration::factory($old, $new, $options);
278:
279: return $migration->transfer();
280: }
281: }
282: