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