24 from BeautifulSoup import BeautifulSoup |
24 from BeautifulSoup import BeautifulSoup |
25 from optparse import OptionParser |
25 from optparse import OptionParser |
26 import hashlib |
26 import hashlib |
27 import xml.etree.ElementTree as ET |
27 import xml.etree.ElementTree as ET |
28 |
28 |
29 version = '0.16' |
29 version = '0.17' |
30 user_agent = 'downloadkit.py script v' + version |
30 user_agent = 'downloadkit.py script v' + version |
31 headers = { 'User-Agent' : user_agent } |
31 headers = { 'User-Agent' : user_agent } |
32 top_level_url = "https://developer.symbian.org" |
32 top_level_url = "https://developer.symbian.org" |
33 passman = urllib2.HTTPPasswordMgrWithDefaultRealm() # not relevant for live Symbian website |
33 passman = urllib2.HTTPPasswordMgrWithDefaultRealm() # not relevant for live Symbian website |
34 download_list = [] |
34 download_list = [] |
|
35 failure_list = [] |
35 unzip_list = [] |
36 unzip_list = [] |
36 |
37 |
37 def build_opener(debug=False): |
38 def build_opener(debug=False): |
38 # Create a HTTP and HTTPS handler with the appropriate debug |
39 # Create a HTTP and HTTPS handler with the appropriate debug |
39 # level. We intentionally create a new one because the |
40 # level. We intentionally create a new one because the |
251 md5.update(data) |
252 md5.update(data) |
252 file.close() |
253 file.close() |
253 return md5.hexdigest().upper() |
254 return md5.hexdigest().upper() |
254 |
255 |
255 checksums = {} |
256 checksums = {} |
|
257 filesizes = {} |
256 def parse_release_metadata(filename): |
258 def parse_release_metadata(filename): |
257 if os.path.exists(filename): |
259 if os.path.exists(filename): |
258 tree = ET.parse(filename) |
260 tree = ET.parse(filename) |
259 iter = tree.getiterator('package') |
261 iter = tree.getiterator('package') |
260 for element in iter: |
262 for element in iter: |
261 if element.keys(): |
263 if element.keys(): |
262 file = element.get("name") |
264 file = element.get("name") |
263 md5 = element.get("md5checksum") |
265 md5 = element.get("md5checksum") |
264 checksums[file] = md5.upper() |
266 checksums[file] = md5.upper() |
|
267 size = element.get("size") |
|
268 filesizes[file] = int(size) |
265 |
269 |
266 def download_file(filename,url): |
270 def download_file(filename,url): |
267 global options |
271 global options |
268 global checksums |
272 global checksums |
|
273 global filesizes |
|
274 resume_start = 0 |
269 if os.path.exists(filename): |
275 if os.path.exists(filename): |
270 if filename in checksums: |
276 if filename in checksums: |
271 print 'Checking existing ' + filename |
277 print 'Checking existing ' + filename |
272 file_checksum = md5_checksum(filename) |
278 file_size = os.stat(filename).st_size |
273 if file_checksum == checksums[filename]: |
279 if file_size == filesizes[filename]: |
|
280 file_checksum = md5_checksum(filename) |
|
281 if file_checksum == checksums[filename]: |
|
282 if options.progress: |
|
283 print '- OK ' + filename |
|
284 return True |
|
285 elif file_size < filesizes[filename]: |
274 if options.progress: |
286 if options.progress: |
275 print '- OK ' + filename |
287 print '- %s is too short' % (filename) |
276 return True |
288 if options.debug: |
|
289 print '- %s is %d bytes, should be %d bytes' % (filename, file_size, filesizes[filename]) |
|
290 if options.resume: |
|
291 resume_start = file_size |
277 |
292 |
278 if options.dryrun and not re.match(r"release_metadata", filename): |
293 if options.dryrun and not re.match(r"release_metadata", filename): |
279 global download_list |
294 global download_list |
280 download_info = "download %s %s" % (filename, url) |
295 download_info = "download %s %s" % (filename, url) |
281 download_list.append(download_info) |
296 download_list.append(download_info) |
282 return True |
297 return True |
283 |
298 |
284 print 'Downloading ' + filename |
299 print 'Downloading ' + filename |
285 global headers |
300 global headers |
286 req = urllib2.Request(url, None, headers) |
301 request_headers = headers.copy() # want a fresh copy for each request |
|
302 if resume_start > 0: |
|
303 request_headers['Range'] = "bytes=%d-%d" % (resume_start, filesizes[filename]) |
|
304 req = urllib2.Request(url, None, request_headers) |
287 |
305 |
288 CHUNK = 128 * 1024 |
306 CHUNK = 128 * 1024 |
289 size = 0 |
307 size = 0 |
290 filesize = -1 |
308 filesize = -1 |
291 start_time = time.time() |
309 start_time = time.time() |
295 response = urllib2.urlopen(req) |
313 response = urllib2.urlopen(req) |
296 chunk = response.read(CHUNK) |
314 chunk = response.read(CHUNK) |
297 if chunk.find('<div id="sign_in_box">') != -1: |
315 if chunk.find('<div id="sign_in_box">') != -1: |
298 # our urllib2 cookies have gone awol - login again |
316 # our urllib2 cookies have gone awol - login again |
299 login(False) |
317 login(False) |
300 req = urllib2.Request(url, None, headers) |
318 req = urllib2.Request(url, None, request_headers) |
301 response = urllib2.urlopen(req) |
319 response = urllib2.urlopen(req) |
302 chunk = response.read(CHUNK) |
320 chunk = response.read(CHUNK) |
303 if chunk.find('<div id="sign_in_box">') != -1: |
321 if chunk.find('<div id="sign_in_box">') != -1: |
304 # still broken - give up on this one |
322 # still broken - give up on this one |
305 print "*** ERROR trying to download %s" % (filename) |
323 print "*** ERROR trying to download %s" % (filename) |
306 return False |
324 return False |
307 info = response.info() |
325 info = response.info() |
308 if 'Content-Length' in info: |
326 if 'Content-Length' in info: |
309 filesize = int(info['Content-Length']) |
327 filesize = resume_start + int(info['Content-Length']) # NB. length of the requested content, taking into account the range |
|
328 if resume_start > 0 and 'Content-Range' not in info: |
|
329 # server doesn't believe in our range |
|
330 filesize = int(info['Content-Length']) |
|
331 if options.debug: |
|
332 print "Server reports filesize as %d, ignoring our range request (%d-%d)" % (filesize, resume_start, filesizes[filename]) |
|
333 resume_start = 0; # will have to download from scratch |
|
334 if filename in filesizes: |
|
335 if filesize != filesizes[filename]: |
|
336 print "WARNING: %s size %d does not match release_metadata.xml (%d)" % ( filename, filesize, filesizes[filename]) |
310 else: |
337 else: |
311 match = re.search('>([^>]+Licen[^<]+)<', chunk, re.IGNORECASE) |
338 match = re.search('>([^>]+Licen[^<]+)<', chunk, re.IGNORECASE) |
312 if match: |
339 if match: |
313 license = match.group(1).replace('&','&') |
340 license = match.group(1).replace('&','&') |
314 print "*** %s is subject to the %s which you have not yet accepted\n" % (filename,license) |
341 print "*** %s is subject to the %s which you have not yet accepted\n" % (filename,license) |
326 elif hasattr(e, 'code'): |
353 elif hasattr(e, 'code'): |
327 print 'Error code: ', e.code |
354 print 'Error code: ', e.code |
328 return False |
355 return False |
329 |
356 |
330 # we are now up and running, and chunk contains the start of the download |
357 # we are now up and running, and chunk contains the start of the download |
331 |
358 if options.debug: |
|
359 print "\nReading %s from effective URL %s" % (filename, response.geturl()) |
|
360 |
332 try: |
361 try: |
333 fp = open(filename, 'wb') |
362 if resume_start > 0: |
|
363 fp = open(filename, 'a+b') # append to existing content |
|
364 if options.progress: |
|
365 print " - Resuming at offset %d" % (resume_start) |
|
366 size = resume_start |
|
367 last_size = size |
|
368 else: |
|
369 fp = open(filename, 'wb') # write new file |
334 md5 = hashlib.md5() |
370 md5 = hashlib.md5() |
335 while True: |
371 while True: |
336 fp.write(chunk) |
372 fp.write(chunk) |
337 md5.update(chunk) |
373 md5.update(chunk) |
338 size += len(chunk) |
374 size += len(chunk) |
352 last_size = size |
388 last_size = size |
353 chunk = response.read(CHUNK) |
389 chunk = response.read(CHUNK) |
354 if not chunk: break |
390 if not chunk: break |
355 |
391 |
356 fp.close() |
392 fp.close() |
357 if options.progress: |
|
358 now = time.time() |
|
359 print "- Completed %s - %d Kb in %d seconds" % (filename, (filesize/1024)+0.5, now-start_time) |
|
360 |
393 |
361 #handle errors |
394 #handle errors |
362 except urllib2.URLError, e: |
395 except urllib2.URLError, e: |
363 print '- ERROR: Failed while downloading ' + filename |
396 print '- ERROR: Failed while downloading ' + filename |
364 if hasattr(e, 'reason'): |
397 if hasattr(e, 'reason'): |
365 print 'Reason: ', e.reason |
398 print 'Reason: ', e.reason |
366 elif hasattr(e, 'code'): |
399 elif hasattr(e, 'code'): |
367 print 'Error code: ', e.code |
400 print 'Error code: ', e.code |
368 return False |
401 return False |
369 |
402 |
|
403 if options.debug: |
|
404 info = response.info() |
|
405 print "Info from final response of transfer:" |
|
406 print response.info() |
|
407 |
|
408 if filesize > 0 and size != filesize: |
|
409 print "Incomplete transfer - only received %d bytes of the expected %d byte file" % (size, filesize) |
|
410 return False |
|
411 |
|
412 if options.progress: |
|
413 now = time.time() |
|
414 print "- Completed %s - %d Kb in %d seconds" % (filename, (filesize/1024)+0.5, now-start_time) |
|
415 |
370 if filename in checksums: |
416 if filename in checksums: |
371 download_checksum = md5.hexdigest().upper() |
417 download_checksum = md5.hexdigest().upper() |
|
418 if resume_start > 0: |
|
419 # did a partial download, so need to checksum the whole file |
|
420 download_checksum = md5_checksum(filename) |
372 if download_checksum != checksums[filename]: |
421 if download_checksum != checksums[filename]: |
373 print '- WARNING: %s checksum does not match' % filename |
422 if options.debug: |
|
423 print '- Checksum for %s was %s, expected %s' % (filename, download_checksum, checksums[filename]) |
|
424 print '- ERROR: %s checksum does not match' % filename |
|
425 return False |
374 |
426 |
375 return True |
427 return True |
376 |
428 |
377 def downloadkit(version): |
429 def downloadkit(version): |
378 global headers |
430 global headers |
379 global options |
431 global options |
|
432 global failure_list |
380 urlbase = top_level_url + '/main/tools_and_kits/downloads/' |
433 urlbase = top_level_url + '/main/tools_and_kits/downloads/' |
381 |
434 |
382 viewid = 5 # default to Symbian^3 |
435 viewid = 5 # default to Symbian^3 |
383 if version[0] == '2': |
436 if version[0] == '2': |
384 viewid= 1 # Symbian^2 |
437 viewid= 1 # Symbian^2 |
437 if options.noarmv5 and re.search(r"armv5", filename) : |
490 if options.noarmv5 and re.search(r"armv5", filename) : |
438 continue # no armv5 emulator... |
491 continue # no armv5 emulator... |
439 if options.noarmv5 and options.nowinscw and re.search(r"binaries_epoc.zip|binaries_epoc_sdk", filename) : |
492 if options.noarmv5 and options.nowinscw and re.search(r"binaries_epoc.zip|binaries_epoc_sdk", filename) : |
440 continue # skip binaries_epoc and binaries_sdk ... |
493 continue # skip binaries_epoc and binaries_sdk ... |
441 if download_file(filename, downloadurl) != True : |
494 if download_file(filename, downloadurl) != True : |
|
495 failure_list.append(filename) |
442 continue # download failed |
496 continue # download failed |
443 |
497 |
444 # unzip the file (if desired) |
498 # unzip the file (if desired) |
445 if re.match(r"patch", filename): |
499 if re.match(r"patch", filename): |
446 complete_outstanding_unzips() # ensure that the thing we are patching is completed first |
500 complete_outstanding_unzips() # ensure that the thing we are patching is completed first |
455 schedule_unzip(filename, 1, 1) # unpack then delete zip as it's not needed again |
509 schedule_unzip(filename, 1, 1) # unpack then delete zip as it's not needed again |
456 |
510 |
457 # wait for the unzipping threads to complete |
511 # wait for the unzipping threads to complete |
458 complete_outstanding_unzips() |
512 complete_outstanding_unzips() |
459 |
513 |
|
514 if len(failure_list) > 0: |
|
515 print "\n" |
|
516 print "Downloading completed, with failures in %d files\n" % (len(failure_list)) |
|
517 print "\n\t".join(failure_list) |
|
518 print "\n" |
|
519 elif not options.dryrun: |
|
520 print "\nDownloading completed successfully" |
460 return 1 |
521 return 1 |
461 |
522 |
462 parser = OptionParser(version="%prog "+version, usage="Usage: %prog [options] version") |
523 parser = OptionParser(version="%prog "+version, usage="Usage: %prog [options] version") |
463 parser.add_option("-n", "--dryrun", action="store_true", dest="dryrun", |
524 parser.add_option("-n", "--dryrun", action="store_true", dest="dryrun", |
464 help="print the files to be downloaded, the 7z commands, and the recommended deletions") |
525 help="print the files to be downloaded, the 7z commands, and the recommended deletions") |
480 help="specify the account password") |
541 help="specify the account password") |
481 parser.add_option("--debug", action="store_true", dest="debug", |
542 parser.add_option("--debug", action="store_true", dest="debug", |
482 help="debug HTML traffic (not recommended!)") |
543 help="debug HTML traffic (not recommended!)") |
483 parser.add_option("--webhost", dest="webhost", metavar="SITE", |
544 parser.add_option("--webhost", dest="webhost", metavar="SITE", |
484 help="use alternative website (for testing!)") |
545 help="use alternative website (for testing!)") |
|
546 parser.add_option("--noresume", action="store_false", dest="resume", |
|
547 help="Do not attempt to continue a previous failed transfer") |
485 parser.set_defaults( |
548 parser.set_defaults( |
486 dryrun=False, |
549 dryrun=False, |
487 nosrc=False, |
550 nosrc=False, |
488 nowinscw=False, |
551 nowinscw=False, |
489 noarmv5=False, |
552 noarmv5=False, |