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
17 from logging
import info
19 download_starting
= "starting" # Waiting for UI to start it
20 download_fetching
= "fetching" # In progress
21 download_checking
= "checking" # Checking GPG sig (possibly interactive)
22 download_complete
= "complete" # Downloaded and cached OK
23 download_failed
= "failed"
25 class DownloadError(SafeException
):
28 class Download(object):
29 __slots__
= ['url', 'tempfile', 'status', 'errors', 'expected_size',
30 'expected_size', 'child_pid', 'child_stderr', 'on_success']
32 def __init__(self
, url
):
33 "Initial status is starting."
35 self
.status
= download_starting
38 self
.tempfile
= None # Stream for result
41 self
.expected_size
= None # Final size (excluding skipped bytes)
44 self
.child_stderr
= None
47 """Returns stderr stream from child. Call error_stream_closed() when
49 assert self
.status
== download_starting
50 self
.tempfile
= tempfile
.TemporaryFile(prefix
= 'injector-dl-data-')
52 error_r
, error_w
= os
.pipe()
55 self
.child_pid
= os
.fork()
56 if self
.child_pid
== 0:
62 self
.download_as_child()
68 self
.status
= download_fetching
69 return os
.fdopen(error_r
, 'r')
71 def download_as_child(self
):
72 from urllib2
import urlopen
, HTTPError
, URLError
75 #print "Child downloading", self.url
76 if self
.url
.startswith('/'):
77 if not os
.path
.isfile(self
.url
):
78 print >>sys
.stderr
, "File '%s' does not " \
82 elif self
.url
.startswith('http:') or self
.url
.startswith('ftp:'):
83 src
= urlopen(self
.url
)
85 raise Exception('Unsupported URL protocol in: ' + self
.url
)
87 shutil
.copyfileobj(src
, self
.tempfile
)
91 except (HTTPError
, URLError
), ex
:
92 print >>sys
.stderr
, "Error downloading '" + self
.url
+ "': " + str(ex
)
96 def error_stream_data(self
, data
):
97 """Passed with result of os.read(error_stream, n). Can be
98 called multiple times, once for each read."""
100 assert self
.status
is download_fetching
103 def error_stream_closed(self
):
104 """Ends a download. Status changes from fetching to checking.
105 Calls the on_success callbacks with the rewound data stream on success,
106 or throws DownloadError on failure."""
107 assert self
.status
is download_fetching
108 assert self
.tempfile
is not None
109 assert self
.child_pid
is not None
111 pid
, status
= os
.waitpid(self
.child_pid
, 0)
112 assert pid
== self
.child_pid
113 self
.child_pid
= None
118 if status
and not errors
:
119 errors
= 'Download process exited with error status ' \
120 'code ' + hex(status
)
122 stream
= self
.tempfile
126 error
= DownloadError(errors
)
130 # Check that the download has the correct size, if we know what it should be.
131 if self
.expected_size
is not None and not error
:
132 size
= os
.fstat(stream
.fileno()).st_size
133 if size
!= self
.expected_size
:
134 error
= SafeException('Downloaded archive has incorrect size.\n'
136 'Expected: %d bytes\n'
137 'Received: %d bytes' % (self
.url
, self
.expected_size
, size
))
140 self
.status
= download_failed
141 self
.on_success
= [] # Break GC cycles
144 self
.status
= download_checking
146 for x
in self
.on_success
:
151 if self
.child_pid
is not None:
152 info("Killing download process %s", self
.child_pid
)
154 os
.kill(self
.child_pid
, signal
.SIGTERM
)
156 self
.status
= download_failed
158 def get_current_fraction(self
):
159 """Returns the current fraction of this download that has been fetched (from 0 to 1),
160 or None if the total size isn't known."""
161 if self
.status
is download_starting
:
163 if self
.tempfile
is None:
165 if self
.expected_size
is None:
166 return None # Unknown
167 current_size
= os
.fstat(self
.tempfile
.fileno()).st_size
168 return float(current_size
) / self
.expected_size
171 return "<Download from %s>" % self
.url