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