2054a0d0de6b22dd2f7bafc52a846e16550a8922
[mediagoblin.git] / mediagoblin / storage / cloudfiles.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 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 # the Mime Type webm doesn't exists, let's add it
46 mimetypes.add_type("video/webm", "webm")
47
48 if not self.param_host:
49 print('No CloudFiles host URL specified, '
50 'defaulting to Rackspace US')
51
52 self.connection = cloudfiles.get_connection(
53 username=self.param_user,
54 api_key=self.param_api_key,
55 servicenet=True if self.param_use_servicenet == 'true' or \
56 self.param_use_servicenet == True else False)
57
58 if not self.param_container == \
59 self.connection.get_container(self.param_container):
60 self.container = self.connection.create_container(
61 self.param_container)
62 self.container.make_public(
63 ttl=60 * 60 * 2)
64 else:
65 self.container = self.connection.get_container(
66 self.param_container)
67
68 self.container_uri = self.container.public_uri()
69
70 def _resolve_filepath(self, filepath):
71 return '/'.join(
72 clean_listy_filepath(filepath))
73
74 def file_exists(self, filepath):
75 try:
76 object = self.container.get_object(
77 self._resolve_filepath(filepath))
78 return True
79 except cloudfiles.errors.NoSuchObject:
80 return False
81
82 def get_file(self, filepath, *args, **kwargs):
83 """
84 - Doesn't care about the "mode" argument
85 """
86 try:
87 obj = self.container.get_object(
88 self._resolve_filepath(filepath))
89 except cloudfiles.errors.NoSuchObject:
90 obj = self.container.create_object(
91 self._resolve_filepath(filepath))
92
93 mimetype = mimetypes.guess_type(
94 filepath[-1])
95
96 if mimetype:
97 obj.content_type = mimetype[0]
98 # this should finally fix the bug #429
99 meta_data = {'mime-type' : mimetype}
100 obj.metadata = meta_data
101
102 return CloudFilesStorageObjectWrapper(obj, *args, **kwargs)
103
104 def delete_file(self, filepath):
105 # TODO: Also delete unused directories if empty (safely, with
106 # checks to avoid race conditions).
107 try:
108 self.container.delete_object(
109 self._resolve_filepath(filepath))
110 except cloudfiles.container.ResponseError:
111 pass
112 finally:
113 pass
114
115
116 def file_url(self, filepath):
117 return '/'.join([
118 self.container_uri,
119 self._resolve_filepath(filepath)])
120
121
122 class CloudFilesStorageObjectWrapper():
123 """
124 Wrapper for python-cloudfiles's cloudfiles.storage_object.Object
125 used to circumvent the mystic `medium.jpg` corruption issue, where
126 we had both python-cloudfiles and PIL doing buffering on both
127 ends and that breaking things.
128
129 This wrapper currently meets mediagoblin's needs for a public_store
130 file-like object.
131 """
132 def __init__(self, storage_object, *args, **kwargs):
133 self.storage_object = storage_object
134
135 def read(self, *args, **kwargs):
136 return self.storage_object.read(*args, **kwargs)
137
138 def write(self, data, *args, **kwargs):
139 """
140 write data to the cloudfiles storage object
141
142 The original motivation for this wrapper is to ensure
143 that buffered writing to a cloudfiles storage object does not overwrite
144 any preexisting data.
145
146 Currently this method does not support any write modes except "append".
147 However if we should need it it would be easy implement.
148 """
149 if self.storage_object.size and type(data) == str:
150 data = self.read() + data
151
152 self.storage_object.write(data, *args, **kwargs)
153
154 def close(self):
155 pass
156
157 def __enter__(self):
158 """
159 Context Manager API implementation
160 http://docs.python.org/library/stdtypes.html#context-manager-types
161 """
162 return self
163
164 def __exit__(self, *exc_info):
165 """
166 Context Manger API implementation
167 see self.__enter__()
168 """
169 self.close()