1
2
3 """This module encapsulates mime operations.
4
5 http://www.dwheeler.com/essays/open-files-urls.html
6 """
7
8 __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>"
9 __license__ = "GPL"
10
11
12 import sys
13 import os
14 import mailcap
15 import mimetypes
16 import subprocess
17 import shutil
18 import logging
19 import io
20
21
22
23 if __name__ == '__main__':
24 sys.path.insert(0, '../../')
25 from Gnumed.pycommon import gmShellAPI
26 from Gnumed.pycommon import gmTools
27 from Gnumed.pycommon import gmCfg2
28 from Gnumed.pycommon import gmWorkerThread
29
30
31 _log = logging.getLogger('gm.mime')
32
33
35 """Guess mime type of arbitrary file.
36
37 filenames are supposed to be in Unicode
38 """
39 worst_case = "application/octet-stream"
40 _log.debug('guessing mime type of [%s]', filename)
41
42 try:
43 import extractor
44 xtract = extractor.Extractor()
45 props = xtract.extract(filename = filename)
46 for prop, val in props:
47 if (prop == 'mimetype') and (val != worst_case):
48 return val
49 except ImportError:
50 _log.debug('module <extractor> (python wrapper for libextractor) not installed')
51 except OSError as exc:
52
53 if exc.errno != 22:
54 raise
55 _log.exception('module <extractor> (python wrapper for libextractor) not installed')
56
57 ret_code = -1
58
59
60
61 mime_guesser_cmd = 'file -i -b "%s"' % filename
62
63
64 aPipe = os.popen(mime_guesser_cmd, 'r')
65 if aPipe is None:
66 _log.debug("cannot open pipe to [%s]" % mime_guesser_cmd)
67 else:
68 pipe_output = aPipe.readline().replace('\n', '').strip()
69 ret_code = aPipe.close()
70 if ret_code is None:
71 _log.debug('[%s]: <%s>' % (mime_guesser_cmd, pipe_output))
72 if pipe_output not in ['', worst_case]:
73 return pipe_output.split(';')[0].strip()
74 else:
75 _log.error('[%s] on %s (%s): failed with exit(%s)' % (mime_guesser_cmd, os.name, sys.platform, ret_code))
76
77
78 mime_guesser_cmd = 'extract -p mimetype "%s"' % filename
79 aPipe = os.popen(mime_guesser_cmd, 'r')
80 if aPipe is None:
81 _log.debug("cannot open pipe to [%s]" % mime_guesser_cmd)
82 else:
83 pipe_output = aPipe.readline()[11:].replace('\n', '').strip()
84 ret_code = aPipe.close()
85 if ret_code is None:
86 _log.debug('[%s]: <%s>' % (mime_guesser_cmd, pipe_output))
87 if pipe_output not in ['', worst_case]:
88 return pipe_output
89 else:
90 _log.error('[%s] on %s (%s): failed with exit(%s)' % (mime_guesser_cmd, os.name, sys.platform, ret_code))
91
92
93
94
95
96 _log.info("OS level mime detection failed, falling back to built-in magic")
97
98 import gmMimeMagic
99 mime_type = gmTools.coalesce(gmMimeMagic.filedesc(filename), worst_case)
100 del gmMimeMagic
101 _log.debug('"%s" -> <%s>' % (filename, mime_type))
102 return mime_type
103
104
105 -def get_viewer_cmd(aMimeType = None, aFileName = None, aToken = None):
106 """Return command for viewer for this mime type complete with this file"""
107
108 if aFileName is None:
109 _log.error("You should specify a file name for the replacement of %s.")
110
111
112 aFileName = """%s"""
113
114 mailcaps = mailcap.getcaps()
115 (viewer, junk) = mailcap.findmatch(mailcaps, aMimeType, key = 'view', filename = '%s' % aFileName)
116
117
118 _log.debug("<%s> viewer: [%s]" % (aMimeType, viewer))
119
120 return viewer
121
122
124
125 if filename is None:
126 _log.error("You should specify a file name for the replacement of %s.")
127
128
129 filename = """%s"""
130
131 mailcaps = mailcap.getcaps()
132 (editor, junk) = mailcap.findmatch(mailcaps, mimetype, key = 'edit', filename = '%s' % filename)
133
134
135
136 _log.debug("<%s> editor: [%s]" % (mimetype, editor))
137
138 return editor
139
140
142 """Return file extension based on what the OS thinks a file of this mimetype should end in."""
143
144
145 ext = mimetypes.guess_extension(mimetype)
146 if ext is not None:
147 _log.debug('<%s>: %s' % (mimetype, ext))
148 return ext
149
150 _log.error("<%s>: no suitable file extension known to the OS" % mimetype)
151
152
153 cfg = gmCfg2.gmCfgData()
154 ext = cfg.get (
155 group = 'extensions',
156 option = mimetype,
157 source_order = [('user-mime', 'return'), ('system-mime', 'return')]
158 )
159
160 if ext is not None:
161 _log.debug('<%s>: %s (%s)' % (mimetype, ext, candidate))
162 return ext
163
164 _log.error("<%s>: no suitable file extension found in config files" % mimetype)
165
166 return ext
167
168
170 if aFile is None:
171 return None
172
173 (path_name, f_ext) = os.path.splitext(aFile)
174 if f_ext != '':
175 return f_ext
176
177
178 mime_type = guess_mimetype(aFile)
179 f_ext = guess_ext_by_mimetype(mime_type)
180 if f_ext is None:
181 _log.error('unable to guess file extension for mime type [%s]' % mime_type)
182 return None
183
184 return f_ext
185
186
188 mimetype = guess_mimetype(filename)
189 mime_suffix = guess_ext_by_mimetype(mimetype)
190 if mime_suffix is None:
191 return filename
192 old_name, old_ext = os.path.splitext(filename)
193 if old_ext == '':
194 new_filename = filename + mime_suffix
195 elif old_ext.lower() == mime_suffix.lower():
196 return filename
197 new_filename = old_name + mime_suffix
198 _log.debug('[%s] -> [%s]', filename, new_filename)
199 try:
200 os.rename(filename, new_filename)
201 return new_filename
202 except OSError:
203 _log.exception('cannot rename, returning original filename')
204 return filename
205
206
207 _system_startfile_cmd = None
208
209 open_cmds = {
210 'xdg-open': 'xdg-open "%s"',
211 'kfmclient': 'kfmclient exec "%s"',
212 'gnome-open': 'gnome-open "%s"',
213 'exo-open': 'exo-open "%s"',
214 'op': 'op "%s"',
215 'open': 'open "%s"',
216 'cmd.exe': 'cmd.exe /c "%s"'
217
218
219 }
220
243
244
245 -def convert_file(filename=None, target_mime=None, target_filename=None, target_extension=None, verbose=False):
246 """Convert file from one format into another.
247
248 target_mime: a mime type
249 """
250 assert (target_mime is not None), '<target_mime> must not be None'
251 assert (filename is not None), '<filename> must not be None'
252 assert (filename != target_filename), '<target_filename> must be different from <filename>'
253
254 source_mime = guess_mimetype(filename = filename)
255 if source_mime.lower() == target_mime.lower():
256 _log.debug('source file [%s] already target mime type [%s]', filename, target_mime)
257 if target_filename is None:
258 return filename
259
260 shutil.copyfile(filename, target_filename)
261 return target_filename
262
263 converted_ext = guess_ext_by_mimetype(target_mime)
264 if converted_ext is None:
265 if target_filename is not None:
266 tmp, converted_ext = os.path.splitext(target_filename)
267 if converted_ext is None:
268 converted_ext = target_extension
269 converted_fname = gmTools.get_unique_filename(suffix = converted_ext)
270 _log.debug('attempting conversion: [%s] -> [<%s>:%s]', filename, target_mime, gmTools.coalesce(target_filename, converted_fname))
271 script_name = 'gm-convert_file'
272 paths = gmTools.gmPaths()
273 local_script = os.path.join(paths.local_base_dir, '..', 'external-tools', script_name)
274 candidates = [ script_name, local_script ]
275 found, binary = gmShellAPI.find_first_binary(binaries = candidates)
276 if not found:
277
278 binary = script_name
279 _log.debug('<%s> API: SOURCEFILE TARGET_MIMETYPE TARGET_EXTENSION TARGET_FILENAME' % binary)
280 cmd_line = [
281 binary,
282 filename,
283 target_mime,
284 converted_ext.lstrip('.'),
285 converted_fname
286 ]
287 success, returncode, stdout = gmShellAPI.run_process(cmd_line = cmd_line, verbose = True)
288 if not success:
289 _log.error('conversion returned error exit code')
290 if not os.path.exists(converted_fname):
291 return None
292 _log.info('conversion target file found')
293 stats = os.stat(converted_fname)
294 if stats.st_size == 0:
295 return None
296 _log.info('conversion target file size > 0')
297 achieved_mime = guess_mimetype(filename = converted_fname)
298 if achieved_mime != target_mime:
299 _log.error('target: [%s], achieved: [%s]', target_mime, achieved_mime)
300 return None
301 _log.info('conversion target file mime type [%s], as expected, might be usable', achieved_mime)
302
303
304 if target_filename is None:
305 return converted_fname
306
307 shutil.copyfile(converted_fname, target_filename)
308 return target_filename
309
310
312 base_name = 'gm-describe_file'
313 paths = gmTools.gmPaths()
314 local_script = os.path.join(paths.local_base_dir, '..', 'external-tools', base_name)
315 candidates = [base_name, local_script]
316 found, binary = gmShellAPI.find_first_binary(binaries = candidates)
317 if not found:
318 _log.error('cannot find <%s(.bat)>', base_name)
319 return (False, _('<%s(.bat)> not found') % base_name)
320
321 cmd_line = [binary, filename]
322 _log.debug('describing: %s', cmd_line)
323 try:
324 proc_result = subprocess.run (
325 args = cmd_line,
326 stdin = subprocess.PIPE,
327 stdout = subprocess.PIPE,
328 stderr = subprocess.PIPE,
329
330 encoding = 'utf8',
331 errors = 'backslashreplace'
332 )
333 except (subprocess.TimeoutExpired, FileNotFoundError):
334 _log.exception('there was a problem running external process')
335 return (False, _('problem with <%s>') % binary)
336
337 _log.info('exit code [%s]', proc_result.returncode)
338 if proc_result.returncode != 0:
339 _log.error('[%s] failed', binary)
340 _log.error('STDERR:\n%s', proc_result.stderr)
341 _log.error('STDOUT:\n%s', proc_result.stdout)
342 return (False, _('problem with <%s>') % binary)
343 return (True, proc_result.stdout)
344
345
347 if callback is None:
348 return __run_file_describer(filename)
349
350 payload_kwargs = {'filename': filename}
351 gmWorkerThread.execute_in_worker_thread (
352 payload_function = __run_file_describer,
353 payload_kwargs = payload_kwargs,
354 completion_callback = callback
355 )
356
357
359 """Try to find an appropriate viewer with all tricks and call it.
360
361 block: try to detach from viewer or not, None means to use mailcap default
362 """
363 if not os.path.isdir(aFile):
364
365 try:
366 open(aFile).close()
367 except Exception:
368 _log.exception('cannot read [%s]', aFile)
369 msg = _('[%s] is not a readable file') % aFile
370 return False, msg
371
372
373 found, startfile_cmd = _get_system_startfile_cmd(aFile)
374 if found:
375 if gmShellAPI.run_command_in_shell(command = startfile_cmd, blocking = block):
376 return True, ''
377
378 mime_type = guess_mimetype(aFile)
379 viewer_cmd = get_viewer_cmd(mime_type, aFile)
380
381 if viewer_cmd is not None:
382 if gmShellAPI.run_command_in_shell(command = viewer_cmd, blocking = block):
383 return True, ''
384
385 _log.warning("no viewer found via standard mailcap system")
386 if os.name == "posix":
387 _log.warning("you should add a viewer for this mime type to your mailcap file")
388
389 _log.info("let's see what the OS can do about that")
390
391
392 (path_name, f_ext) = os.path.splitext(aFile)
393
394 if f_ext in ['', '.tmp']:
395
396 f_ext = guess_ext_by_mimetype(mime_type)
397 if f_ext is None:
398 _log.warning("no suitable file extension found, trying anyway")
399 file_to_display = aFile
400 f_ext = '?unknown?'
401 else:
402 file_to_display = aFile + f_ext
403 shutil.copyfile(aFile, file_to_display)
404
405 else:
406 file_to_display = aFile
407
408 file_to_display = os.path.normpath(file_to_display)
409 _log.debug("file %s <type %s> (ext %s) -> file %s" % (aFile, mime_type, f_ext, file_to_display))
410
411 try:
412 os.startfile(file_to_display)
413 return True, ''
414 except AttributeError:
415 _log.exception('os.startfile() does not exist on this platform')
416 except Exception:
417 _log.exception('os.startfile(%s) failed', file_to_display)
418
419 msg = _("Unable to display the file:\n\n"
420 " [%s]\n\n"
421 "Your system does not seem to have a (working)\n"
422 "viewer registered for the file type\n"
423 " [%s]"
424 ) % (file_to_display, mime_type)
425 return False, msg
426
427
429 """Try to find an appropriate editor with all tricks and call it.
430
431 block: try to detach from editor or not, None means to use mailcap default.
432 """
433 if not os.path.isdir(filename):
434
435 try:
436 open(filename).close()
437 except Exception:
438 _log.exception('cannot read [%s]', filename)
439 msg = _('[%s] is not a readable file') % filename
440 return False, msg
441
442 mime_type = guess_mimetype(filename)
443
444 editor_cmd = get_editor_cmd(mime_type, filename)
445 if editor_cmd is not None:
446 if gmShellAPI.run_command_in_shell(command = editor_cmd, blocking = block):
447 return True, ''
448 viewer_cmd = get_viewer_cmd(mime_type, filename)
449 if viewer_cmd is not None:
450 if gmShellAPI.run_command_in_shell(command = viewer_cmd, blocking = block):
451 return True, ''
452 _log.warning("no editor or viewer found via standard mailcap system")
453
454 if os.name == "posix":
455 _log.warning("you should add an editor and/or viewer for this mime type to your mailcap file")
456
457 _log.info("let's see what the OS can do about that")
458
459 (path_name, f_ext) = os.path.splitext(filename)
460 if f_ext in ['', '.tmp']:
461
462 f_ext = guess_ext_by_mimetype(mime_type)
463 if f_ext is None:
464 _log.warning("no suitable file extension found, trying anyway")
465 file_to_display = filename
466 f_ext = '?unknown?'
467 else:
468 file_to_display = filename + f_ext
469 shutil.copyfile(filename, file_to_display)
470 else:
471 file_to_display = filename
472
473 file_to_display = os.path.normpath(file_to_display)
474 _log.debug("file %s <type %s> (ext %s) -> file %s" % (filename, mime_type, f_ext, file_to_display))
475
476
477 found, startfile_cmd = _get_system_startfile_cmd(filename)
478 if found:
479 if gmShellAPI.run_command_in_shell(command = startfile_cmd, blocking = block):
480 return True, ''
481
482
483 try:
484 os.startfile(file_to_display)
485 return True, ''
486 except AttributeError:
487 _log.exception('os.startfile() does not exist on this platform')
488 except Exception:
489 _log.exception('os.startfile(%s) failed', file_to_display)
490
491 msg = _("Unable to edit/view the file:\n\n"
492 " [%s]\n\n"
493 "Your system does not seem to have a (working)\n"
494 "editor or viewer registered for the file type\n"
495 " [%s]"
496 ) % (file_to_display, mime_type)
497 return False, msg
498
499
500 if __name__ == "__main__":
501
502 if len(sys.argv) < 2:
503 sys.exit()
504
505 if sys.argv[1] != 'test':
506 sys.exit()
507
508 from Gnumed.pycommon import gmI18N
509
510
511 logging.basicConfig(level = logging.DEBUG)
512
513 filename = sys.argv[2]
514 _get_system_startfile_cmd(filename)
515
516
518
519 mimetypes = [
520 'application/x-latex',
521 'application/x-tex',
522 'text/latex',
523 'text/tex',
524 'text/plain'
525 ]
526
527 for mimetype in mimetypes:
528 editor_cmd = get_editor_cmd(mimetype, filename)
529 if editor_cmd is not None:
530 break
531
532 if editor_cmd is None:
533
534
535 for mimetype in mimetypes:
536 editor_cmd = get_viewer_cmd(mimetype, filename)
537 if editor_cmd is not None:
538 break
539
540 if editor_cmd is None:
541 return False
542
543 result = gmShellAPI.run_command_in_shell(command = editor_cmd, blocking = True)
544
545 return result
546
547
552
553
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575 test_convert_file()
576