1
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
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
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
34
36
37
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
60
61
62
65
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
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
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
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
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
149
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
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
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
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
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
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
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
352
353
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
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
394 elif mimetype.startswith('image/'):
395 img = io.open(filename, mode = 'rb')
396 attachment = MIMEImage(img.read())
397 img.close()
398
399 elif mimetype.startswith('audio/'):
400 song = io.open(filename, mode = 'rb')
401 attachment = MIMEAudio(song.read())
402 song.close()
403
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
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
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
502 email = compose_email (
503 message = 'compose_email() test: üü ßß',
504 files2attach = [[sys.argv[2]]]
505 )
506 print(send_email (
507
508 email = email,
509
510 auth = {'user': default_mail_sender, 'password': 'gnumed-at-gmx-net'},
511 debug = True
512 ))
513
514
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
533
534
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
546
547
548
549
550 test_send_email()
551
552
553
554
555