1  """ 
  2  container operations 
  3   
  4  Containers are storage compartments where you put your data (objects). 
  5  A container is similar to a directory or folder on a conventional filesystem 
  6  with the exception that they exist in a flat namespace, you can not create 
  7  containers inside of containers. 
  8   
  9  See COPYING for license information. 
 10  """ 
 11   
 12  from storage_object import Object, ObjectResults 
 13  from errors import ResponseError, InvalidContainerName, InvalidObjectName, \ 
 14                     ContainerNotPublic, CDNNotEnabled 
 15  from utils  import requires_name 
 16  import consts 
 17  from fjson  import json_loads 
 25      """ 
 26      Container object and Object instance factory. 
 27   
 28      If your account has the feature enabled, containers can be publically 
 29      shared over a global content delivery network. 
 30   
 31      @ivar name: the container's name (generally treated as read-only) 
 32      @type name: str 
 33      @ivar object_count: the number of objects in this container (cached) 
 34      @type object_count: number 
 35      @ivar size_used: the sum of the sizes of all objects in this container 
 36              (cached) 
 37      @type size_used: number 
 38      @ivar cdn_ttl: the time-to-live of the CDN's public cache of this container 
 39              (cached, use make_public to alter) 
 40      @type cdn_ttl: number 
 41      @ivar cdn_log_retention: retention of the logs in the container. 
 42      @type cdn_log_retention: bool 
 43   
 44      @undocumented: _fetch_cdn_data 
 45      @undocumented: _list_objects_raw 
 46      """ 
 53   
 54      name = property(fget=lambda self: self._name, fset=__set_name, 
 55          doc="the name of the container (read-only)") 
 56   
 57 -    def __init__(self, connection=None, name=None, count=None, size=None, metadata=None): 
  58          """ 
 59          Containers will rarely if ever need to be instantiated directly by the 
 60          user. 
 61   
 62          Instead, use the L{create_container<Connection.create_container>}, 
 63          L{get_container<Connection.get_container>}, 
 64          L{list_containers<Connection.list_containers>} and 
 65          other methods on a valid Connection object. 
 66          """ 
 67          self._name = None 
 68          self.name = name 
 69          self.conn = connection 
 70          self.object_count = count 
 71          self.size_used = size 
 72          self.metadata = metadata 
 73          self.cdn_uri = None 
 74          self.cdn_ssl_uri = None 
 75          self.cdn_streaming_uri = None 
 76          self.cdn_ttl = None 
 77          self.cdn_log_retention = None 
 78          if self.metadata == None: 
 79              self.metadata = {} 
 80          if connection.cdn_enabled: 
 81              self._fetch_cdn_data() 
  82   
 83      @requires_name(InvalidContainerName) 
 98       
 99 -    def enable_static_web(self, index=None, listings=None, error=None, listings_css=None): 
 100          """ 
101          Enable static web for this Container 
102   
103          >>> container.enable_static_web('index.html', 'error.html', True, 'style.css') 
104   
105          @param index: The name of the index landing page 
106          @type index : str 
107          @param listings: A boolean value to enable listing. 
108          @type error: bool 
109          @param listings_css: The file to be used when applying CSS to the listing. 
110          @type listings_css: str 
111          @param error: The suffix to be used for 404 and 401 error pages. 
112          @type error: str 
113   
114          """ 
115          metadata = {'X-Container-Meta-Web-Index' : '', 
116                      'X-Container-Meta-Web-Listings' : '', 
117                      'X-Container-Meta-Web-Error' : '', 
118                      'X-Container-Meta-Web-Listings-CSS' : ''} 
119          if index is not None: 
120              metadata['X-Container-Meta-Web-Index'] = index 
121          if listings is not None: 
122              metadata['X-Container-Meta-Web-Listings'] = str(listings) 
123          if error is not None: 
124              metadata['X-Container-Meta-Web-Error'] = error 
125          if listings_css is not None: 
126              metadata['X-Container-Meta-Web-Listings-CSS'] = listings_css 
127          self.update_metadata(metadata) 
 128   
130          """ 
131          Disable static web for this Container 
132   
133          >>> container.disable_static_web() 
134          """ 
135          self.enable_static_web() 
 136   
138          """ 
139          Enable object versioning on this container 
140           
141          >>> container.enable_object_versioning('container_i_want_versions_to_go_to') 
142           
143          @param container_url: The container where versions will be stored 
144          @type container_name: str 
145          """ 
146          self.update_metadata({'X-Versions-Location' : container_name}) 
 147   
149          """ 
150          Disable object versioning on this container 
151   
152          >>> container.disable_object_versioning() 
153          """ 
154          self.update_metadata({'X-Versions-Location' : ''}) 
 155   
156      @requires_name(InvalidContainerName) 
158          """ 
159          Fetch the object's CDN data from the CDN service 
160          """ 
161          response = self.conn.cdn_request('HEAD', [self.name]) 
162          if response.status >= 200 and response.status < 300: 
163              for hdr in response.getheaders(): 
164                  if hdr[0].lower() == 'x-cdn-uri': 
165                      self.cdn_uri = hdr[1] 
166                  if hdr[0].lower() == 'x-ttl': 
167                      self.cdn_ttl = int(hdr[1]) 
168                  if hdr[0].lower() == 'x-cdn-ssl-uri': 
169                      self.cdn_ssl_uri = hdr[1] 
170                  if hdr[0].lower() == 'x-cdn-streaming-uri': 
171                      self.cdn_streaming_uri = hdr[1] 
172                  if hdr[0].lower() == 'x-log-retention': 
173                      self.cdn_log_retention = hdr[1] == "True" and True or False 
 174   
175      @requires_name(InvalidContainerName) 
177          """ 
178          Either publishes the current container to the CDN or updates its 
179          CDN attributes.  Requires CDN be enabled on the account. 
180   
181          >>> container.make_public(ttl=604800) # expire in 1 week 
182   
183          @param ttl: cache duration in seconds of the CDN server 
184          @type ttl: number 
185          """ 
186          if not self.conn.cdn_enabled: 
187              raise CDNNotEnabled() 
188          if self.cdn_uri: 
189              request_method = 'POST' 
190          else: 
191              request_method = 'PUT' 
192          hdrs = {'X-TTL': str(ttl), 'X-CDN-Enabled': 'True'} 
193          response = self.conn.cdn_request(request_method, \ 
194                                               [self.name], hdrs=hdrs) 
195          if (response.status < 200) or (response.status >= 300): 
196              raise ResponseError(response.status, response.reason) 
197          self.cdn_ttl = ttl 
198          for hdr in response.getheaders(): 
199              if hdr[0].lower() == 'x-cdn-uri': 
200                  self.cdn_uri = hdr[1] 
201              if hdr[0].lower() == 'x-cdn-ssl-uri': 
202                  self.cdn_ssl_uri = hdr[1] 
 203   
204      @requires_name(InvalidContainerName) 
206          """ 
207          Disables CDN access to this container. 
208          It may continue to be available until its TTL expires. 
209   
210          >>> container.make_private() 
211          """ 
212          if not self.conn.cdn_enabled: 
213              raise CDNNotEnabled() 
214          hdrs = {'X-CDN-Enabled': 'False'} 
215          self.cdn_uri = None 
216          response = self.conn.cdn_request('POST', [self.name], hdrs=hdrs) 
217          if (response.status < 200) or (response.status >= 300): 
218              raise ResponseError(response.status, response.reason) 
 219   
220      @requires_name(InvalidContainerName) 
222          """ 
223          Purge Edge cache for all object inside of this container. 
224          You will be notified by email if one is provided when the 
225          job completes. 
226   
227          >>> container.purge_from_cdn("user@dmain.com") 
228           
229          or 
230   
231          >>> container.purge_from_cdn("user@domain.com,user2@domain.com") 
232           
233          or 
234           
235          >>> container.purge_from_cdn() 
236           
237          @param email: A Valid email address 
238          @type email: str 
239          """ 
240          if not self.conn.cdn_enabled: 
241              raise CDNNotEnabled() 
242   
243          if email: 
244              hdrs = {"X-Purge-Email": email} 
245              response = self.conn.cdn_request('DELETE', [self.name], hdrs=hdrs) 
246          else: 
247              response = self.conn.cdn_request('DELETE', [self.name]) 
248   
249          if (response.status < 200) or (response.status >= 300): 
250              raise ResponseError(response.status, response.reason) 
 251   
252      @requires_name(InvalidContainerName) 
253 -    def log_retention(self, log_retention=consts.cdn_log_retention): 
 254          """ 
255          Enable CDN log retention on the container. If enabled logs will be 
256          periodically (at unpredictable intervals) compressed and uploaded to 
257          a ".CDN_ACCESS_LOGS" container in the form of 
258          "container_name/YYYY/MM/DD/HH/XXXX.gz". Requires CDN be enabled on the 
259          account. 
260   
261          >>> container.log_retention(True) 
262   
263          @param log_retention: Enable or disable logs retention. 
264          @type log_retention: bool 
265          """ 
266          if not self.conn.cdn_enabled: 
267              raise CDNNotEnabled() 
268   
269          hdrs = {'X-Log-Retention': log_retention} 
270          response = self.conn.cdn_request('POST', [self.name], hdrs=hdrs) 
271          if (response.status < 200) or (response.status >= 300): 
272              raise ResponseError(response.status, response.reason) 
273   
274          self.cdn_log_retention = log_retention 
 275   
277          """ 
278          Returns a boolean indicating whether or not this container is 
279          publically accessible via the CDN. 
280   
281          >>> container.is_public() 
282          False 
283          >>> container.make_public() 
284          >>> container.is_public() 
285          True 
286   
287          @rtype: bool 
288          @return: whether or not this container is published to the CDN 
289          """ 
290          if not self.conn.cdn_enabled: 
291              raise CDNNotEnabled() 
292          return self.cdn_uri is not None 
 293   
294      @requires_name(InvalidContainerName) 
296          """ 
297          Return the URI for this container, if it is publically 
298          accessible via the CDN. 
299   
300          >>> connection['container1'].public_uri() 
301          'http://c00061.cdn.cloudfiles.rackspacecloud.com' 
302   
303          @rtype: str 
304          @return: the public URI for this container 
305          """ 
306          if not self.is_public(): 
307              raise ContainerNotPublic() 
308          return self.cdn_uri 
 309   
310      @requires_name(InvalidContainerName) 
312          """ 
313          Return the SSL URI for this container, if it is publically 
314          accessible via the CDN. 
315   
316          >>> connection['container1'].public_ssl_uri() 
317          'https://c61.ssl.cf0.rackcdn.com' 
318   
319          @rtype: str 
320          @return: the public SSL URI for this container 
321          """ 
322          if not self.is_public(): 
323              raise ContainerNotPublic() 
324          return self.cdn_ssl_uri 
 325   
326      @requires_name(InvalidContainerName) 
328          """ 
329          Return the Streaming URI for this container, if it is publically 
330          accessible via the CDN. 
331   
332          >>> connection['container1'].public_ssl_uri() 
333          'https://c61.stream.rackcdn.com' 
334   
335          @rtype: str 
336          @return: the public Streaming URI for this container 
337          """ 
338          if not self.is_public(): 
339              raise ContainerNotPublic() 
340          return self.cdn_streaming_uri 
 341   
342      @requires_name(InvalidContainerName) 
344          """ 
345          Return an L{Object} instance, creating it if necessary. 
346   
347          When passed the name of an existing object, this method will 
348          return an instance of that object, otherwise it will create a 
349          new one. 
350   
351          >>> container.create_object('new_object') 
352          <cloudfiles.storage_object.Object object at 0xb778366c> 
353          >>> obj = container.create_object('new_object') 
354          >>> obj.name 
355          'new_object' 
356   
357          @type object_name: str 
358          @param object_name: the name of the object to create 
359          @rtype: L{Object} 
360          @return: an object representing the newly created storage object 
361          """ 
362          return Object(self, object_name) 
 363   
364      @requires_name(InvalidContainerName) 
365 -    def get_objects(self, prefix=None, limit=None, marker=None, 
366                      path=None, delimiter=None, **parms): 
 367          """ 
368          Return a result set of all Objects in the Container. 
369   
370          Keyword arguments are treated as HTTP query parameters and can 
371          be used to limit the result set (see the API documentation). 
372   
373          >>> container.get_objects(limit=2) 
374          ObjectResults: 2 objects 
375          >>> for obj in container.get_objects(): 
376          ...     print obj.name 
377          new_object 
378          old_object 
379   
380          @param prefix: filter the results using this prefix 
381          @type prefix: str 
382          @param limit: return the first "limit" objects found 
383          @type limit: int 
384          @param marker: return objects whose names are greater than "marker" 
385          @type marker: str 
386          @param path: return all objects in "path" 
387          @type path: str 
388          @param delimiter: use this character as a delimiter for subdirectories 
389          @type delimiter: char 
390   
391          @rtype: L{ObjectResults} 
392          @return: an iterable collection of all storage objects in the container 
393          """ 
394          return ObjectResults(self, self.list_objects_info( 
395                  prefix, limit, marker, path, delimiter, **parms)) 
 396   
397      @requires_name(InvalidContainerName) 
399          """ 
400          Return an L{Object} instance for an existing storage object. 
401   
402          If an object with a name matching object_name does not exist 
403          then a L{NoSuchObject} exception is raised. 
404   
405          >>> obj = container.get_object('old_object') 
406          >>> obj.name 
407          'old_object' 
408   
409          @param object_name: the name of the object to retrieve 
410          @type object_name: str 
411          @rtype: L{Object} 
412          @return: an Object representing the storage object requested 
413          """ 
414          return Object(self, object_name, force_exists=True) 
 415   
416      @requires_name(InvalidContainerName) 
417 -    def list_objects_info(self, prefix=None, limit=None, marker=None, 
418                            path=None, delimiter=None, **parms): 
 419          """ 
420          Return information about all objects in the Container. 
421   
422          Keyword arguments are treated as HTTP query parameters and can 
423          be used limit the result set (see the API documentation). 
424   
425          >>> conn['container1'].list_objects_info(limit=2) 
426          [{u'bytes': 4820, 
427            u'content_type': u'application/octet-stream', 
428            u'hash': u'db8b55400b91ce34d800e126e37886f8', 
429            u'last_modified': u'2008-11-05T00:56:00.406565', 
430            u'name': u'new_object'}, 
431           {u'bytes': 1896, 
432            u'content_type': u'application/octet-stream', 
433            u'hash': u'1b49df63db7bc97cd2a10e391e102d4b', 
434            u'last_modified': u'2008-11-05T00:56:27.508729', 
435            u'name': u'old_object'}] 
436   
437          @param prefix: filter the results using this prefix 
438          @type prefix: str 
439          @param limit: return the first "limit" objects found 
440          @type limit: int 
441          @param marker: return objects with names greater than "marker" 
442          @type marker: str 
443          @param path: return all objects in "path" 
444          @type path: str 
445          @param delimiter: use this character as a delimiter for subdirectories 
446          @type delimiter: char 
447   
448          @rtype: list({"name":"...", "hash":..., "size":..., "type":...}) 
449          @return: a list of all container info as dictionaries with the 
450                   keys "name", "hash", "size", and "type" 
451          """ 
452          parms['format'] = 'json' 
453          resp = self._list_objects_raw( 
454              prefix, limit, marker, path, delimiter, **parms) 
455          return json_loads(resp) 
 456   
457      @requires_name(InvalidContainerName) 
458 -    def list_objects(self, prefix=None, limit=None, marker=None, 
459                       path=None, delimiter=None, **parms): 
 460          """ 
461          Return names of all L{Object}s in the L{Container}. 
462   
463          Keyword arguments are treated as HTTP query parameters and can 
464          be used to limit the result set (see the API documentation). 
465   
466          >>> container.list_objects() 
467          ['new_object', 'old_object'] 
468   
469          @param prefix: filter the results using this prefix 
470          @type prefix: str 
471          @param limit: return the first "limit" objects found 
472          @type limit: int 
473          @param marker: return objects with names greater than "marker" 
474          @type marker: str 
475          @param path: return all objects in "path" 
476          @type path: str 
477          @param delimiter: use this character as a delimiter for subdirectories 
478          @type delimiter: char 
479   
480          @rtype: list(str) 
481          @return: a list of all container names 
482          """ 
483          resp = self._list_objects_raw(prefix=prefix, limit=limit, 
484                                        marker=marker, path=path, 
485                                        delimiter=delimiter, **parms) 
486          return resp.splitlines() 
 487   
488      @requires_name(InvalidContainerName) 
489 -    def _list_objects_raw(self, prefix=None, limit=None, marker=None, 
490                            path=None, delimiter=None, **parms): 
 491          """ 
492          Returns a chunk list of storage object info. 
493          """ 
494          if prefix: 
495              parms['prefix'] = prefix 
496          if limit: 
497              parms['limit'] = limit 
498          if marker: 
499              parms['marker'] = marker 
500          if delimiter: 
501              parms['delimiter'] = delimiter 
502          if not path is None: 
503              parms['path'] = path   
504          response = self.conn.make_request('GET', [self.name], parms=parms) 
505          if (response.status < 200) or (response.status > 299): 
506              response.read() 
507              raise ResponseError(response.status, response.reason) 
508          return response.read() 
 509   
512   
515   
516      @requires_name(InvalidContainerName) 
518          """ 
519          Permanently remove a storage object. 
520   
521          >>> container.list_objects() 
522          ['new_object', 'old_object'] 
523          >>> container.delete_object('old_object') 
524          >>> container.list_objects() 
525          ['new_object'] 
526   
527          @param object_name: the name of the object to retrieve 
528          @type object_name: str 
529          """ 
530          if isinstance(object_name, Object): 
531              object_name = object_name.name 
532          if not object_name: 
533              raise InvalidObjectName(object_name) 
534          response = self.conn.make_request('DELETE', [self.name, object_name]) 
535          if (response.status < 200) or (response.status > 299): 
536              response.read() 
537              raise ResponseError(response.status, response.reason) 
538          response.read() 
  539   
542      """ 
543      An iterable results set object for Containers. 
544   
545      This class implements dictionary- and list-like interfaces. 
546      """ 
547 -    def __init__(self, conn, containers=list()): 
 548          self._containers = containers 
549          self._names = [k['name'] for k in containers] 
550          self.conn = conn 
 551   
553          return Container(self.conn, 
554                           self._containers[key]['name'], 
555                           self._containers[key]['count'], 
556                           self._containers[key]['bytes']) 
 557   
559          return [Container(self.conn, k['name'], k['count'], \ 
560                                k['size']) for k in self._containers[i:j]] 
 561   
563          return item in self._names 
 564   
566          return 'ContainerResults: %s containers' % len(self._containers) 
 567      __str__ = __repr__ 
568   
570          return len(self._containers) 
 571   
572 -    def index(self, value, *args): 
 573          """ 
574          returns an integer for the first index of value 
575          """ 
576          return self._names.index(value, *args) 
 577   
579          """ 
580          returns the number of occurrences of value 
581          """ 
582          return self._names.count(value) 
  583   
584   
585