Gnumed.pycommon.gmNetworkTools

Package Gnumed :: Package pycommon :: Module gmNetworkTools |
[frames] | no frames]

# Source Code for Module Gnumed.pycommon.gmNetworkTools

      1  # -*- coding: utf-8 -*-
      2
      3  __doc__ = """GNUmed internetworking tools."""
      4
      5  #===========================================================================
      6  __author__ = "K. Hilbert <Karsten.Hilbert@gmx.net>"
      7  __license__ = "GPL v2 or later (details at http://www.gnu.org)"
      8
      9  # std libs
     10  import sys
     11  import os.path
     12  import logging
     13  import urllib.request
     14  import urllib.error
     15  import zipfile
     16  import webbrowser
     17  import io
     18
     19
     20  # GNUmed libs
     21  if __name__ == '__main__':
     22          sys.path.insert(0, '../../')
     23  from Gnumed.pycommon import gmLog2
     24  from Gnumed.pycommon import gmTools
     25  from Gnumed.pycommon import gmShellAPI
     26  from Gnumed.pycommon import gmCfg2
     27  from Gnumed.pycommon import gmMimeLib
     28
     29
     30  _log = logging.getLogger('gm.net')
     31
     32  #===========================================================================
     33  # browser access
     34  #---------------------------------------------------------------------------
     35 -def open_url_in_browser(url, new=2, autoraise=True, *args, **kwargs):
     36          # url, new=0, autoraise=True
     37          # new=2: open new tab if possible
     38          try:
     39                  webbrowser.open(url, new = new, autoraise = autoraise, **kwargs)
     40          except (webbrowser.Error, OSError, UnicodeEncodeError):
     41                  _log.exception('error calling browser with url=%s', url)
     42                  return False
     43          return True
     44  #===========================================================================
     45 -def download_file(url, filename=None, suffix=None):
     46
     47          if filename is None:
     48                  filename = gmTools.get_unique_filename(prefix = 'gm-dl-', suffix = suffix)
     49          _log.debug('downloading [%s] into [%s]', url, filename)
     50
     51          try:
     52                  dl_name, headers = urllib.request.urlretrieve(url, filename)
     53          except (ValueError, OSError, IOError):
     54                  _log.exception('cannot download from [%s]', url)
     55                  gmLog2.log_stack_trace()
     56                  return None
     57
     58          _log.debug('%s' % headers)
     59          return dl_name
     60  #===========================================================================
     61  # data pack handling
     62  #---------------------------------------------------------------------------
     63 -def download_data_packs_list(url, filename=None):
     64          return download_file(url, filename = filename, suffix = 'conf')
     65  #---------------------------------------------------------------------------
     66 -def download_data_pack(pack_url, filename=None, md5_url=None):
     67
     68          _log.debug('downloading data pack from: %s', pack_url)
     69          dp_fname = download_file(pack_url, filename = filename, suffix = 'zip')
     70          _log.debug('downloading MD5 from: %s', md5_url)
     71          md5_fname = download_file(md5_url, filename = dp_fname + '.md5')
     72
     73          md5_file = io.open(md5_fname, mode = 'rt', encoding = 'utf8')
     74          md5_expected = md5_file.readline().strip('\n')
     75          md5_file.close()
     76          _log.debug('expected MD5: %s', md5_expected)
     77          md5_calculated = gmTools.file2md5(dp_fname, return_hex = True)
     78          _log.debug('calculated MD5: %s', md5_calculated)
     79
     80          if md5_calculated != md5_expected:
     81                  _log.error('mismatch of expected vs calculated MD5: [%s] vs [%s]', md5_expected, md5_calculated)
     82                  return (False, (md5_expected, md5_calculated))
     83
     84          return True, dp_fname
     85  #---------------------------------------------------------------------------
     86 -def unzip_data_pack(filename=None):
     87
     88          unzip_dir = os.path.splitext(filename)[0]
     89          _log.debug('unzipping data pack into [%s]', unzip_dir)
     90          gmTools.mkdir(unzip_dir)
     91          try:
     92                  data_pack = zipfile.ZipFile(filename, 'r')
     93          except (zipfile.BadZipfile):
     94                  _log.exception('cannot unzip data pack [%s]', filename)
     95                  gmLog2.log_stack_trace()
     96                  return None
     97
     98          data_pack.extractall(unzip_dir)
     99
    100          return unzip_dir
    101
    102  #---------------------------------------------------------------------------
    103 -def install_data_pack(data_pack=None, conn=None):
    104          from Gnumed.pycommon import gmPsql
    105          psql = gmPsql.Psql(conn)
    106          sql_script = os.path.join(data_pack['unzip_dir'], 'install-data-pack.sql')
    107          if psql.run(sql_script) == 0:
    108                  curs = conn.cursor()
    109                  curs.execute('select gm.log_script_insertion(%(name)s, %(ver)s)', {'name': data_pack['pack_url'], 'ver': 'current'})
    110                  curs.close()
    111                  conn.commit()
    112                  return True
    113
    114          _log.error('error installing data pack: %s', data_pack)
    115          return False
    116  #---------------------------------------------------------------------------
    117 -def download_data_pack_old(url, target_dir=None):
    118
    119          if target_dir is None:
    120                  target_dir = gmTools.get_unique_filename(prefix = 'gm-dl-')
    121
    122          _log.debug('downloading [%s]', url)
    123          _log.debug('unpacking into [%s]', target_dir)
    124
    125          gmTools.mkdir(directory = target_dir)
    126
    127          # FIXME: rewrite to use urllib.request.urlretrieve() and
    128
    129          paths = gmTools.gmPaths()
    130          local_script = os.path.join(paths.local_base_dir, '..', 'external-tools', 'gm-download_data')
    131
    132          candidates = ['gm-download_data', 'gm-download_data.bat', local_script, 'gm-download_data.bat']
    133          args = ' %s %s' % (url, target_dir)
    134
    135          success = gmShellAPI.run_first_available_in_shell (
    136                  binaries = candidates,
    137                  args = args,
    138                  blocking = True,
    139                  run_last_one_anyway = True
    140          )
    141
    142          if success:
    143                  return True, target_dir
    144
    145          _log.error('download failed')
    146          return False, None
    147  #===========================================================================
    148  # client update handling
    149  #---------------------------------------------------------------------------
    150 -def compare_versions(left_version, right_version):
    151          """
    152           0: left == right
    153          -1: left < right
    154           1: left > right
    155          """
    156          _log.debug('comparing [%s] with [%s]', left_version, right_version)
    157          if left_version == right_version:
    158                  _log.debug('same version')
    159                  return 0
    160
    161          if right_version in ['head', 'dev', 'devel']:
    162                  _log.debug('development code')
    163                  return -1
    164
    165          if left_version in ['head', 'dev', 'devel']:
    166                  _log.debug('development code')
    167                  return 1
    168
    169          left_parts = left_version.split('.')
    170          right_parts = right_version.split('.')
    171
    172          tmp, left_major = gmTools.input2decimal('%s.%s' % (left_parts[0], left_parts[1]))
    173          tmp, right_major = gmTools.input2decimal('%s.%s' % (right_parts[0], right_parts[1]))
    174
    175          if left_major < right_major:
    176                  _log.debug('left version [%s] < right version [%s]: major part', left_version, right_version)
    177                  return -1
    178
    179          if left_major > right_major:
    180                  _log.debug('left version [%s] > right version [%s]: major part', left_version, right_version)
    181                  return 1
    182
    183          tmp, left_part3 = gmTools.input2decimal(left_parts[2].replace('rc', '0.'))
    184          tmp, right_part3 = gmTools.input2decimal(right_parts[2].replace('rc', '0.'))
    185
    186          if left_part3 < right_part3:
    187                  _log.debug('left version [%s] < right version [%s]: minor part', left_version, right_version)
    188                  return -1
    189
    190          if left_part3 > right_part3:
    191                  _log.debug('left version [%s] > right version [%s]: minor part', left_version, right_version)
    192                  return 1
    193
    194          return 0
    195  #---------------------------------------------------------------------------
    196 -def check_for_update(url=None, current_branch=None, current_version=None, consider_latest_branch=False):
    197          """Check for new releases at <url>.
    198
    199          Returns (bool, text).
    200          True: new release available
    201          False: up to date
    202          None: don't know
    203          """
    204          if current_version is None:
    205                  _log.debug('<current_version> is None, currency unknown')
    206                  return (None, None)
    207
    208          if current_version.lower() in ['git head', 'head', 'tip', 'dev', 'devel']:
    209                  _log.debug('[%s] always considered up to date', current_version)
    210                  return (False, None)
    211
    212          try:
    213                  remote_file = urllib.request.urlopen(url)
    214          except (urllib.error.URLError, ValueError, OSError, IOError):
    215                  # IOError: socket.error
    216                  _log.exception("cannot retrieve version file from [%s]", url)
    217                  return (None, _('Cannot retrieve version information from:\n\n%s') % url)
    218
    219          _log.debug('retrieving version information from [%s]', url)
    220
    221          cfg = gmCfg2.gmCfgData()
    222          try:
    223                  #remote_file.read().decode(resource.headers.get_content_charset())
    224                  cfg.add_stream_source(source = 'gm-versions', stream = remote_file, encoding = u'utf8')
    225          except (UnicodeDecodeError):
    226                  remote_file.close()
    227                  _log.exception("cannot read version file from [%s]", url)
    228                  return (None, _('Cannot read version information from:\n\n%s') % url)
    229
    230          remote_file.close()
    231
    232          latest_branch = cfg.get('latest branch', 'branch', source_order = [('gm-versions', 'return')])
    233          latest_release_on_latest_branch = cfg.get('branch %s' % latest_branch, 'latest release', source_order = [('gm-versions', 'return')])
    234          latest_release_on_current_branch = cfg.get('branch %s' % current_branch, 'latest release', source_order = [('gm-versions', 'return')])
    235
    236          cfg.remove_source('gm-versions')
    237
    238          _log.info('current release: %s', current_version)
    239          _log.info('current branch: %s', current_branch)
    240          _log.info('latest release on current branch: %s', latest_release_on_current_branch)
    241          _log.info('latest branch: %s', latest_branch)
    242          _log.info('latest release on latest branch: %s', latest_release_on_latest_branch)
    243
    244          # anything known ?
    245          no_release_information_available = (
    246                  (
    247                          (latest_release_on_current_branch is None) and
    248                          (latest_release_on_latest_branch is None)
    249                  ) or (
    250                          not consider_latest_branch and
    251                          (latest_release_on_current_branch is None)
    252                  )
    253          )
    254          if no_release_information_available:
    255                  _log.warning('no release information available')
    256                  msg = _('There is no version information available from:\n\n%s') % url
    257                  return (None, msg)
    258
    259          # up to date ?
    260          if consider_latest_branch:
    261                  _log.debug('latest branch taken into account')
    262                  if latest_release_on_latest_branch is None:
    263                          if compare_versions(latest_release_on_current_branch, current_version) in [-1, 0]:
    264                                  _log.debug('up to date: current version >= latest version on current branch and no latest branch available')
    265                                  return (False, None)
    266                  else:
    267                          if compare_versions(latest_release_on_latest_branch, current_version) in [-1, 0]:
    268                                  _log.debug('up to date: current version >= latest version on latest branch')
    269                                  return (False, None)
    270          else:
    271                  _log.debug('latest branch not taken into account')
    272                  if compare_versions(latest_release_on_current_branch, current_version) in [-1, 0]:
    273                          _log.debug('up to date: current version >= latest version on current branch')
    274                          return (False, None)
    275
    276          new_release_on_current_branch_available = (
    277                  (latest_release_on_current_branch is not None) and
    278                  (compare_versions(latest_release_on_current_branch, current_version) == 1)
    279          )
    280          _log.info('%snew release on current branch available', gmTools.bool2str(new_release_on_current_branch_available, '', 'no '))
    281
    282          new_release_on_latest_branch_available = (
    283                  (latest_branch is not None)
    284                          and
    285                  (
    286                          (latest_branch > current_branch) or (
    287                                  (latest_branch == current_branch) and
    288                                  (compare_versions(latest_release_on_latest_branch, current_version) == 1)
    289                          )
    290                  )
    291          )
    292          _log.info('%snew release on latest branch available', gmTools.bool2str(new_release_on_latest_branch_available, '', 'no '))
    293
    294          if not (new_release_on_current_branch_available or new_release_on_latest_branch_available):
    295                  _log.debug('up to date: no new releases available')
    296                  return (False, None)
    297
    298          # not up to date
    299          msg = _('A new version of GNUmed is available.\n\n')
    300          msg += _(' Your current version: "%s"\n') % current_version
    301          if consider_latest_branch:
    302                  if new_release_on_current_branch_available:
    303                          msg += '\n'
    304                          msg += _(' New version: "%s"') % latest_release_on_current_branch
    305                          msg += '\n'
    306                          msg += _(' - bug fixes only\n')
    307                          msg += _(' - database fixups may be needed\n')
    308                  if new_release_on_latest_branch_available:
    309                          if current_branch != latest_branch:
    310                                  msg += '\n'
    311                                  msg += _(' New version: "%s"') % latest_release_on_latest_branch
    312                                  msg += '\n'
    313                                  msg += _(' - bug fixes and new features\n')
    314                                  msg += _(' - database upgrade required\n')
    315          else:
    316                  msg += '\n'
    317                  msg += _(' New version: "%s"') % latest_release_on_current_branch
    318                  msg += '\n'
    319                  msg += _(' - bug fixes only\n')
    320                  msg += _(' - database fixups may be needed\n')
    321
    322          msg += '\n\n'
    323          msg += _(
    324                  'Note, however, that this version may not yet\n'
    325                  'be available *pre-packaged* for your system.'
    326          )
    327
    328          msg += '\n\n'
    329          msg += _('Details are found on <http://wiki.gnumed.de>.\n')
    330          msg += '\n'
    331          msg += _('Version information loaded from:\n\n %s') % url
    332
    333          return (True, msg)
    334
    335  #===========================================================================
    336  # mail handling
    337  #---------------------------------------------------------------------------
    338  default_mail_sender = 'gnumed@gmx.net'
    339  default_mail_receiver = 'gnumed-devel@gnu.org'
    340  default_mail_server = 'mail.gmx.net'
    341
    342
    343  #---------------------------------------------------------------------------
    344  from email.mime.text import MIMEText
    345  from email.mime.multipart import MIMEMultipart
    346  from email.mime.image import MIMEImage
    347  from email.mime.audio import MIMEAudio
    348  from email.mime.application import MIMEApplication
    349
    350 -def compose_email(sender=None, receiver=None, message=None, subject=None, files2attach=None):
    351          # filenames unicode
    352          # file content binary or utf8
    353          # files2attach = [(filename, mimetype-or-None), ...]
    354
    355          if message is None:
    356                  raise ValueError('<message> is None, cannot compose email')
    357
    358          message = message.lstrip().lstrip('\r\n').lstrip()
    359
    360          if sender is None:
    361                  sender = default_mail_sender
    362
    363          if receiver is None:
    364                  receiver = [default_mail_receiver]
    365
    366          if subject is None:
    367                  subject = 'compose_email() test'
    368
    369          if files2attach is None:
    370                  email = MIMEText(message, 'plain', 'utf8')
    371          else:
    372                  email = MIMEMultipart()
    373                  email.attach(MIMEText(message, 'plain', 'utf8'))
    374
    375          email['From'] = sender
    376          email['To'] = ', '.join(receiver)
    377          email['Subject'] = subject
    378
    379          if files2attach is None:
    380                  return email
    381
    382          for file2attach in files2attach:
    383                  filename = file2attach[0]
    384                  try:
    385                          mimetype = file2attach[1]
    386                  except IndexError:
    387                          mimetype = gmMimeLib.guess_mimetype(filename = filename)
    388                  # text/*
    389                  if mimetype.startswith('text/'):
    390                          txt = io.open(filename, mode = 'rt', encoding = 'utf8')
    391                          attachment = MIMEText(txt.read(), 'plain', 'utf8')
    392                          txt.close()
    393                  # image/*
    394                  elif mimetype.startswith('image/'):
    395                          img = io.open(filename, mode = 'rb')
    396                          attachment = MIMEImage(img.read())
    397                          img.close()
    398                  # audio/*
    399                  elif mimetype.startswith('audio/'):
    400                          song = io.open(filename, mode = 'rb')
    401                          attachment = MIMEAudio(song.read())
    402                          song.close()
    403                  # catch-all application/*
    404                  else:
    405                          _log.debug('attaching [%s] with type [%s]', filename, mimetype)
    406                          mime_subtype = mimetype.split('/', 1)[1]
    407                          data = io.open(filename, mode = 'rb')
    408                          attachment = MIMEApplication(data.read(), mime_subtype)
    409                          data.close()
    410
    411                  try:
    412                          attachment.replace_header('Content-Disposition', 'attachment; filename="%s"' % gmTools.fname_from_path(filename))
    413                  except KeyError:
    414                          attachment.add_header('Content-Disposition', 'attachment; filename="%s"' % gmTools.fname_from_path(filename))
    415                  email.attach(attachment)
    416
    417          return email
    418
    419  #---------------------------------------------------------------------------
    420 -def send_email(sender=None, receiver=None, email=None, server=None, auth=None, debug=False):
    421
    422          if email is None:
    423                  raise ValueError('<email> is None, cannot send')
    424
    425          if sender is None:
    426                  sender = default_mail_sender
    427
    428          if receiver is None:
    429                  receiver = [default_mail_receiver]
    430
    431          if server is None:
    432                  server = default_mail_server
    433
    434          import smtplib
    435          failed = False
    436          refused = []
    437          try:
    438                  session = smtplib.SMTP(server)
    439                  session.set_debuglevel(debug)
    440                  try:
    441                          session.starttls()
    442                  except smtplib.SMTPException:
    443                          _log.error('cannot enable TLS on [%s]', server)
    444                  session.ehlo()
    445                  if auth is not None:
    446                          session.login(auth['user'], auth['password'])
    447                  refused = session.sendmail(sender, receiver, email.as_string())
    448                  session.quit()
    449          except smtplib.SMTPException:
    450                  failed = True
    451                  _log.exception('cannot send email')
    452                  gmLog2.log_stack_trace()
    453
    454          if len(refused) > 0:
    455                  _log.error("refused recipients: %s" % refused)
    456
    457          if failed:
    458                  return False
    459
    460          return True
    461
    462  #---------------------------------------------------------------------------
    463 -def compose_and_send_email(sender=None, receiver=None, message=None, server=None, auth=None, debug=False, subject=None, attachments=None):
    464          email = compose_email (
    465                  sender = sender,
    466                  receiver = receiver,
    467                  message = message,
    468                  subject = subject,
    469                  files2attach = attachments
    470          )
    471          return send_email (
    472                  sender = sender,
    473                  receiver = receiver,
    474                  email = email,
    475                  server = server,
    476                  auth = auth,
    477                  debug = debug
    478          )
    479
    480  #===========================================================================
    481  # main
    482  #---------------------------------------------------------------------------
    483  if __name__ == '__main__':
    484
    485          if len(sys.argv) < 2:
    486                  sys.exit()
    487
    488          if sys.argv[1] != 'test':
    489                  sys.exit()
    490
    491          #-----------------------------------------------------------------------
    492 -        def test_compose_email():
    493                  email = compose_email (
    494                          message = 'compose_email() test: üü ßß',
    495                          files2attach = [[sys.argv[2]]]
    496                  )
    497                  print(email.as_string())
    498                  return email
    499
    500          #-----------------------------------------------------------------------
    501 -        def test_send_email():
    502                  email = compose_email (
    503                          message = 'compose_email() test: üü ßß',
    504                          files2attach = [[sys.argv[2]]]
    505                  )
    506                  print(send_email (
    507  #                       receiver = u'ncq@localhost',
    508                          email = email,
    509  #                       server = 'localhost',
    510                          auth = {'user': default_mail_sender, 'password': 'gnumed-at-gmx-net'},
    511                          debug = True
    512                  ))
    513
    514          #-----------------------------------------------------------------------
    515 -        def test_check_for_update():
    516
    517                  test_data = [
    518                          ('http://www.gnumed.de/downloads/gnumed-versions.txt', None, None, False),
    519                          ('file:///home/ncq/gm-versions.txt', None, None, False),
    520                          ('file:///home/ncq/gm-versions.txt', '0.2', '0.2.8.1', False),
    521                          ('file:///home/ncq/gm-versions.txt', '0.2', '0.2.8.1', True),
    522                          ('file:///home/ncq/gm-versions.txt', '0.2', '0.2.8.5', True)
    523                  ]
    524
    525                  for test in test_data:
    526                          print("arguments:", test)
    527                          found, msg = check_for_update(test[0], test[1], test[2], test[3])
    528                          print(msg)
    529
    530                  return
    531          #-----------------------------------------------------------------------
    532 -        def test_dl_data_pack():
    533                  #url = 'file:./x-data_pack.zip'
    534                  #url = 'missing-file.zip'
    535                  url = 'gmTools.py'
    536                  dl_name = download_data_pack(url)
    537                  print(url, "->", dl_name)
    538                  unzip_dir = unzip_data_pack(dl_name)
    539                  print("unzipped into", unzip_dir)
    540
    541          #-----------------------------------------------------------------------
    542 -        def test_browser():
    543                  success = open_url_in_browser(sys.argv[2])
    544                  print(success)
    545                  open_url_in_browser(sys.argv[2], abc=222)
    546
    547          #-----------------------------------------------------------------------
    548          #test_check_for_update()
    549          #test_compose_email()
    550          test_send_email()
    551          #test_dl_data_pack()
    552          #test_browser()
    553
    554  #===========================================================================
    555