Fixed attachments
[mediagoblin.git] / mediagoblin / storage / cloudfiles.py
CommitLineData
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'''
18Make it so that ``import cloudfiles`` does not pick THIS file, but the
19python-cloudfiles one.
20
21http://docs.python.org/whatsnew/2.5.html#pep-328-absolute-and-relative-imports
22'''
23from __future__ import absolute_import
24
25from mediagoblin.storage import StorageInterface, clean_listy_filepath
26
27import cloudfiles
28import mimetypes
a07d052f
JW
29import logging
30
31_log = logging.getLogger(__name__)
a2468d18 32
243c3843 33
a2468d18
JW
34class 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
134class 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()