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.docs') 
 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 _log.exception('module <extractor> (python wrapper for libextractor) not installed') 57 else: 58 raise 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 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 #-----------------------------------------------------------------------------------
303 -def __run_file_describer(filename=None):
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] #, base_name + '.bat' 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 #timeout = timeout, 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 #-----------------------------------------------------------------------------------
338 -def describe_file(filename, callback=None):
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 #-----------------------------------------------------------------------------------
350 -def call_viewer_on_file(aFile = None, block=None):
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 # is the file accessible at all ? 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 # try to detect any of the UNIX openers 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 # does the file already have an extension ? 384 (path_name, f_ext) = os.path.splitext(aFile) 385 # no 386 if f_ext in ['', '.tmp']: 387 # try to guess one 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 # yes 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 #-----------------------------------------------------------------------------------
420 -def call_editor_on_file(filename=None, block=True):
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 # is the file accessible at all ? 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 # does the file already have a useful extension ? 451 (path_name, f_ext) = os.path.splitext(filename) 452 if f_ext in ['', '.tmp']: 453 # try to guess one 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 # try to detect any of the UNIX openers (will only find viewers) 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 # last resort: hand over to Python itself 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 # for testing: 503 logging.basicConfig(level = logging.DEBUG) 504 505 filename = sys.argv[2] 506 _get_system_startfile_cmd(filename) 507 508 #--------------------------------------------------------
509 - def test_edit():
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 # LaTeX code is text: also consider text *viewers* 526 # since pretty much any of them will be an editor as well 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 #--------------------------------------------------------
540 - def test_describer():
541 status, desc = describe_file(filename) 542 print(status) 543 print(desc)
544 545 #--------------------------------------------------------
546 - def test_convert_file():
547 print(convert_file ( 548 filename = filename, 549 target_mime = sys.argv[3] 550 #,target_filename = filename + , 551 #target_extension=None 552 ))
553 554 #-------------------------------------------------------- 555 # print(_system_startfile_cmd) 556 # print(guess_mimetype(filename)) 557 # print(get_viewer_cmd(guess_mimetype(filename), filename)) 558 # print(get_editor_cmd(guess_mimetype(filename), filename)) 559 # print(get_editor_cmd('application/x-latex', filename)) 560 # print(get_editor_cmd('application/x-tex', filename)) 561 # print(get_editor_cmd('text/latex', filename)) 562 # print(get_editor_cmd('text/tex', filename)) 563 # print(get_editor_cmd('text/plain', filename)) 564 #print(guess_ext_by_mimetype(mimetype=filename)) 565 # call_viewer_on_file(aFile = filename, block = True) 566 #call_editor_on_file(filename) 567 #test_describer() 568 #print(test_edit()) 569 test_convert_file() 570