Commit | Line | Data |
---|---|---|
a2468d18 | 1 | # GNU MediaGoblin -- federated, autonomous media hosting |
cf29e8a8 | 2 | # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. |
a2468d18 JW |
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 | |
a07d052f JW |
29 | import logging |
30 | ||
31 | _log = logging.getLogger(__name__) | |
a2468d18 | 32 | |
243c3843 | 33 | |
a2468d18 JW |
34 | class CloudFilesStorage(StorageInterface): |
35 | ''' | |
36 | OpenStack/Rackspace Cloud's Swift/CloudFiles support | |
37 | ''' | |
38 | ||
39 | local_storage = False | |
40 | ||
41 | def __init__(self, **kwargs): | |
42 | self.param_container = kwargs.get('cloudfiles_container') | |
43 | self.param_user = kwargs.get('cloudfiles_user') | |
44 | self.param_api_key = kwargs.get('cloudfiles_api_key') | |
45 | self.param_host = kwargs.get('cloudfiles_host') | |
46 | self.param_use_servicenet = kwargs.get('cloudfiles_use_servicenet') | |
47 | ||
ea42790b S |
48 | # the Mime Type webm doesn't exists, let's add it |
49 | mimetypes.add_type("video/webm", "webm") | |
50 | ||
a2468d18 | 51 | if not self.param_host: |
a07d052f | 52 | _log.info('No CloudFiles host URL specified, ' |
a2468d18 JW |
53 | 'defaulting to Rackspace US') |
54 | ||
55 | self.connection = cloudfiles.get_connection( | |
56 | username=self.param_user, | |
57 | api_key=self.param_api_key, | |
58 | servicenet=True if self.param_use_servicenet == 'true' or \ | |
59 | self.param_use_servicenet == True else False) | |
60 | ||
a07d052f JW |
61 | _log.debug('Connected to {0} (auth: {1})'.format( |
62 | self.connection.connection.host, | |
63 | self.connection.auth.host)) | |
64 | ||
a2468d18 JW |
65 | if not self.param_container == \ |
66 | self.connection.get_container(self.param_container): | |
67 | self.container = self.connection.create_container( | |
68 | self.param_container) | |
69 | self.container.make_public( | |
70 | ttl=60 * 60 * 2) | |
71 | else: | |
72 | self.container = self.connection.get_container( | |
73 | self.param_container) | |
74 | ||
a07d052f JW |
75 | _log.debug('Container: {0}'.format( |
76 | self.container.name)) | |
77 | ||
a2468d18 JW |
78 | self.container_uri = self.container.public_uri() |
79 | ||
80 | def _resolve_filepath(self, filepath): | |
81 | return '/'.join( | |
82 | clean_listy_filepath(filepath)) | |
83 | ||
84 | def file_exists(self, filepath): | |
85 | try: | |
a07d052f | 86 | self.container.get_object(self._resolve_filepath(filepath)) |
a2468d18 JW |
87 | return True |
88 | except cloudfiles.errors.NoSuchObject: | |
89 | return False | |
90 | ||
91 | def get_file(self, filepath, *args, **kwargs): | |
92 | """ | |
a07d052f | 93 | - Doesn't care about the "mode" argument. |
a2468d18 JW |
94 | """ |
95 | try: | |
96 | obj = self.container.get_object( | |
97 | self._resolve_filepath(filepath)) | |
98 | except cloudfiles.errors.NoSuchObject: | |
99 | obj = self.container.create_object( | |
100 | self._resolve_filepath(filepath)) | |
101 | ||
a07d052f JW |
102 | # Detect the mimetype ourselves, since some extensions (webm) |
103 | # may not be universally accepted as video/webm | |
a2468d18 JW |
104 | mimetype = mimetypes.guess_type( |
105 | filepath[-1]) | |
106 | ||
c43f8c1d | 107 | if mimetype[0]: |
a07d052f | 108 | # Set the mimetype on the CloudFiles object |
a2468d18 | 109 | obj.content_type = mimetype[0] |
a07d052f | 110 | obj.metadata = {'mime-type': mimetype[0]} |
c43f8c1d JW |
111 | else: |
112 | obj.content_type = 'application/octet-stream' | |
113 | obj.metadata = {'mime-type': 'application/octet-stream'} | |
a2468d18 JW |
114 | |
115 | return CloudFilesStorageObjectWrapper(obj, *args, **kwargs) | |
116 | ||
117 | def delete_file(self, filepath): | |
118 | # TODO: Also delete unused directories if empty (safely, with | |
119 | # checks to avoid race conditions). | |
93bdab9d JW |
120 | try: |
121 | self.container.delete_object( | |
122 | self._resolve_filepath(filepath)) | |
123 | except cloudfiles.container.ResponseError: | |
124 | pass | |
125 | finally: | |
126 | pass | |
a2468d18 JW |
127 | |
128 | def file_url(self, filepath): | |
129 | return '/'.join([ | |
130 | self.container_uri, | |
131 | self._resolve_filepath(filepath)]) | |
132 | ||
133 | ||
134 | class CloudFilesStorageObjectWrapper(): | |
135 | """ | |
136 | Wrapper for python-cloudfiles's cloudfiles.storage_object.Object | |
137 | used to circumvent the mystic `medium.jpg` corruption issue, where | |
138 | we had both python-cloudfiles and PIL doing buffering on both | |
a07d052f | 139 | ends and causing breakage. |
a2468d18 JW |
140 | |
141 | This wrapper currently meets mediagoblin's needs for a public_store | |
142 | file-like object. | |
143 | """ | |
144 | def __init__(self, storage_object, *args, **kwargs): | |
145 | self.storage_object = storage_object | |
146 | ||
147 | def read(self, *args, **kwargs): | |
a07d052f JW |
148 | _log.debug('Reading {0}'.format( |
149 | self.storage_object.name)) | |
a2468d18 JW |
150 | return self.storage_object.read(*args, **kwargs) |
151 | ||
152 | def write(self, data, *args, **kwargs): | |
153 | """ | |
154 | write data to the cloudfiles storage object | |
155 | ||
156 | The original motivation for this wrapper is to ensure | |
157 | that buffered writing to a cloudfiles storage object does not overwrite | |
158 | any preexisting data. | |
159 | ||
160 | Currently this method does not support any write modes except "append". | |
161 | However if we should need it it would be easy implement. | |
162 | """ | |
163 | if self.storage_object.size and type(data) == str: | |
a07d052f JW |
164 | _log.debug('{0} is > 0 in size, appending data'.format( |
165 | self.storage_object.name)) | |
a2468d18 JW |
166 | data = self.read() + data |
167 | ||
a07d052f JW |
168 | _log.debug('Writing {0}'.format( |
169 | self.storage_object.name)) | |
a2468d18 JW |
170 | self.storage_object.write(data, *args, **kwargs) |
171 | ||
172 | def close(self): | |
a07d052f JW |
173 | """ |
174 | Not implemented. | |
175 | """ | |
a2468d18 JW |
176 | pass |
177 | ||
178 | def __enter__(self): | |
179 | """ | |
180 | Context Manager API implementation | |
181 | http://docs.python.org/library/stdtypes.html#context-manager-types | |
182 | """ | |
183 | return self | |
184 | ||
185 | def __exit__(self, *exc_info): | |
186 | """ | |
187 | Context Manger API implementation | |
188 | see self.__enter__() | |
189 | """ | |
190 | self.close() |