4 This is the low-level interface for downloading interfaces, implementations, icons, etc.
6 @see: L{fetch} higher-level API for downloads that uses this module
9 # Copyright (C) 2006, Thomas Leonard
10 # See the README file for details, or visit http://0install.net.
12 import tempfile
, os
, sys
13 from zeroinstall
import SafeException
14 from zeroinstall
.support
import tasks
16 from logging
import info
18 download_starting
= "starting" # Waiting for UI to start it
19 download_fetching
= "fetching" # In progress
20 download_complete
= "complete" # Downloaded and cached OK
21 download_failed
= "failed"
23 class DownloadError(SafeException
):
24 """Download process failed."""
27 class DownloadAborted(DownloadError
):
28 """Download aborted because of a call to L{Download.abort}"""
29 def __init__(self
, message
):
30 SafeException
.__init
__(self
, message
or "Download aborted at user's request")
32 class Download(object):
33 """A download of a single resource to a temporary file.
34 @ivar url: the URL of the resource being fetched
36 @ivar tempfile: the file storing the downloaded data
38 @ivar status: the status of the download
39 @type status: (download_starting | download_fetching | download_failed | download_complete)
40 @ivar errors: data received from the child's stderr
42 @ivar expected_size: the expected final size of the file
43 @type expected_size: int | None
44 @ivar downloaded: triggered when the download ends (on success or failure)
45 @type downloaded: L{tasks.Blocker}
46 @ivar hint: hint passed by and for caller
48 @ivar child_pid: the child process's PID
50 @ivar aborted_by_user: whether anyone has called L{abort}
51 @type aborted_by_user: bool
53 __slots__
= ['url', 'tempfile', 'status', 'errors', 'expected_size', 'downloaded',
54 'hint', 'child_pid', '_final_total_size', 'aborted_by_user']
56 def __init__(self
, url
, hint
= None):
57 """Create a new download object.
58 @param url: the resource to download
59 @param hint: object with which this download is associated (an optional hint for the GUI)
60 @postcondition: L{status} == L{download_starting}."""
62 self
.status
= download_starting
64 self
.aborted_by_user
= False
66 self
.tempfile
= None # Stream for result
68 self
.downloaded
= None
70 self
.expected_size
= None # Final size (excluding skipped bytes)
71 self
._final
_total
_size
= None # Set when download is finished
76 """Create a temporary file and begin the download.
77 @precondition: L{status} == L{download_starting}"""
78 assert self
.status
== download_starting
79 assert self
.downloaded
is None
81 self
.tempfile
= tempfile
.TemporaryFile(prefix
= 'injector-dl-data-')
83 task
= tasks
.Task(self
._do
_download
(), "download " + self
.url
)
84 self
.downloaded
= task
.finished
86 def _do_download(self
):
87 """Will trigger L{downloaded} when done (on success or failure)."""
88 error_r
, error_w
= os
.pipe()
91 self
.child_pid
= os
.fork()
92 if self
.child_pid
== 0:
98 self
._download
_as
_child
()
104 self
.status
= download_fetching
106 # Wait for child to exit, collecting error output as we go
109 yield tasks
.InputBlocker(error_r
, "read data from " + self
.url
)
111 data
= os
.read(error_r
, 100)
116 # Download is complete...
118 assert self
.status
is download_fetching
119 assert self
.tempfile
is not None
120 assert self
.child_pid
is not None
122 pid
, status
= os
.waitpid(self
.child_pid
, 0)
123 assert pid
== self
.child_pid
124 self
.child_pid
= None
129 if status
and not self
.aborted_by_user
and not errors
:
130 errors
= 'Download process exited with error status ' \
131 'code ' + hex(status
)
133 self
._final
_total
_size
= self
.get_bytes_downloaded_so_far()
135 stream
= self
.tempfile
139 if self
.aborted_by_user
:
140 raise DownloadAborted(errors
)
143 raise DownloadError(errors
.strip())
145 # Check that the download has the correct size, if we know what it should be.
146 if self
.expected_size
is not None:
147 size
= os
.fstat(stream
.fileno()).st_size
148 if size
!= self
.expected_size
:
149 raise SafeException('Downloaded archive has incorrect size.\n'
151 'Expected: %d bytes\n'
152 'Received: %d bytes' % (self
.url
, self
.expected_size
, size
))
154 self
.status
= download_failed
155 _
, ex
, tb
= sys
.exc_info()
156 self
.downloaded
.trigger(exception
= (ex
, tb
))
158 self
.status
= download_complete
159 self
.downloaded
.trigger()
161 def _download_as_child(self
):
163 from httplib
import HTTPException
164 from urllib2
import urlopen
, HTTPError
, URLError
167 #print "Child downloading", self.url
168 if self
.url
.startswith('/'):
169 if not os
.path
.isfile(self
.url
):
170 print >>sys
.stderr
, "File '%s' does not " \
174 elif self
.url
.startswith('http:') or self
.url
.startswith('ftp:'):
175 src
= urlopen(self
.url
)
177 raise Exception('Unsupported URL protocol in: ' + self
.url
)
179 shutil
.copyfileobj(src
, self
.tempfile
, length
=1)
180 self
.tempfile
.flush()
183 except (HTTPError
, URLError
, HTTPException
), ex
:
184 print >>sys
.stderr
, "Error downloading '" + self
.url
+ "': " + (str(ex
) or str(ex
.__class
__.__name
__))
186 traceback
.print_exc()
189 """Signal the current download to stop.
190 @postcondition: L{aborted_by_user}"""
191 if self
.child_pid
is not None:
192 info("Killing download process %s", self
.child_pid
)
194 os
.kill(self
.child_pid
, signal
.SIGTERM
)
195 self
.aborted_by_user
= True
197 self
.status
= download_failed
199 def get_current_fraction(self
):
200 """Returns the current fraction of this download that has been fetched (from 0 to 1),
201 or None if the total size isn't known.
202 @return: fraction downloaded
203 @rtype: int | None"""
204 if self
.status
is download_starting
:
206 if self
.tempfile
is None:
208 if self
.expected_size
is None:
209 return None # Unknown
210 current_size
= self
.get_bytes_downloaded_so_far()
211 return float(current_size
) / self
.expected_size
213 def get_bytes_downloaded_so_far(self
):
214 """Get the download progress. Will be zero if the download has not yet started.
216 if self
.status
is download_starting
:
218 elif self
.status
is download_fetching
:
219 return os
.fstat(self
.tempfile
.fileno()).st_size
221 return self
._final
_total
_size
224 return "<Download from %s>" % self
.url