"""
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
syntax highlighted by Code2HTML, v. 0.9.1