""" Handles URL downloads. This is the low-level interface for downloading interfaces, implementations, icons, etc. @see: L{policy.Policy.begin_iface_download} @see: L{policy.Policy.begin_archive_download} @see: L{policy.Policy.begin_icon_download} """ # Copyright (C) 2006, Thomas Leonard # See the README file for details, or visit http://0install.net. import tempfile, os, sys from zeroinstall import SafeException import traceback from logging import info download_starting = "starting" # Waiting for UI to start it download_fetching = "fetching" # In progress download_checking = "checking" # Checking GPG sig (possibly interactive) download_complete = "complete" # Downloaded and cached OK download_failed = "failed" class DownloadError(SafeException): pass class Download(object): __slots__ = ['url', 'tempfile', 'status', 'errors', 'expected_size', 'expected_size', 'child_pid', 'child_stderr', 'on_success'] def __init__(self, url): "Initial status is starting." self.url = url self.status = download_starting self.on_success = [] self.tempfile = None # Stream for result self.errors = None self.expected_size = None # Final size (excluding skipped bytes) self.child_pid = None self.child_stderr = None def start(self): """Returns stderr stream from child. Call error_stream_closed() when it returns EOF.""" assert self.status == download_starting self.tempfile = tempfile.TemporaryFile(prefix = 'injector-dl-data-') error_r, error_w = os.pipe() self.errors = '' self.child_pid = os.fork() if self.child_pid == 0: # We are the child try: os.close(error_r) os.dup2(error_w, 2) os.close(error_w) self.download_as_child() finally: os._exit(1) # We are the parent os.close(error_w) self.status = download_fetching return os.fdopen(error_r, 'r') def download_as_child(self): from urllib2 import urlopen, HTTPError, URLError try: import shutil #print "Child downloading", self.url if self.url.startswith('/'): if not os.path.isfile(self.url): print >>sys.stderr, "File '%s' does not " \ "exist!" % self.url return src = file(self.url) elif self.url.startswith('http:') or self.url.startswith('ftp:'): src = urlopen(self.url) else: raise Exception('Unsupported URL protocol in: ' + self.url) shutil.copyfileobj(src, self.tempfile) self.tempfile.flush() os._exit(0) except (HTTPError, URLError), ex: print >>sys.stderr, "Error downloading '" + self.url + "': " + str(ex) except: traceback.print_exc() def error_stream_data(self, data): """Passed with result of os.read(error_stream, n). Can be called multiple times, once for each read.""" assert data assert self.status is download_fetching self.errors += data def error_stream_closed(self): """Ends a download. Status changes from fetching to checking. Calls the on_success callbacks with the rewound data stream on success, or throws DownloadError on failure.""" assert self.status is download_fetching assert self.tempfile is not None assert self.child_pid is not None pid, status = os.waitpid(self.child_pid, 0) assert pid == self.child_pid self.child_pid = None errors = self.errors self.errors = None if status and not errors: errors = 'Download process exited with error status ' \ 'code ' + hex(status) stream = self.tempfile self.tempfile = None if errors: error = DownloadError(errors) else: error = None # Check that the download has the correct size, if we know what it should be. if self.expected_size is not None and not error: size = os.fstat(stream.fileno()).st_size if size != self.expected_size: error = SafeException('Downloaded archive has incorrect size.\n' 'URL: %s\n' 'Expected: %d bytes\n' 'Received: %d bytes' % (self.url, self.expected_size, size)) if error: self.status = download_failed self.on_success = [] # Break GC cycles raise error else: self.status = download_checking for x in self.on_success: stream.seek(0) x(stream) def abort(self): if self.child_pid is not None: info("Killing download process %s", self.child_pid) import signal os.kill(self.child_pid, signal.SIGTERM) else: self.status = download_failed def get_current_fraction(self): """Returns the current fraction of this download that has been fetched (from 0 to 1), or None if the total size isn't known.""" if self.status is download_starting: return 0 if self.tempfile is None: return 1 if self.expected_size is None: return None # Unknown current_size = os.fstat(self.tempfile.fileno()).st_size return float(current_size) / self.expected_size