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