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 + '.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