-update to latest master
[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.queued_filename = None
90
91 def set_workbench(self, wb):
92 self.workbench = wb
93
94 def get_queued_filename(self):
95 """
96 Get the a filename for the original, on local storage
97 """
98 if self.queued_filename is not None:
99 return self.queued_filename
100 queued_filepath = self.entry.queued_media_file
101 queued_filename = self.workbench.localized_file(
102 mgg.queue_store, queued_filepath,
103 'source')
104 self.queued_filename = queued_filename
105 return queued_filename
106
107 def copy_original(self, target_name, keyname=u"original"):
108 self.store_public(keyname, self.get_queued_filename(), target_name)
109
110 def store_public(self, keyname, local_file, target_name=None):
111 if target_name is None:
112 target_name = os.path.basename(local_file)
113 target_filepath = create_pub_filepath(self.entry, target_name)
114 if keyname in self.entry.media_files:
115 _log.warn("store_public: keyname %r already used for file %r, "
116 "replacing with %r", keyname,
117 self.entry.media_files[keyname], target_filepath)
118 mgg.public_store.copy_local_to_storage(local_file, target_filepath)
119 self.entry.media_files[keyname] = target_filepath
120
121 def delete_queue_file(self):
122 # Remove queued media file from storage and database.
123 # queued_filepath is in the task_id directory which should
124 # be removed too, but fail if the directory is not empty to be on
125 # the super-safe side.
126 queued_filepath = self.entry.queued_media_file
127 mgg.queue_store.delete_file(queued_filepath) # rm file
128 mgg.queue_store.delete_dir(queued_filepath[:-1]) # rm dir
129 self.entry.queued_media_file = []
130
131
132 def mark_entry_failed(entry_id, exc):
133 """
134 Mark a media entry as having failed in its conversion.
135
136 Uses the exception that was raised to mark more information. If
137 the exception is a derivative of BaseProcessingFail then we can
138 store extra information that can be useful for users telling them
139 why their media failed to process.
140
141 Args:
142 - entry_id: The id of the media entry
143
144 """
145 # Was this a BaseProcessingFail? In other words, was this a
146 # type of error that we know how to handle?
147 if isinstance(exc, BaseProcessingFail):
148 # Looks like yes, so record information about that failure and any
149 # metadata the user might have supplied.
150 atomic_update(mgg.database.MediaEntry,
151 {'id': entry_id},
152 {u'state': u'failed',
153 u'fail_error': unicode(exc.exception_path),
154 u'fail_metadata': exc.metadata})
155 else:
156 _log.warn("No idea what happened here, but it failed: %r", exc)
157 # Looks like no, so just mark it as failed and don't record a
158 # failure_error (we'll assume it wasn't handled) and don't record
159 # metadata (in fact overwrite it if somehow it had previous info
160 # here)
161 atomic_update(mgg.database.MediaEntry,
162 {'id': entry_id},
163 {u'state': u'failed',
164 u'fail_error': None,
165 u'fail_metadata': {}})
166
167
168 class BaseProcessingFail(Exception):
169 """
170 Base exception that all other processing failure messages should
171 subclass from.
172
173 You shouldn't call this itself; instead you should subclass it
174 and provid the exception_path and general_message applicable to
175 this error.
176 """
177 general_message = u''
178
179 @property
180 def exception_path(self):
181 return u"%s:%s" % (
182 self.__class__.__module__, self.__class__.__name__)
183
184 def __init__(self, **metadata):
185 self.metadata = metadata or {}
186
187 class BadMediaFail(BaseProcessingFail):
188 """
189 Error that should be raised when an inappropriate file was given
190 for the media type specified.
191 """
192 general_message = _(u'Invalid file given for media type.')