Massive refactoring for tasks system.
[zeroinstall/zeroinstall-rsl.git] / zeroinstall / injector / download.py
blobe82dd5c26ef84ab997087c23efad960d6f18c2a3
1 """
2 Handles URL downloads.
4 This is the low-level interface for downloading interfaces, implementations, icons, etc.
6 @see: L{policy.Policy.begin_iface_download}
7 @see: L{policy.Policy.begin_archive_download}
8 @see: L{policy.Policy.begin_icon_download}
9 """
11 # Copyright (C) 2006, Thomas Leonard
12 # See the README file for details, or visit http://0install.net.
14 import tempfile, os, sys
15 from zeroinstall import SafeException
16 from zeroinstall.support import tasks
17 import traceback
18 from logging import info
20 download_starting = "starting" # Waiting for UI to start it
21 download_fetching = "fetching" # In progress
22 download_checking = "checking" # Checking GPG sig (possibly interactive)
23 download_complete = "complete" # Downloaded and cached OK
24 download_failed = "failed"
26 class DownloadError(SafeException):
27 pass
29 class Download(object):
30 __slots__ = ['url', 'tempfile', 'status', 'errors', 'expected_size', 'downloaded',
31 'expected_size', 'child_pid', 'child_stderr']
33 def __init__(self, url):
34 "Initial status is starting."
35 self.url = url
36 self.status = download_starting
37 self.downloaded = tasks.Blocker("Download " + url)
39 self.tempfile = None # Stream for result
40 self.errors = None
42 self.expected_size = None # Final size (excluding skipped bytes)
44 self.child_pid = None
45 self.child_stderr = None
47 def start(self):
48 """Returns stderr stream from child. Call error_stream_closed() when
49 it returns EOF."""
50 assert self.status == download_starting
51 self.tempfile = tempfile.TemporaryFile(prefix = 'injector-dl-data-')
53 error_r, error_w = os.pipe()
54 self.errors = ''
56 self.child_pid = os.fork()
57 if self.child_pid == 0:
58 # We are the child
59 try:
60 os.close(error_r)
61 os.dup2(error_w, 2)
62 os.close(error_w)
63 self.download_as_child()
64 finally:
65 os._exit(1)
67 # We are the parent
68 os.close(error_w)
69 self.status = download_fetching
70 return os.fdopen(error_r, 'r')
72 def download_as_child(self):
73 from urllib2 import urlopen, HTTPError, URLError
74 try:
75 import shutil
76 #print "Child downloading", self.url
77 if self.url.startswith('/'):
78 if not os.path.isfile(self.url):
79 print >>sys.stderr, "File '%s' does not " \
80 "exist!" % self.url
81 return
82 src = file(self.url)
83 elif self.url.startswith('http:') or self.url.startswith('ftp:'):
84 src = urlopen(self.url)
85 else:
86 raise Exception('Unsupported URL protocol in: ' + self.url)
88 shutil.copyfileobj(src, self.tempfile)
89 self.tempfile.flush()
91 os._exit(0)
92 except (HTTPError, URLError), ex:
93 print >>sys.stderr, "Error downloading '" + self.url + "': " + str(ex)
94 except:
95 traceback.print_exc()
97 def error_stream_data(self, data):
98 """Passed with result of os.read(error_stream, n). Can be
99 called multiple times, once for each read."""
100 assert data
101 assert self.status is download_fetching
102 self.errors += data
104 def error_stream_closed(self):
105 """Ends a download. Status changes from fetching to checking."""
106 assert self.status is download_fetching
107 assert self.tempfile is not None
108 assert self.child_pid is not None
110 pid, status = os.waitpid(self.child_pid, 0)
111 assert pid == self.child_pid
112 self.child_pid = None
114 errors = self.errors
115 self.errors = None
117 if status and not errors:
118 errors = 'Download process exited with error status ' \
119 'code ' + hex(status)
121 stream = self.tempfile
122 self.tempfile = None
124 try:
125 if errors:
126 raise DownloadError(errors)
128 # Check that the download has the correct size, if we know what it should be.
129 if self.expected_size is not None:
130 size = os.fstat(stream.fileno()).st_size
131 if size != self.expected_size:
132 raise SafeException('Downloaded archive has incorrect size.\n'
133 'URL: %s\n'
134 'Expected: %d bytes\n'
135 'Received: %d bytes' % (self.url, self.expected_size, size))
136 except:
137 self.status = download_failed
138 _, ex, tb = sys.exc_info()
139 self.downloaded.trigger(exception = (ex, tb))
140 else:
141 self.status = download_checking
142 self.downloaded.trigger()
144 def abort(self):
145 if self.child_pid is not None:
146 info("Killing download process %s", self.child_pid)
147 import signal
148 os.kill(self.child_pid, signal.SIGTERM)
149 else:
150 self.status = download_failed
152 def get_current_fraction(self):
153 """Returns the current fraction of this download that has been fetched (from 0 to 1),
154 or None if the total size isn't known."""
155 if self.status is download_starting:
156 return 0
157 if self.tempfile is None:
158 return 1
159 if self.expected_size is None:
160 return None # Unknown
161 current_size = os.fstat(self.tempfile.fileno()).st_size
162 return float(current_size) / self.expected_size
164 def __str__(self):
165 return "<Download from %s>" % self.url