Package Gnumed :: Package pycommon :: Module gmMimeLib
[frames] | no frames]

Source Code for Module Gnumed.pycommon.gmMimeLib

  1  # -*- coding: utf-8 -*- 
  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  # stdlib 
 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  # GNUmed 
 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  #======================================================================================= 
34 -def guess_mimetype(filename=None):
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 # 1) use Python libextractor 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 # winerror 126, errno 22 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 # 2) use "file" system command 62 # -i get mime type 63 # -b don't display a header 64 mime_guesser_cmd = 'file -i -b "%s"' % filename 65 # this only works on POSIX with 'file' installed (which is standard, however) 66 # it might work on Cygwin installations 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 # 3) use "extract" shell level libextractor wrapper 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 # If we and up here we either have an insufficient systemwide 96 # magic number file or we suffer from a deficient operating system 97 # alltogether. It can't get much worse if we try ourselves. 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 # last resort: if no file name given replace %s in original with literal '%s' 115 # and hope for the best - we certainly don't want the module default "/dev/null" 116 aFileName = """%s""" 117 118 mailcaps = mailcap.getcaps() 119 (viewer, junk) = mailcap.findmatch(mailcaps, aMimeType, key = 'view', filename = '%s' % aFileName) 120 # FIXME: we should check for "x-token" flags 121 122 _log.debug("<%s> viewer: [%s]" % (aMimeType, viewer)) 123 124 return viewer
125 126 #-----------------------------------------------------------------------------------
127 -def get_editor_cmd(mimetype=None, filename=None):
128 129 if filename is None: 130 _log.error("You should specify a file name for the replacement of %s.") 131 # last resort: if no file name given replace %s in original with literal '%s' 132 # and hope for the best - we certainly don't want the module default "/dev/null" 133 filename = """%s""" 134 135 mailcaps = mailcap.getcaps() 136 (editor, junk) = mailcap.findmatch(mailcaps, mimetype, key = 'edit', filename = '%s' % filename) 137 138 # FIXME: we should check for "x-token" flags 139 140 _log.debug("<%s> editor: [%s]" % (mimetype, editor)) 141 142 return editor
143 144 #-----------------------------------------------------------------------------------
145 -def guess_ext_by_mimetype(mimetype=''):
146 """Return file extension based on what the OS thinks a file of this mimetype should end in.""" 147 148 # ask system first 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 # try to help the OS a bit 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 #-----------------------------------------------------------------------------------
173 -def guess_ext_for_file(aFile=None):
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 # try to guess one 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 #-----------------------------------------------------------------------------------
191 -def adjust_extension_by_mimetype(filename):
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"', # nascent standard on Linux 215 'kfmclient': 'kfmclient exec "%s"', # KDE 216 'gnome-open': 'gnome-open "%s"', # GNOME 217 'exo-open': 'exo-open "%s"', 218 'op': 'op "%s"', 219 'open': 'open "%s"', # MacOSX: "open -a AppName file" (-a allows to override the default app for the file type) 220 'cmd.exe': 'cmd.exe /c "%s"' # Windows 221 #'run-mailcap' 222 #'explorer' 223 } 224
225 -def _get_system_startfile_cmd(filename):
226 227 global _system_startfile_cmd 228 229 if _system_startfile_cmd == '': 230 return False, None 231 232 if _system_startfile_cmd is not None: 233 return True, _system_startfile_cmd % filename 234 235 open_cmd_candidates = open_cmds.keys() 236 237 for candidate in open_cmd_candidates: 238 found, binary = gmShellAPI.detect_external_binary(binary = candidate) 239 if not found: 240 continue 241 _system_startfile_cmd = open_cmds[candidate] 242 _log.info('detected local startfile cmd: [%s]', _system_startfile_cmd) 243 return True, _system_startfile_cmd % filename 244 245 _system_startfile_cmd = '' 246 return False, None
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 # can still stay None 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 ] #, script_name + u'.bat' 279 found, binary = gmShellAPI.find_first_binary(binaries = candidates) 280 if not found: 281 # try anyway 282 binary = script_name# + r'.bat' 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 # we may actually have something despite a non-0 exit code 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 #-----------------------------------------------------------------------------------
315 -def __run_file_describer(filename=None):
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] #, base_name + '.bat' 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 #timeout = timeout, 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 #-----------------------------------------------------------------------------------
350 -def describe_file(filename, callback=None):
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 #-----------------------------------------------------------------------------------
362 -def call_viewer_on_file(aFile = None, block=None):
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 # is the file accessible at all ? 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 # try to detect any of the UNIX openers 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 # does the file already have an extension ? 396 (path_name, f_ext) = os.path.splitext(aFile) 397 # no 398 if f_ext in ['', '.tmp']: 399 # try to guess one 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 # yes 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 #-----------------------------------------------------------------------------------
432 -def call_editor_on_file(filename=None, block=True):
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 # is the file accessible at all ? 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 # does the file already have a useful extension ? 463 (path_name, f_ext) = os.path.splitext(filename) 464 if f_ext in ['', '.tmp']: 465 # try to guess one 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 # try to detect any of the UNIX openers (will only find viewers) 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 # last resort: hand over to Python itself 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 # for testing: 515 logging.basicConfig(level = logging.DEBUG) 516 517 filename = sys.argv[2] 518 _get_system_startfile_cmd(filename) 519 520 #--------------------------------------------------------
521 - def test_edit():
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 # LaTeX code is text: also consider text *viewers* 538 # since pretty much any of them will be an editor as well 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 #--------------------------------------------------------
552 - def test_describer():
553 status, desc = describe_file(filename) 554 print(status) 555 print(desc)
556 557 #--------------------------------------------------------
558 - def test_convert_file():
559 print(convert_file ( 560 filename = filename, 561 target_mime = sys.argv[3] 562 ))
563 564 #-------------------------------------------------------- 565 # print(_system_startfile_cmd) 566 # print(guess_mimetype(filename)) 567 # print(get_viewer_cmd(guess_mimetype(filename), filename)) 568 # print(get_editor_cmd(guess_mimetype(filename), filename)) 569 # print(get_editor_cmd('application/x-latex', filename)) 570 # print(get_editor_cmd('application/x-tex', filename)) 571 # print(get_editor_cmd('text/latex', filename)) 572 # print(get_editor_cmd('text/tex', filename)) 573 # print(get_editor_cmd('text/plain', filename)) 574 #print(guess_ext_by_mimetype(mimetype=filename)) 575 # call_viewer_on_file(aFile = filename, block = True) 576 #call_editor_on_file(filename) 577 #test_describer() 578 #print(test_edit()) 579 test_convert_file() 580