Clarified copyrights.
[zeroinstall.git] / zeroinstall / injector / download.py
blob759290431f090741c92a233e2b684d78188018c2
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
6 import traceback
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):
19 pass
21 class Download:
22 url = None
23 tempfile = None # Stream for result
24 status = None # download_*
25 errors = None
27 child_pid = None
28 child_stderr = None
30 def __init__(self, url):
31 "Initial status is starting."
32 self.url = url
33 self.status = download_starting
35 def start(self):
36 """Returns stderr stream from child. Call error_stream_closed() when
37 it returns EOF."""
38 assert self.status == download_starting
39 self.tempfile = tempfile.TemporaryFile(prefix = 'injector-dl-data-')
41 error_r, error_w = os.pipe()
42 self.errors = ''
44 self.child_pid = os.fork()
45 if self.child_pid == 0:
46 # We are the child
47 try:
48 os.close(error_r)
49 os.dup2(error_w, 2)
50 os.close(error_w)
51 self.download_as_child()
52 finally:
53 os._exit(1)
55 # We are the parent
56 os.close(error_w)
57 self.status = download_fetching
58 return os.fdopen(error_r, 'r')
60 def download_as_child(self):
61 try:
62 import shutil
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 " \
67 "exist!" % self.url
68 return
69 src = file(self.url)
70 elif self.url.startswith('http:') or self.url.startswith('ftp:'):
71 src = urlopen(self.url)
72 else:
73 raise Exception('Unsupported URL protocol in: ' + self.url)
75 shutil.copyfileobj(src, self.tempfile)
76 self.tempfile.flush()
78 os._exit(0)
79 except (HTTPError, URLError), ex:
80 print >>sys.stderr, "Error downloading '" + self.url + "': " + str(ex)
81 except:
82 traceback.print_exc()
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."""
87 assert data
88 assert self.status is download_fetching
89 self.errors += data
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
102 errors = self.errors
103 self.errors = None
105 if status and not errors:
106 errors = 'Download process exited with error status ' \
107 'code ' + hex(status)
109 stream = self.tempfile
110 self.tempfile = None
112 if errors:
113 self.status = download_failed
114 raise DownloadError(errors)
115 else:
116 self.status = download_checking
118 stream.seek(0)
119 return stream
121 def abort(self):
122 if self.child_pid is not None:
123 warn("Killing download process %s", self.child_pid)
124 import signal
125 os.kill(self.child_pid, signal.SIGTERM)
126 else:
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)
139 self.source = source
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'
146 'URL: %s\n'
147 'Expected: %d bytes\n'
148 'Received: %d bytes' % (self.url, self.source.size, size))
149 return stream
151 def get_current_fraction(self):
152 if self.status is download_starting:
153 return 0
154 if self.tempfile is None:
155 return 1
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):
168 for x in path_dirs:
169 if os.path.isfile(os.path.join(x, command)):
170 return True
171 return False
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)
184 if dl:
185 if force:
186 dl.abort()
187 del _downloads[new_dl.url]
188 else:
189 return None # Already downloading
191 _downloads[new_dl.url] = new_dl
193 assert new_dl.status == download_starting
194 return new_dl