Merge remote-tracking branch 'is_derek/bug405_email_notifications_for_comments' into...
[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 self.container.get_object( self._resolve_filepath(filepath))
77 return True
78 except cloudfiles.errors.NoSuchObject:
79 return False
80
81 def get_file(self, filepath, *args, **kwargs):
82 """
83 - Doesn't care about the "mode" argument
84 """
85 try:
86 obj = self.container.get_object(
87 self._resolve_filepath(filepath))
88 except cloudfiles.errors.NoSuchObject:
89 obj = self.container.create_object(
90 self._resolve_filepath(filepath))
91
92 mimetype = mimetypes.guess_type(
93 filepath[-1])
94
95 if mimetype:
96 obj.content_type = mimetype[0]
97 # this should finally fix the bug #429
98 meta_data = {'mime-type' : mimetype[0]}
99 obj.metadata = meta_data
100
101 return CloudFilesStorageObjectWrapper(obj, *args, **kwargs)
102
103 def delete_file(self, filepath):
104 # TODO: Also delete unused directories if empty (safely, with
105 # checks to avoid race conditions).
106 try:
107 self.container.delete_object(
108 self._resolve_filepath(filepath))
109 except cloudfiles.container.ResponseError:
110 pass
111 finally:
112 pass
113
114
115 def file_url(self, filepath):
116 return '/'.join([
117 self.container_uri,
118 self._resolve_filepath(filepath)])
119
120
121 class CloudFilesStorageObjectWrapper():
122 """
123 Wrapper for python-cloudfiles's cloudfiles.storage_object.Object
124 used to circumvent the mystic `medium.jpg` corruption issue, where
125 we had both python-cloudfiles and PIL doing buffering on both
126 ends and that breaking things.
127
128 This wrapper currently meets mediagoblin's needs for a public_store
129 file-like object.
130 """
131 def __init__(self, storage_object, *args, **kwargs):
132 self.storage_object = storage_object
133
134 def read(self, *args, **kwargs):
135 return self.storage_object.read(*args, **kwargs)
136
137 def write(self, data, *args, **kwargs):
138 """
139 write data to the cloudfiles storage object
140
141 The original motivation for this wrapper is to ensure
142 that buffered writing to a cloudfiles storage object does not overwrite
143 any preexisting data.
144
145 Currently this method does not support any write modes except "append".
146 However if we should need it it would be easy implement.
147 """
148 if self.storage_object.size and type(data) == str:
149 data = self.read() + data
150
151 self.storage_object.write(data, *args, **kwargs)
152
153 def close(self):
154 pass
155
156 def __enter__(self):
157 """
158 Context Manager API implementation
159 http://docs.python.org/library/stdtypes.html#context-manager-types
160 """
161 return self
162
163 def __exit__(self, *exc_info):
164 """
165 Context Manger API implementation
166 see self.__enter__()
167 """
168 self.close()