1 # Copyright (C) 2006, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 import tempfile
, os
, sys
5 from model
import Interface
, DownloadSource
, SafeException
7 from urllib2
import urlopen
, HTTPError
, URLError
8 from logging
import warn
10 download_starting
= "starting" # Waiting for UI to start it
11 download_fetching
= "fetching" # In progress
12 download_checking
= "checking" # Checking GPG sig (possibly interactive)
13 download_complete
= "complete" # Downloaded and cached OK
14 download_failed
= "failed"
16 _downloads
= {} # URL -> Download
18 class DownloadError(SafeException
):
23 tempfile
= None # Stream for result
24 status
= None # download_*
30 def __init__(self
, url
):
31 "Initial status is starting."
33 self
.status
= download_starting
36 """Returns stderr stream from child. Call error_stream_closed() when
38 assert self
.status
== download_starting
39 self
.tempfile
= tempfile
.TemporaryFile(prefix
= 'injector-dl-data-')
41 error_r
, error_w
= os
.pipe()
44 self
.child_pid
= os
.fork()
45 if self
.child_pid
== 0:
51 self
.download_as_child()
57 self
.status
= download_fetching
58 return os
.fdopen(error_r
, 'r')
60 def download_as_child(self
):
63 #print "Child downloading", self.url
64 if self
.url
.startswith('/'):
65 if not os
.path
.isfile(self
.url
):
66 print >>sys
.stderr
, "File '%s' does not " \
70 elif self
.url
.startswith('http:') or self
.url
.startswith('ftp:'):
71 src
= urlopen(self
.url
)
73 raise Exception('Unsupported URL protocol in: ' + self
.url
)
75 shutil
.copyfileobj(src
, self
.tempfile
)
79 except (HTTPError
, URLError
), ex
:
80 print >>sys
.stderr
, "Error downloading '" + self
.url
+ "': " + str(ex
)
84 def error_stream_data(self
, data
):
85 """Passed with result of os.read(error_stream, n). Can be
86 called multiple times, once for each read."""
88 assert self
.status
is download_fetching
91 def error_stream_closed(self
):
92 """Ends a download. Status changes from fetching to checking.
93 Returns data stream."""
94 assert self
.status
is download_fetching
95 assert self
.tempfile
is not None
96 assert self
.child_pid
is not None
98 pid
, status
= os
.waitpid(self
.child_pid
, 0)
99 assert pid
== self
.child_pid
100 self
.child_pid
= None
105 if status
and not errors
:
106 errors
= 'Download process exited with error status ' \
107 'code ' + hex(status
)
109 stream
= self
.tempfile
113 self
.status
= download_failed
114 raise DownloadError(errors
)
116 self
.status
= download_checking
122 if self
.child_pid
is not None:
123 warn("Killing download process %s", self
.child_pid
)
125 os
.kill(self
.child_pid
, signal
.SIGTERM
)
127 self
.status
= download_failed
129 class InterfaceDownload(Download
):
130 def __init__(self
, interface
, url
= None):
131 assert isinstance(interface
, Interface
)
132 Download
.__init
__(self
, url
or interface
.uri
)
133 self
.interface
= interface
135 class ImplementationDownload(Download
):
136 def __init__(self
, source
):
137 assert isinstance(source
, DownloadSource
)
138 Download
.__init
__(self
, source
.url
)
141 def error_stream_closed(self
):
142 stream
= Download
.error_stream_closed(self
)
143 size
= os
.fstat(stream
.fileno()).st_size
144 if size
!= self
.source
.size
:
145 raise SafeException('Downloaded archive has incorrect size.\n'
147 'Expected: %d bytes\n'
148 'Received: %d bytes' % (self
.url
, self
.source
.size
, size
))
151 def get_current_fraction(self
):
152 if self
.status
is download_starting
:
154 if self
.tempfile
is None:
156 current_size
= os
.fstat(self
.tempfile
.fileno()).st_size
157 return float(current_size
) / self
.source
.size
159 def begin_iface_download(interface
, force
):
160 """Start downloading interface.
161 If a Download object already exists (any state; in progress, failed or
162 completed) and force is False, does nothing and returns None.
163 If force is True, any existing download is destroyed and a new one created."""
164 return _begin_download(InterfaceDownload(interface
), force
)
166 path_dirs
= os
.environ
.get('PATH', '/bin:/usr/bin').split(':')
167 def _available_in_path(command
):
169 if os
.path
.isfile(os
.path
.join(x
, command
)):
173 def begin_impl_download(source
, force
= False):
174 #print "Need to downlaod", source.url
175 if source
.url
.endswith('.rpm'):
176 if not _available_in_path('rpm2cpio'):
177 raise SafeException("The URL '%s' looks like an RPM, but you don't have the rpm2cpio command "
178 "I need to extract it. Install the 'rpm' package first (this works even if "
179 "you're on a non-RPM-based distribution such as Debian)." % source
.url
)
180 return _begin_download(ImplementationDownload(source
), force
)
182 def _begin_download(new_dl
, force
):
183 dl
= _downloads
.get(new_dl
.url
, None)
187 del _downloads
[new_dl
.url
]
189 return None # Already downloading
191 _downloads
[new_dl
.url
] = new_dl
193 assert new_dl
.status
== download_starting