Merge branch 'master' of git://gitorious.org/mediagoblin/mediagoblin
[mediagoblin.git] / mediagoblin / storage / cloudfiles.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011 MediaGoblin contributors. See AUTHORS.
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17 '''
18 Make it so that ``import cloudfiles`` does not pick THIS file, but the
19 python-cloudfiles one.
20
21 http://docs.python.org/whatsnew/2.5.html#pep-328-absolute-and-relative-imports
22 '''
23 from __future__ import absolute_import
24
25 from mediagoblin.storage import StorageInterface, clean_listy_filepath
26
27 import cloudfiles
28 import mimetypes
29
30
31 class CloudFilesStorage(StorageInterface):
32 '''
33 OpenStack/Rackspace Cloud's Swift/CloudFiles support
34 '''
35
36 local_storage = False
37
38 def __init__(self, **kwargs):
39 self.param_container = kwargs.get('cloudfiles_container')
40 self.param_user = kwargs.get('cloudfiles_user')
41 self.param_api_key = kwargs.get('cloudfiles_api_key')
42 self.param_host = kwargs.get('cloudfiles_host')
43 self.param_use_servicenet = kwargs.get('cloudfiles_use_servicenet')
44
45 if not self.param_host:
46 print('No CloudFiles host URL specified, '
47 'defaulting to Rackspace US')
48
49 self.connection = cloudfiles.get_connection(
50 username=self.param_user,
51 api_key=self.param_api_key,
52 servicenet=True if self.param_use_servicenet == 'true' or \
53 self.param_use_servicenet == True else False)
54
55 if not self.param_container == \
56 self.connection.get_container(self.param_container):
57 self.container = self.connection.create_container(
58 self.param_container)
59 self.container.make_public(
60 ttl=60 * 60 * 2)
61 else:
62 self.container = self.connection.get_container(
63 self.param_container)
64
65 self.container_uri = self.container.public_uri()
66
67 def _resolve_filepath(self, filepath):
68 return '/'.join(
69 clean_listy_filepath(filepath))
70
71 def file_exists(self, filepath):
72 try:
73 object = self.container.get_object(
74 self._resolve_filepath(filepath))
75 return True
76 except cloudfiles.errors.NoSuchObject:
77 return False
78
79 def get_file(self, filepath, *args, **kwargs):
80 """
81 - Doesn't care about the "mode" argument
82 """
83 try:
84 obj = self.container.get_object(
85 self._resolve_filepath(filepath))
86 except cloudfiles.errors.NoSuchObject:
87 obj = self.container.create_object(
88 self._resolve_filepath(filepath))
89
90 mimetype = mimetypes.guess_type(
91 filepath[-1])
92
93 if mimetype:
94 obj.content_type = mimetype[0]
95
96 return CloudFilesStorageObjectWrapper(obj, *args, **kwargs)
97
98 def delete_file(self, filepath):
99 # TODO: Also delete unused directories if empty (safely, with
100 # checks to avoid race conditions).
101 self.container.delete_object(
102 self._resolve_filepath(filepath))
103
104 def file_url(self, filepath):
105 return '/'.join([
106 self.container_uri,
107 self._resolve_filepath(filepath)])
108
109
110 class CloudFilesStorageObjectWrapper():
111 """
112 Wrapper for python-cloudfiles's cloudfiles.storage_object.Object
113 used to circumvent the mystic `medium.jpg` corruption issue, where
114 we had both python-cloudfiles and PIL doing buffering on both
115 ends and that breaking things.
116
117 This wrapper currently meets mediagoblin's needs for a public_store
118 file-like object.
119 """
120 def __init__(self, storage_object, *args, **kwargs):
121 self.storage_object = storage_object
122
123 def read(self, *args, **kwargs):
124 return self.storage_object.read(*args, **kwargs)
125
126 def write(self, data, *args, **kwargs):
127 """
128 write data to the cloudfiles storage object
129
130 The original motivation for this wrapper is to ensure
131 that buffered writing to a cloudfiles storage object does not overwrite
132 any preexisting data.
133
134 Currently this method does not support any write modes except "append".
135 However if we should need it it would be easy implement.
136 """
137 if self.storage_object.size and type(data) == str:
138 data = self.read() + data
139
140 self.storage_object.write(data, *args, **kwargs)
141
142 def close(self):
143 pass
144
145 def __enter__(self):
146 """
147 Context Manager API implementation
148 http://docs.python.org/library/stdtypes.html#context-manager-types
149 """
150 return self
151
152 def __exit__(self, *exc_info):
153 """
154 Context Manger API implementation
155 see self.__enter__()
156 """
157 self.close()