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