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) 2009, Thomas Leonard
10 # See the README file for details, or visit http://0install.net.
12 import tempfile
, os
, sys
, subprocess
13 from zeroinstall
import SafeException
14 from zeroinstall
.support
import tasks
15 from logging
import info
17 download_starting
= "starting" # Waiting for UI to start it
18 download_fetching
= "fetching" # In progress
19 download_complete
= "complete" # Downloaded and cached OK
20 download_failed
= "failed"
22 class DownloadError(SafeException
):
23 """Download process failed."""
26 class DownloadAborted(DownloadError
):
27 """Download aborted because of a call to L{Download.abort}"""
28 def __init__(self
, message
):
29 SafeException
.__init
__(self
, message
or "Download aborted at user's request")
31 class Download(object):
32 """A download of a single resource to a temporary file.
33 @ivar url: the URL of the resource being fetched
35 @ivar tempfile: the file storing the downloaded data
37 @ivar status: the status of the download
38 @type status: (download_starting | download_fetching | download_failed | download_complete)
39 @ivar errors: data received from the child's stderr
41 @ivar expected_size: the expected final size of the file
42 @type expected_size: int | None
43 @ivar downloaded: triggered when the download ends (on success or failure)
44 @type downloaded: L{tasks.Blocker}
45 @ivar hint: hint passed by and for caller
47 @ivar child: the child process
48 @type child: L{subprocess.Popen}
49 @ivar aborted_by_user: whether anyone has called L{abort}
50 @type aborted_by_user: bool
52 __slots__
= ['url', 'tempfile', 'status', 'errors', 'expected_size', 'downloaded',
53 'hint', 'child', '_final_total_size', 'aborted_by_user']
55 def __init__(self
, url
, hint
= None):
56 """Create a new download object.
57 @param url: the resource to download
58 @param hint: object with which this download is associated (an optional hint for the GUI)
59 @postcondition: L{status} == L{download_starting}."""
61 self
.status
= download_starting
63 self
.aborted_by_user
= False
65 self
.tempfile
= None # Stream for result
67 self
.downloaded
= None
69 self
.expected_size
= None # Final size (excluding skipped bytes)
70 self
._final
_total
_size
= None # Set when download is finished
75 """Create a temporary file and begin the download.
76 @precondition: L{status} == L{download_starting}"""
77 assert self
.status
== download_starting
78 assert self
.downloaded
is None
80 self
.tempfile
= tempfile
.TemporaryFile(prefix
= 'injector-dl-data-')
82 task
= tasks
.Task(self
._do
_download
(), "download " + self
.url
)
83 self
.downloaded
= task
.finished
85 def _do_download(self
):
86 """Will trigger L{downloaded} when done (on success or failure)."""
89 # Can't use fork here, because Windows doesn't have it
90 assert self
.child
is None, self
.child
91 self
.child
= subprocess
.Popen([sys
.executable
, '-u', __file__
, self
.url
], stderr
= subprocess
.PIPE
, stdout
= self
.tempfile
)
93 self
.status
= download_fetching
95 # Wait for child to exit, collecting error output as we go
98 yield tasks
.InputBlocker(self
.child
.stderr
, "read data from " + self
.url
)
100 data
= os
.read(self
.child
.stderr
.fileno(), 100)
105 # Download is complete...
107 assert self
.status
is download_fetching
108 assert self
.tempfile
is not None
109 assert self
.child
is not None
111 status
= self
.child
.wait()
117 if status
and not self
.aborted_by_user
and not errors
:
118 errors
= 'Download process exited with error status ' \
119 'code ' + hex(status
)
121 self
._final
_total
_size
= self
.get_bytes_downloaded_so_far()
123 stream
= self
.tempfile
127 if self
.aborted_by_user
:
128 raise DownloadAborted(errors
)
131 raise DownloadError(errors
.strip())
133 # Check that the download has the correct size, if we know what it should be.
134 if self
.expected_size
is not None:
135 size
= os
.fstat(stream
.fileno()).st_size
136 if size
!= self
.expected_size
:
137 raise SafeException('Downloaded archive has incorrect size.\n'
139 'Expected: %d bytes\n'
140 'Received: %d bytes' % (self
.url
, self
.expected_size
, size
))
142 self
.status
= download_failed
143 _
, ex
, tb
= sys
.exc_info()
144 self
.downloaded
.trigger(exception
= (ex
, tb
))
146 self
.status
= download_complete
147 self
.downloaded
.trigger()
150 """Signal the current download to stop.
151 @postcondition: L{aborted_by_user}"""
152 if self
.child
is not None:
153 info("Killing download process %s", self
.child
.pid
)
155 os
.kill(self
.child
.pid
, signal
.SIGTERM
)
156 self
.aborted_by_user
= True
158 self
.status
= download_failed
160 def get_current_fraction(self
):
161 """Returns the current fraction of this download that has been fetched (from 0 to 1),
162 or None if the total size isn't known.
163 @return: fraction downloaded
164 @rtype: int | None"""
165 if self
.status
is download_starting
:
167 if self
.tempfile
is None:
169 if self
.expected_size
is None:
170 return None # Unknown
171 current_size
= self
.get_bytes_downloaded_so_far()
172 return float(current_size
) / self
.expected_size
174 def get_bytes_downloaded_so_far(self
):
175 """Get the download progress. Will be zero if the download has not yet started.
177 if self
.status
is download_starting
:
179 elif self
.status
is download_fetching
:
180 return os
.fstat(self
.tempfile
.fileno()).st_size
182 return self
._final
_total
_size
185 return "<Download from %s>" % self
.url
187 if __name__
== '__main__':
188 def _download_as_child(url
):
189 from httplib
import HTTPException
190 from urllib2
import urlopen
, HTTPError
, URLError
192 #print "Child downloading", url
193 if url
.startswith('/'):
194 if not os
.path
.isfile(url
):
195 print >>sys
.stderr
, "File '%s' does not " \
199 elif url
.startswith('http:') or url
.startswith('ftp:'):
202 raise Exception('Unsupported URL protocol in: ' + url
)
205 data
= src
.fp
._sock
.recv(256)
210 except (HTTPError
, URLError
, HTTPException
), ex
:
211 print >>sys
.stderr
, "Error downloading '" + url
+ "': " + (str(ex
) or str(ex
.__class
__.__name
__))
213 assert len(sys
.argv
) == 2, "Usage: download URL, not %s" % sys
.argv
214 _download_as_child(sys
.argv
[1])