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}
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
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
):
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."
36 self
.status
= download_starting
37 self
.downloaded
= tasks
.Blocker("Download " + url
)
39 self
.tempfile
= None # Stream for result
42 self
.expected_size
= None # Final size (excluding skipped bytes)
45 self
.child_stderr
= None
48 """Returns stderr stream from child. Call error_stream_closed() when
50 assert self
.status
== download_starting
51 self
.tempfile
= tempfile
.TemporaryFile(prefix
= 'injector-dl-data-')
53 error_r
, error_w
= os
.pipe()
56 self
.child_pid
= os
.fork()
57 if self
.child_pid
== 0:
63 self
.download_as_child()
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
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 " \
83 elif self
.url
.startswith('http:') or self
.url
.startswith('ftp:'):
84 src
= urlopen(self
.url
)
86 raise Exception('Unsupported URL protocol in: ' + self
.url
)
88 shutil
.copyfileobj(src
, self
.tempfile
)
92 except (HTTPError
, URLError
), ex
:
93 print >>sys
.stderr
, "Error downloading '" + self
.url
+ "': " + str(ex
)
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."""
101 assert self
.status
is download_fetching
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
117 if status
and not errors
:
118 errors
= 'Download process exited with error status ' \
119 'code ' + hex(status
)
121 stream
= self
.tempfile
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'
134 'Expected: %d bytes\n'
135 'Received: %d bytes' % (self
.url
, self
.expected_size
, size
))
137 self
.status
= download_failed
138 _
, ex
, tb
= sys
.exc_info()
139 self
.downloaded
.trigger(exception
= (ex
, tb
))
141 self
.status
= download_checking
142 self
.downloaded
.trigger()
145 if self
.child_pid
is not None:
146 info("Killing download process %s", self
.child_pid
)
148 os
.kill(self
.child_pid
, signal
.SIGTERM
)
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
:
157 if self
.tempfile
is None:
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
165 return "<Download from %s>" % self
.url