Fixed bug where PackageKit downloaded the wrong architecture
[zeroinstall/solver.git] / zeroinstall / injector / download.py
blob84e5c1b619a9e986bd683d960f0ea66ca5323af3
1 """
2 Handles URL downloads.
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
7 """
9 # Copyright (C) 2009, Thomas Leonard
10 # See the README file for details, or visit http://0install.net.
12 import tempfile, os
14 from zeroinstall import SafeException
15 from zeroinstall.support import tasks
16 from zeroinstall import _, logger
18 download_starting = "starting" # Waiting for UI to start it (no longer used)
19 download_fetching = "fetching" # In progress
20 download_complete = "complete" # Downloaded and cached OK
21 download_failed = "failed"
23 RESULT_OK = 0
24 RESULT_FAILED = 1
25 RESULT_NOT_MODIFIED = 2
26 RESULT_REDIRECT = 3
28 class DownloadError(SafeException):
29 """Download process failed."""
30 pass
32 class DownloadAborted(DownloadError):
33 """Download aborted because of a call to L{Download.abort}"""
34 def __init__(self, message = None):
35 SafeException.__init__(self, message or _("Download aborted at user's request"))
37 class Download(object):
38 """A download of a single resource to a temporary file.
39 @ivar url: the URL of the resource being fetched
40 @type url: str
41 @ivar tempfile: the file storing the downloaded data
42 @type tempfile: file
43 @ivar status: the status of the download
44 @type status: (download_fetching | download_failed | download_complete)
45 @ivar expected_size: the expected final size of the file
46 @type expected_size: int | None
47 @ivar downloaded: triggered when the download ends (on success or failure)
48 @type downloaded: L{tasks.Blocker}
49 @ivar hint: hint passed by and for caller
50 @type hint: object
51 @ivar aborted_by_user: whether anyone has called L{abort}
52 @type aborted_by_user: bool
53 @ivar unmodified: whether the resource was not modified since the modification_time given at construction
54 @type unmodified: bool
55 @ivar mirror: an alternative URL to try if this download fails
56 @type mirror: str | None
57 """
58 __slots__ = ['url', 'tempfile', 'status', 'expected_size', 'downloaded',
59 'hint', '_final_total_size', 'aborted_by_user', 'mirror',
60 'modification_time', 'unmodified', '_aborted']
62 def __init__(self, url, hint = None, modification_time = None, expected_size = None, auto_delete = True):
63 """Create a new download object.
64 @param url: the resource to download
65 @param hint: object with which this download is associated (an optional hint for the GUI)
66 @param modification_time: string with HTTP date that indicates last modification time.
67 The resource will not be downloaded if it was not modified since that date.
68 @postcondition: L{status} == L{download_fetching}."""
69 self.url = url
70 self.hint = hint
71 self.aborted_by_user = False # replace with _aborted?
72 self.modification_time = modification_time
73 self.unmodified = False
75 self.tempfile = None # Stream for result
76 self.downloaded = None
77 self.mirror = None
79 self.expected_size = expected_size # Final size (excluding skipped bytes)
80 self._final_total_size = None # Set when download is finished
82 self.status = download_fetching
83 if auto_delete:
84 self.tempfile = tempfile.TemporaryFile(prefix = 'injector-dl-data-', mode = 'w+b')
85 else:
86 self.tempfile = tempfile.NamedTemporaryFile(prefix = 'injector-dl-data-', mode = 'w+b', delete = False)
88 self._aborted = tasks.Blocker("abort " + url)
90 def _finish(self, status):
91 assert self.status is download_fetching
92 assert self.tempfile is not None
93 assert not self.aborted_by_user
95 if status == RESULT_NOT_MODIFIED:
96 logger.debug("%s not modified", self.url)
97 self.tempfile = None
98 self.unmodified = True
99 self.status = download_complete
100 self._final_total_size = 0
101 return
103 self._final_total_size = self.get_bytes_downloaded_so_far()
105 self.tempfile = None
107 try:
108 assert status == RESULT_OK
110 # Check that the download has the correct size, if we know what it should be.
111 if self.expected_size is not None:
112 if self._final_total_size != self.expected_size:
113 raise SafeException(_('Downloaded archive has incorrect size.\n'
114 'URL: %(url)s\n'
115 'Expected: %(expected_size)d bytes\n'
116 'Received: %(size)d bytes') % {'url': self.url, 'expected_size': self.expected_size, 'size': self._final_total_size})
117 except:
118 self.status = download_failed
119 raise
120 else:
121 self.status = download_complete
123 def abort(self):
124 """Signal the current download to stop.
125 @postcondition: L{aborted_by_user}"""
126 self.status = download_failed
128 if self.tempfile is not None:
129 logger.info(_("Aborting download of %s"), self.url)
130 # TODO: we currently just close the output file; the thread will end when it tries to
131 # write to it. We should try harder to stop the thread immediately (e.g. by closing its
132 # socket when known), although we can never cover all cases (e.g. a stuck DNS lookup).
133 # In any case, we don't wait for the child to exit before notifying tasks that are waiting
134 # on us.
135 self.aborted_by_user = True
136 self.tempfile.close()
137 if hasattr(self.tempfile, 'delete') and not self.tempfile.delete:
138 os.remove(self.tempfile.name)
139 self.tempfile = None
140 self._aborted.trigger()
142 def get_current_fraction(self):
143 """Returns the current fraction of this download that has been fetched (from 0 to 1),
144 or None if the total size isn't known.
145 @return: fraction downloaded
146 @rtype: int | None"""
147 if self.tempfile is None:
148 return 1
149 if self.expected_size is None:
150 return None # Unknown
151 current_size = self.get_bytes_downloaded_so_far()
152 return float(current_size) / self.expected_size
154 def get_bytes_downloaded_so_far(self):
155 """Get the download progress. Will be zero if the download has not yet started.
156 @rtype: int"""
157 if self.status is download_fetching:
158 if self.tempfile.closed:
159 return 1
160 else:
161 return os.fstat(self.tempfile.fileno()).st_size
162 else:
163 return self._final_total_size or 0
165 def get_next_mirror_url(self):
166 """Return an alternative download URL to try, or None if we're out of options."""
167 mirror = self.mirror
168 self.mirror = None
169 return mirror
171 def __str__(self):
172 return _("<Download from %s>") % self.url