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\Resource;
19:
20: use Guzzle\Http\EntityBody;
21: use Guzzle\Http\Exception\BadResponseException;
22: use Guzzle\Http\Exception\ClientErrorResponseException;
23: use Guzzle\Http\Message\Response;
24: use Guzzle\Http\Url;
25: use OpenCloud\Common\Constants\Size;
26: use OpenCloud\Common\Exceptions;
27: use OpenCloud\Common\Service\ServiceInterface;
28: use OpenCloud\ObjectStore\Constants\Header as HeaderConst;
29: use OpenCloud\ObjectStore\Exception\ContainerException;
30: use OpenCloud\ObjectStore\Exception\ObjectNotFoundException;
31: use OpenCloud\ObjectStore\Upload\DirectorySync;
32: use OpenCloud\ObjectStore\Upload\TransferBuilder;
33:
34: /**
35: * A container is a storage compartment for your data and provides a way for you
36: * to organize your data. You can think of a container as a folder in Windows
37: * or a directory in Unix. The primary difference between a container and these
38: * other file system concepts is that containers cannot be nested.
39: *
40: * A container can also be CDN-enabled (for public access), in which case you
41: * will need to interact with a CDNContainer object instead of this one.
42: */
43: class Container extends AbstractContainer
44: {
45: const METADATA_LABEL = 'Container';
46:
47: /**
48: * This is the object that holds all the CDN functionality. This Container therefore acts as a simple wrapper and is
49: * interested in storage concerns only.
50: *
51: * @var CDNContainer|null
52: */
53: private $cdn;
54:
55: public function __construct(ServiceInterface $service, $data = null)
56: {
57: parent::__construct($service, $data);
58:
59: // Set metadata items for collection listings
60: if (isset($data->count)) {
61: $this->metadata->setProperty('Object-Count', $data->count);
62: }
63: if (isset($data->bytes)) {
64: $this->metadata->setProperty('Bytes-Used', $data->bytes);
65: }
66: }
67:
68: /**
69: * Factory method that instantiates an object from a Response object.
70: *
71: * @param Response $response
72: * @param ServiceInterface $service
73: * @return static
74: */
75: public static function fromResponse(Response $response, ServiceInterface $service)
76: {
77: $self = parent::fromResponse($response, $service);
78:
79: $segments = Url::factory($response->getEffectiveUrl())->getPathSegments();
80: $self->name = end($segments);
81:
82: return $self;
83: }
84:
85: /**
86: * Get the CDN object.
87: *
88: * @return null|CDNContainer
89: * @throws \OpenCloud\Common\Exceptions\CdnNotAvailableError
90: */
91: public function getCdn()
92: {
93: if (!$this->isCdnEnabled()) {
94: throw new Exceptions\CdnNotAvailableError(
95: 'Either this container is not CDN-enabled or the CDN is not available'
96: );
97: }
98:
99: return $this->cdn;
100: }
101:
102: /**
103: * It would be awesome to put these convenience methods (which are identical to the ones in the Account object) in
104: * a trait, but we have to wait for v5.3 EOL first...
105: *
106: * @return null|string|int
107: */
108: public function getObjectCount()
109: {
110: return $this->metadata->getProperty('Object-Count');
111: }
112:
113: /**
114: * @return null|string|int
115: */
116: public function getBytesUsed()
117: {
118: return $this->metadata->getProperty('Bytes-Used');
119: }
120:
121: /**
122: * @param $value
123: * @return mixed
124: */
125: public function setCountQuota($value)
126: {
127: $this->metadata->setProperty('Quota-Count', $value);
128:
129: return $this->saveMetadata($this->metadata->toArray());
130: }
131:
132: /**
133: * @return null|string|int
134: */
135: public function getCountQuota()
136: {
137: return $this->metadata->getProperty('Quota-Count');
138: }
139:
140: /**
141: * @param $value
142: * @return mixed
143: */
144: public function setBytesQuota($value)
145: {
146: $this->metadata->setProperty('Quota-Bytes', $value);
147:
148: return $this->saveMetadata($this->metadata->toArray());
149: }
150:
151: /**
152: * @return null|string|int
153: */
154: public function getBytesQuota()
155: {
156: return $this->metadata->getProperty('Quota-Bytes');
157: }
158:
159: public function delete($deleteObjects = false)
160: {
161: if ($deleteObjects === true) {
162: $this->deleteAllObjects();
163: }
164:
165: try {
166: return $this->getClient()->delete($this->getUrl())->send();
167: } catch (ClientErrorResponseException $e) {
168: if ($e->getResponse()->getStatusCode() == 409) {
169: throw new ContainerException(sprintf(
170: 'The API returned this error: %s. You might have to delete all existing objects before continuing.',
171: (string) $e->getResponse()->getBody()
172: ));
173: } else {
174: throw $e;
175: }
176: }
177: }
178:
179: /**
180: * Deletes all objects that this container currently contains. Useful when doing operations (like a delete) that
181: * require an empty container first.
182: *
183: * @return mixed
184: */
185: public function deleteAllObjects()
186: {
187: $requests = array();
188:
189: $list = $this->objectList();
190:
191: foreach ($list as $object) {
192: $requests[] = $this->getClient()->delete($object->getUrl());
193: }
194:
195: return $this->getClient()->send($requests);
196: }
197:
198: /**
199: * Creates a Collection of objects in the container
200: *
201: * @param array $params associative array of parameter values.
202: * * account/tenant - The unique identifier of the account/tenant.
203: * * container- The unique identifier of the container.
204: * * limit (Optional) - The number limit of results.
205: * * marker (Optional) - Value of the marker, that the object names
206: * greater in value than are returned.
207: * * end_marker (Optional) - Value of the marker, that the object names
208: * less in value than are returned.
209: * * prefix (Optional) - Value of the prefix, which the returned object
210: * names begin with.
211: * * format (Optional) - Value of the serialized response format, either
212: * json or xml.
213: * * delimiter (Optional) - Value of the delimiter, that all the object
214: * names nested in the container are returned.
215: * @link http://api.openstack.org for a list of possible parameter
216: * names and values
217: * @return 'OpenCloud\Common\Collection
218: * @throws ObjFetchError
219: */
220: public function objectList(array $params = array())
221: {
222: $params['format'] = 'json';
223:
224: return $this->getService()->resourceList('DataObject', $this->getUrl(null, $params), $this);
225: }
226:
227: /**
228: * Turn on access logs, which track all the web traffic that your data objects accrue.
229: *
230: * @return \Guzzle\Http\Message\Response
231: */
232: public function enableLogging()
233: {
234: return $this->saveMetadata($this->appendToMetadata(array(
235: HeaderConst::ACCESS_LOGS => 'True'
236: )));
237: }
238:
239: /**
240: * Disable access logs.
241: *
242: * @return \Guzzle\Http\Message\Response
243: */
244: public function disableLogging()
245: {
246: return $this->saveMetadata($this->appendToMetadata(array(
247: HeaderConst::ACCESS_LOGS => 'False'
248: )));
249: }
250:
251: /**
252: * Enable this container for public CDN access.
253: *
254: * @param null $ttl
255: */
256: public function enableCdn($ttl = null)
257: {
258: $headers = array('X-CDN-Enabled' => 'True');
259: if ($ttl) {
260: $headers['X-TTL'] = (int) $ttl;
261: }
262:
263: $this->getClient()->put($this->getCdnService()->getUrl($this->name), $headers)->send();
264: $this->refresh();
265: }
266:
267: /**
268: * Disables the containers CDN function. Note that the container will still
269: * be available on the CDN until its TTL expires.
270: *
271: * @return \Guzzle\Http\Message\Response
272: */
273: public function disableCdn()
274: {
275: $headers = array('X-CDN-Enabled' => 'False');
276:
277: return $this->getClient()
278: ->put($this->getCdnService()->getUrl($this->name), $headers)
279: ->send();
280: }
281:
282: public function refresh($id = null, $url = null)
283: {
284: $headers = $this->createRefreshRequest()->send()->getHeaders();
285: $this->setMetadata($headers, true);
286:
287: try {
288: if (null !== ($cdnService = $this->getService()->getCDNService())) {
289: $cdn = new CDNContainer($cdnService);
290: $cdn->setName($this->name);
291:
292: $response = $cdn->createRefreshRequest()->send();
293:
294: if ($response->isSuccessful()) {
295: $this->cdn = $cdn;
296: $this->cdn->setMetadata($response->getHeaders(), true);
297: }
298: } else {
299: $this->cdn = null;
300: }
301: } catch (ClientErrorResponseException $e) {
302: }
303: }
304:
305: /**
306: * Get either a fresh data object (no $info), or get an existing one by passing in data for population.
307: *
308: * @param mixed $info
309: * @return DataObject
310: */
311: public function dataObject($info = null)
312: {
313: return new DataObject($this, $info);
314: }
315:
316: /**
317: * Retrieve an object from the API. Apart from using the name as an
318: * identifier, you can also specify additional headers that will be used
319: * fpr a conditional GET request. These are
320: *
321: * * `If-Match'
322: * * `If-None-Match'
323: * * `If-Modified-Since'
324: * * `If-Unmodified-Since'
325: * * `Range' For example:
326: * bytes=-5 would mean the last 5 bytes of the object
327: * bytes=10-15 would mean 5 bytes after a 10 byte offset
328: * bytes=32- would mean all dat after first 32 bytes
329: *
330: * These are also documented in RFC 2616.
331: *
332: * @param string $name
333: * @param array $headers
334: * @return DataObject
335: */
336: public function getObject($name, array $headers = array())
337: {
338: try {
339: $response = $this->getClient()
340: ->get($this->getUrl($name), $headers)
341: ->send();
342: } catch (BadResponseException $e) {
343: if ($e->getResponse()->getStatusCode() == 404) {
344: throw ObjectNotFoundException::factory($name, $e);
345: }
346: throw $e;
347: }
348:
349: return $this->dataObject()
350: ->populateFromResponse($response)
351: ->setName($name);
352: }
353:
354: /**
355: * Essentially the same as {@see getObject()}, except only the metadata is fetched from the API.
356: * This is useful for cases when the user does not want to fetch the full entity body of the
357: * object, only its metadata.
358: *
359: * @param $name
360: * @param array $headers
361: * @return $this
362: */
363: public function getPartialObject($name, array $headers = array())
364: {
365: $response = $this->getClient()
366: ->head($this->getUrl($name), $headers)
367: ->send();
368:
369: return $this->dataObject()
370: ->populateFromResponse($response)
371: ->setName($name);
372: }
373:
374: /**
375: * Check if an object exists inside a container. Uses {@see getPartialObject()}
376: * to save on bandwidth and time.
377: *
378: * @param $name Object name
379: * @return boolean True, if object exists in this container; false otherwise.
380: */
381: public function objectExists($name)
382: {
383: try {
384: // Send HEAD request to check resource existence
385: $url = clone $this->getUrl();
386: $url->addPath((string) $name);
387: $this->getClient()->head($url)->send();
388: } catch (ClientErrorResponseException $e) {
389: // If a 404 was returned, then the object doesn't exist
390: if ($e->getResponse()->getStatusCode() === 404) {
391: return false;
392: } else {
393: throw $e;
394: }
395: }
396:
397: return true;
398: }
399:
400: /**
401: * Upload a single file to the API.
402: *
403: * @param $name Name that the file will be saved as in your container.
404: * @param $data Either a string or stream representation of the file contents to be uploaded.
405: * @param array $headers Optional headers that will be sent with the request (useful for object metadata).
406: * @return DataObject
407: */
408: public function uploadObject($name, $data, array $headers = array())
409: {
410: $entityBody = EntityBody::factory($data);
411:
412: $url = clone $this->getUrl();
413: $url->addPath($name);
414:
415: // @todo for new major release: Return response rather than populated DataObject
416:
417: $response = $this->getClient()->put($url, $headers, $entityBody)->send();
418:
419: return $this->dataObject()
420: ->populateFromResponse($response)
421: ->setName($name)
422: ->setContent($entityBody);
423: }
424:
425: /**
426: * Upload an array of objects for upload. This method optimizes the upload procedure by batching requests for
427: * faster execution. This is a very useful procedure when you just have a bunch of unremarkable files to be
428: * uploaded quickly. Each file must be under 5GB.
429: *
430: * @param array $files With the following array structure:
431: * `name' Name that the file will be saved as in your container. Required.
432: * `path' Path to an existing file, OR
433: * `body' Either a string or stream representation of the file contents to be uploaded.
434: * @param array $headers Optional headers that will be sent with the request (useful for object metadata).
435: *
436: * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError
437: * @return \Guzzle\Http\Message\Response
438: */
439: public function uploadObjects(array $files, array $commonHeaders = array())
440: {
441: $requests = $entities = array();
442:
443: foreach ($files as $entity) {
444:
445: if (empty($entity['name'])) {
446: throw new Exceptions\InvalidArgumentError('You must provide a name.');
447: }
448:
449: if (!empty($entity['path']) && file_exists($entity['path'])) {
450: $body = fopen($entity['path'], 'r+');
451: } elseif (!empty($entity['body'])) {
452: $body = $entity['body'];
453: } else {
454: throw new Exceptions\InvalidArgumentError('You must provide either a readable path or a body');
455: }
456:
457: $entityBody = $entities[] = EntityBody::factory($body);
458:
459: // @codeCoverageIgnoreStart
460: if ($entityBody->getContentLength() >= 5 * Size::GB) {
461: throw new Exceptions\InvalidArgumentError(
462: 'For multiple uploads, you cannot upload more than 5GB per '
463: . ' file. Use the UploadBuilder for larger files.'
464: );
465: }
466: // @codeCoverageIgnoreEnd
467:
468: // Allow custom headers and common
469: $headers = (isset($entity['headers'])) ? $entity['headers'] : $commonHeaders;
470:
471: $url = clone $this->getUrl();
472: $url->addPath($entity['name']);
473:
474: $requests[] = $this->getClient()->put($url, $headers, $entityBody);
475: }
476:
477: $responses = $this->getClient()->send($requests);
478:
479: foreach ($entities as $entity) {
480: $entity->close();
481: }
482:
483: return $responses;
484: }
485:
486: /**
487: * When uploading large files (+5GB), you need to upload the file as chunks using multibyte transfer. This method
488: * sets up the transfer, and in order to execute the transfer, you need to call upload() on the returned object.
489: *
490: * @param array Options
491: * @see \OpenCloud\ObjectStore\Upload\UploadBuilder::setOptions for a list of accepted options.
492: * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError
493: * @return mixed
494: */
495: public function setupObjectTransfer(array $options = array())
496: {
497: // Name is required
498: if (empty($options['name'])) {
499: throw new Exceptions\InvalidArgumentError('You must provide a name.');
500: }
501:
502: // As is some form of entity body
503: if (!empty($options['path']) && file_exists($options['path'])) {
504: $body = fopen($options['path'], 'r+');
505: } elseif (!empty($options['body'])) {
506: $body = $options['body'];
507: } else {
508: throw new Exceptions\InvalidArgumentError('You must provide either a readable path or a body');
509: }
510:
511: // Build upload
512: $transfer = TransferBuilder::newInstance()
513: ->setOption('objectName', $options['name'])
514: ->setEntityBody(EntityBody::factory($body))
515: ->setContainer($this);
516:
517: // Add extra options
518: if (!empty($options['metadata'])) {
519: $transfer->setOption('metadata', $options['metadata']);
520: }
521: if (!empty($options['partSize'])) {
522: $transfer->setOption('partSize', $options['partSize']);
523: }
524: if (!empty($options['concurrency'])) {
525: $transfer->setOption('concurrency', $options['concurrency']);
526: }
527: if (!empty($options['progress'])) {
528: $transfer->setOption('progress', $options['progress']);
529: }
530:
531: return $transfer->build();
532: }
533:
534: /**
535: * Upload the contents of a local directory to a remote container, effectively syncing them.
536: *
537: * @param $path The local path to the directory.
538: */
539: public function uploadDirectory($path)
540: {
541: $sync = DirectorySync::factory($path, $this);
542: $sync->execute();
543: }
544:
545: public function isCdnEnabled()
546: {
547: return ($this->cdn instanceof CDNContainer) && $this->cdn->isCdnEnabled();
548: }
549: }
550: