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