5ce9281b584ed334e5e8707ca4dc607eb7745fd6
[mediagoblin.git] / mediagoblin / processing / __init__.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 import logging
18 import os
19
20 from mediagoblin.db.util import atomic_update
21 from mediagoblin import mg_globals as mgg
22
23 from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
24
25 _log = logging.getLogger(__name__)
26
27
28 class ProgressCallback(object):
29 def __init__(self, entry):
30 self.entry = entry
31
32 def __call__(self, progress):
33 if progress:
34 self.entry.transcoding_progress = progress
35 self.entry.save()
36
37
38 def create_pub_filepath(entry, filename):
39 return mgg.public_store.get_unique_filepath(
40 ['media_entries',
41 unicode(entry.id),
42 filename])
43
44
45 class FilenameBuilder(object):
46 """Easily slice and dice filenames.
47
48 Initialize this class with an original file path, then use the fill()
49 method to create new filenames based on the original.
50
51 """
52 MAX_FILENAME_LENGTH = 255 # VFAT's maximum filename length
53
54 def __init__(self, path):
55 """Initialize a builder from an original file path."""
56 self.dirpath, self.basename = os.path.split(path)
57 self.basename, self.ext = os.path.splitext(self.basename)
58 self.ext = self.ext.lower()
59
60 def fill(self, fmtstr):
61 """Build a new filename based on the original.
62
63 The fmtstr argument can include the following:
64 {basename} -- the original basename, with the extension removed
65 {ext} -- the original extension, always lowercase
66
67 If necessary, {basename} will be truncated so the filename does not
68 exceed this class' MAX_FILENAME_LENGTH in length.
69
70 """
71 basename_len = (self.MAX_FILENAME_LENGTH -
72 len(fmtstr.format(basename='', ext=self.ext)))
73 return fmtstr.format(basename=self.basename[:basename_len],
74 ext=self.ext)
75
76
77 class ProcessingState(object):
78 """
79 The first and only argument to the "processor" of a media type
80
81 This could be thought of as a "request" to the processor
82 function. It has the main info for the request (media entry)
83 and a bunch of tools for the request on it.
84 It can get more fancy without impacting old media types.
85 """
86 def __init__(self, entry):
87 self.entry = entry
88 self.workbench = None
89 self.orig_filename = None
90
91 def set_workbench(self, wb):
92 self.workbench = wb
93
94 def get_orig_filename(self):
95 """
96 Get the a filename for the original, on local storage
97
98 If the media entry has a queued_media_file, use that, otherwise
99 use the original.
100
101 In the future, this will return the highest quality file available
102 if neither the original or queued file are available
103 """
104 if self.orig_filename is not None:
105 return self.orig_filename
106
107 if self.entry.queued_media_file:
108 orig_filepath = self.entry.queued_media_file
109 storage = mgg.queue_store
110 else:
111 orig_filepath = self.entry.media_files['original']
112 storage = mgg.public_store
113
114 orig_filename = self.workbench.localized_file(
115 storage, orig_filepath,
116 'source')
117 self.orig_filename = orig_filename
118 return orig_filename
119
120 def copy_original(self, target_name, keyname=u"original"):
121 self.store_public(keyname, self.get_orig_filename(), target_name)
122
123 def store_public(self, keyname, local_file, target_name=None):
124 if target_name is None:
125 target_name = os.path.basename(local_file)
126 target_filepath = create_pub_filepath(self.entry, target_name)
127 if keyname in self.entry.media_files:
128 _log.warn("store_public: keyname %r already used for file %r, "
129 "replacing with %r", keyname,
130 self.entry.media_files[keyname], target_filepath)
131 mgg.public_store.copy_local_to_storage(local_file, target_filepath)
132 self.entry.media_files[keyname] = target_filepath
133
134 def delete_queue_file(self):
135 # Remove queued media file from storage and database.
136 # queued_filepath is in the task_id directory which should
137 # be removed too, but fail if the directory is not empty to be on
138 # the super-safe side.
139 queued_filepath = self.entry.queued_media_file
140 mgg.queue_store.delete_file(queued_filepath) # rm file
141 mgg.queue_store.delete_dir(queued_filepath[:-1]) # rm dir
142 self.entry.queued_media_file = []
143
144
145 def mark_entry_failed(entry_id, exc):
146 """
147 Mark a media entry as having failed in its conversion.
148
149 Uses the exception that was raised to mark more information. If
150 the exception is a derivative of BaseProcessingFail then we can
151 store extra information that can be useful for users telling them
152 why their media failed to process.
153
154 Args:
155 - entry_id: The id of the media entry
156
157 """
158 # Was this a BaseProcessingFail? In other words, was this a
159 # type of error that we know how to handle?
160 if isinstance(exc, BaseProcessingFail):
161 # Looks like yes, so record information about that failure and any
162 # metadata the user might have supplied.
163 atomic_update(mgg.database.MediaEntry,
164 {'id': entry_id},
165 {u'state': u'failed',
166 u'fail_error': unicode(exc.exception_path),
167 u'fail_metadata': exc.metadata})
168 else:
169 _log.warn("No idea what happened here, but it failed: %r", exc)
170 # Looks like no, so just mark it as failed and don't record a
171 # failure_error (we'll assume it wasn't handled) and don't record
172 # metadata (in fact overwrite it if somehow it had previous info
173 # here)
174 atomic_update(mgg.database.MediaEntry,
175 {'id': entry_id},
176 {u'state': u'failed',
177 u'fail_error': None,
178 u'fail_metadata': {}})
179
180
181 class BaseProcessingFail(Exception):
182 """
183 Base exception that all other processing failure messages should
184 subclass from.
185
186 You shouldn't call this itself; instead you should subclass it
187 and provid the exception_path and general_message applicable to
188 this error.
189 """
190 general_message = u''
191
192 @property
193 def exception_path(self):
194 return u"%s:%s" % (
195 self.__class__.__module__, self.__class__.__name__)
196
197 def __init__(self, **metadata):
198 self.metadata = metadata or {}
199
200
201 class BadMediaFail(BaseProcessingFail):
202 """
203 Error that should be raised when an inappropriate file was given
204 for the media type specified.
205 """
206 general_message = _(u'Invalid file given for media type.')