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):
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 ] #, base_name + u'.bat' 269 found, binary = gmShellAPI.find_first_binary(binaries = candidates) 270 if not found: 271 binary = base_name# + r'.bat' 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 #-----------------------------------------------------------------------------------
294 -def __run_file_describer(filename=None):
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 ] #, base_name + u'.bat' 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 #timeout = timeout, 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 #-----------------------------------------------------------------------------------
328 -def describe_file(filename, callback=None):
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 #-----------------------------------------------------------------------------------
340 -def call_viewer_on_file(aFile = None, block=None):
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 # is the file accessible at all ? 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 # try to detect any of the UNIX openers 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 # does the file already have an extension ? 374 (path_name, f_ext) = os.path.splitext(aFile) 375 # no 376 if f_ext in ['', '.tmp']: 377 # try to guess one 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 # yes 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 #-----------------------------------------------------------------------------------
410 -def call_editor_on_file(filename=None, block=True):
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 # is the file accessible at all ? 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 # does the file already have a useful extension ? 441 (path_name, f_ext) = os.path.splitext(filename) 442 if f_ext in ['', '.tmp']: 443 # try to guess one 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 # try to detect any of the UNIX openers (will only find viewers) 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 # last resort: hand over to Python itself 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 # for testing: 493 logging.basicConfig(level = logging.DEBUG) 494 495 filename = sys.argv[2] 496 _get_system_startfile_cmd(filename) 497 498 #--------------------------------------------------------
499 - def test_edit():
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 # LaTeX code is text: also consider text *viewers* 516 # since pretty much any of them will be an editor as well 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 #--------------------------------------------------------
530 - def test_describer():
531 status, desc = describe_file(filename) 532 print(status) 533 print(desc)
534 535 #-------------------------------------------------------- 536 537 # print(_system_startfile_cmd) 538 # print(guess_mimetype(filename)) 539 # print(get_viewer_cmd(guess_mimetype(filename), filename)) 540 # print(get_editor_cmd(guess_mimetype(filename), filename)) 541 # print(get_editor_cmd('application/x-latex', filename)) 542 # print(get_editor_cmd('application/x-tex', filename)) 543 # print(get_editor_cmd('text/latex', filename)) 544 # print(get_editor_cmd('text/tex', filename)) 545 # print(get_editor_cmd('text/plain', filename)) 546 # print(guess_ext_by_mimetype(mimetype=filename)) 547 # call_viewer_on_file(aFile = filename, block = True) 548 #call_editor_on_file(filename) 549 test_describer() 550 551 #print(test_edit()) 552