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.docs')
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 _log.exception('module <extractor> (python wrapper for libextractor) not installed')
57 else:
58 raise
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):
250 """Convert file from one format into another.
251
252 target_mime: a mime type
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 shutil.copyfile(filename, target_filename)
258 return True
259
260 if target_extension is None:
261 tmp, target_extension = os.path.splitext(target_filename)
262
263 base_name = 'gm-convert_file'
264
265 paths = gmTools.gmPaths()
266 local_script = os.path.join(paths.local_base_dir, '..', 'external-tools', base_name)
267
268 candidates = [ base_name, local_script ]
269 found, binary = gmShellAPI.find_first_binary(binaries = candidates)
270 if not found:
271 binary = base_name
272
273 cmd_line = [
274 binary,
275 filename,
276 target_mime,
277 target_extension.strip('.'),
278 target_filename
279 ]
280 _log.debug('converting: %s', cmd_line)
281 try:
282 gm_convert = subprocess.Popen(cmd_line)
283 except OSError:
284 _log.debug('cannot run <%s(.bat)>', base_name)
285 return False
286 gm_convert.communicate()
287 if gm_convert.returncode != 0:
288 _log.error('<%s(.bat)> returned [%s], failed to convert', base_name, gm_convert.returncode)
289 return False
290
291 return True
292
293
295 base_name = 'gm-describe_file'
296 paths = gmTools.gmPaths()
297 local_script = os.path.join(paths.local_base_dir, '..', 'external-tools', base_name)
298 candidates = [ base_name, local_script ]
299 found, binary = gmShellAPI.find_first_binary(binaries = candidates)
300 if not found:
301 _log.error('cannot find <%s(.bat)>', base_name)
302 return (False, _('<%s(.bat)> not found') % base_name)
303
304 cmd_line = [binary, filename]
305 _log.debug('describing: %s', cmd_line)
306 try:
307 proc_result = subprocess.run (
308 args = cmd_line,
309 stdin = subprocess.PIPE,
310 stdout = subprocess.PIPE,
311 stderr = subprocess.PIPE,
312
313 encoding = 'utf8'
314 )
315 except (subprocess.TimeoutExpired, FileNotFoundError):
316 _log.exception('there was a problem running external process')
317 return (False, _('problem with <%s>') % binary)
318
319 _log.info('exit code [%s]', proc_result.returncode)
320 if proc_result.returncode != 0:
321 _log.error('[%s] failed', binary)
322 _log.error('STDERR:\n%s', proc_result.stderr)
323 _log.error('STDOUT:\n%s', proc_result.stdout)
324 return (False, _('problem with <%s>') % binary)
325 return (True, proc_result.stdout)
326
327
329 if callback is None:
330 return __run_file_describer(filename)
331
332 payload_kwargs = {'filename': filename}
333 gmWorkerThread.execute_in_worker_thread (
334 payload_function = __run_file_describer,
335 payload_kwargs = payload_kwargs,
336 completion_callback = callback
337 )
338
339
341 """Try to find an appropriate viewer with all tricks and call it.
342
343 block: try to detach from viewer or not, None means to use mailcap default
344 """
345 if not os.path.isdir(aFile):
346
347 try:
348 open(aFile).close()
349 except:
350 _log.exception('cannot read [%s]', aFile)
351 msg = _('[%s] is not a readable file') % aFile
352 return False, msg
353
354
355 found, startfile_cmd = _get_system_startfile_cmd(aFile)
356 if found:
357 if gmShellAPI.run_command_in_shell(command = startfile_cmd, blocking = block):
358 return True, ''
359
360 mime_type = guess_mimetype(aFile)
361 viewer_cmd = get_viewer_cmd(mime_type, aFile)
362
363 if viewer_cmd is not None:
364 if gmShellAPI.run_command_in_shell(command = viewer_cmd, blocking = block):
365 return True, ''
366
367 _log.warning("no viewer found via standard mailcap system")
368 if os.name == "posix":
369 _log.warning("you should add a viewer for this mime type to your mailcap file")
370
371 _log.info("let's see what the OS can do about that")
372
373
374 (path_name, f_ext) = os.path.splitext(aFile)
375
376 if f_ext in ['', '.tmp']:
377
378 f_ext = guess_ext_by_mimetype(mime_type)
379 if f_ext is None:
380 _log.warning("no suitable file extension found, trying anyway")
381 file_to_display = aFile
382 f_ext = '?unknown?'
383 else:
384 file_to_display = aFile + f_ext
385 shutil.copyfile(aFile, file_to_display)
386
387 else:
388 file_to_display = aFile
389
390 file_to_display = os.path.normpath(file_to_display)
391 _log.debug("file %s <type %s> (ext %s) -> file %s" % (aFile, mime_type, f_ext, file_to_display))
392
393 try:
394 os.startfile(file_to_display)
395 return True, ''
396 except AttributeError:
397 _log.exception('os.startfile() does not exist on this platform')
398 except:
399 _log.exception('os.startfile(%s) failed', file_to_display)
400
401 msg = _("Unable to display the file:\n\n"
402 " [%s]\n\n"
403 "Your system does not seem to have a (working)\n"
404 "viewer registered for the file type\n"
405 " [%s]"
406 ) % (file_to_display, mime_type)
407 return False, msg
408
409
411 """Try to find an appropriate editor with all tricks and call it.
412
413 block: try to detach from editor or not, None means to use mailcap default.
414 """
415 if not os.path.isdir(filename):
416
417 try:
418 open(filename).close()
419 except:
420 _log.exception('cannot read [%s]', filename)
421 msg = _('[%s] is not a readable file') % filename
422 return False, msg
423
424 mime_type = guess_mimetype(filename)
425
426 editor_cmd = get_editor_cmd(mime_type, filename)
427 if editor_cmd is not None:
428 if gmShellAPI.run_command_in_shell(command = editor_cmd, blocking = block):
429 return True, ''
430 viewer_cmd = get_viewer_cmd(mime_type, filename)
431 if viewer_cmd is not None:
432 if gmShellAPI.run_command_in_shell(command = viewer_cmd, blocking = block):
433 return True, ''
434 _log.warning("no editor or viewer found via standard mailcap system")
435
436 if os.name == "posix":
437 _log.warning("you should add an editor and/or viewer for this mime type to your mailcap file")
438
439 _log.info("let's see what the OS can do about that")
440
441 (path_name, f_ext) = os.path.splitext(filename)
442 if f_ext in ['', '.tmp']:
443
444 f_ext = guess_ext_by_mimetype(mime_type)
445 if f_ext is None:
446 _log.warning("no suitable file extension found, trying anyway")
447 file_to_display = filename
448 f_ext = '?unknown?'
449 else:
450 file_to_display = filename + f_ext
451 shutil.copyfile(filename, file_to_display)
452 else:
453 file_to_display = filename
454
455 file_to_display = os.path.normpath(file_to_display)
456 _log.debug("file %s <type %s> (ext %s) -> file %s" % (filename, mime_type, f_ext, file_to_display))
457
458
459 found, startfile_cmd = _get_system_startfile_cmd(filename)
460 if found:
461 if gmShellAPI.run_command_in_shell(command = startfile_cmd, blocking = block):
462 return True, ''
463
464
465 try:
466 os.startfile(file_to_display)
467 return True, ''
468 except AttributeError:
469 _log.exception('os.startfile() does not exist on this platform')
470 except Exception:
471 _log.exception('os.startfile(%s) failed', file_to_display)
472
473 msg = _("Unable to edit/view the file:\n\n"
474 " [%s]\n\n"
475 "Your system does not seem to have a (working)\n"
476 "editor or viewer registered for the file type\n"
477 " [%s]"
478 ) % (file_to_display, mime_type)
479 return False, msg
480
481
482 if __name__ == "__main__":
483
484 if len(sys.argv) < 2:
485 sys.exit()
486
487 if sys.argv[1] != 'test':
488 sys.exit()
489
490 from Gnumed.pycommon import gmI18N
491
492
493 logging.basicConfig(level = logging.DEBUG)
494
495 filename = sys.argv[2]
496 _get_system_startfile_cmd(filename)
497
498
500
501 mimetypes = [
502 'application/x-latex',
503 'application/x-tex',
504 'text/latex',
505 'text/tex',
506 'text/plain'
507 ]
508
509 for mimetype in mimetypes:
510 editor_cmd = get_editor_cmd(mimetype, filename)
511 if editor_cmd is not None:
512 break
513
514 if editor_cmd is None:
515
516
517 for mimetype in mimetypes:
518 editor_cmd = get_viewer_cmd(mimetype, filename)
519 if editor_cmd is not None:
520 break
521
522 if editor_cmd is None:
523 return False
524
525 result = gmShellAPI.run_command_in_shell(command = editor_cmd, blocking = True)
526
527 return result
528
529
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549 test_describer()
550
551
552