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

# Source Code for Module Gnumed.pycommon.gmNetworkTools

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