Gnumed.pycommon.gmMimeLib

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 = list(open_cmds)
    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