1 """
2 Handles URL downloads.
3
4 This is the low-level interface for downloading interfaces, implementations, icons, etc.
5
6 @see: L{fetch} higher-level API for downloads that uses this module
7 """
8
9
10
11
12 import tempfile, os
13
14 from zeroinstall import SafeException
15 from zeroinstall.support import tasks
16 from zeroinstall import _, logger
17
18 download_starting = "starting"
19 download_fetching = "fetching"
20 download_complete = "complete"
21 download_failed = "failed"
22
23 RESULT_OK = 0
24 RESULT_FAILED = 1
25 RESULT_NOT_MODIFIED = 2
26 RESULT_REDIRECT = 3
27
29 """Download process failed."""
30 pass
31
33 """Download aborted because of a call to L{Download.abort}"""
36
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']
61
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 @type url: str
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. The resource will not be downloaded if it was not modified since that date.
68 @type modification_time: str | None
69 @type auto_delete: bool
70 @postcondition: L{status} == L{download_fetching}."""
71 assert auto_delete in (True, False)
72 self.url = url
73 self.hint = hint
74 self.aborted_by_user = False
75 self.modification_time = modification_time
76 self.unmodified = False
77
78 self.tempfile = None
79 self.downloaded = None
80 self.mirror = None
81
82 self.expected_size = expected_size
83 self._final_total_size = None
84
85 self.status = download_fetching
86 if auto_delete:
87 self.tempfile = tempfile.TemporaryFile(prefix = 'injector-dl-data-', mode = 'w+b')
88 else:
89 self.tempfile = tempfile.NamedTemporaryFile(prefix = 'injector-dl-data-', mode = 'w+b', delete = False)
90
91 self._aborted = tasks.Blocker("abort " + url)
92
94 """@type status: int"""
95 assert self.status is download_fetching
96 assert self.tempfile is not None
97 assert not self.aborted_by_user
98
99 if status == RESULT_NOT_MODIFIED:
100 logger.debug("%s not modified", self.url)
101 self.tempfile = None
102 self.unmodified = True
103 self.status = download_complete
104 self._final_total_size = 0
105 return
106
107 self._final_total_size = self.get_bytes_downloaded_so_far()
108
109 self.tempfile = None
110
111 try:
112 assert status == RESULT_OK
113
114
115 if self.expected_size is not None:
116 if self._final_total_size != self.expected_size:
117 raise SafeException(_('Downloaded archive has incorrect size.\n'
118 'URL: %(url)s\n'
119 'Expected: %(expected_size)d bytes\n'
120 'Received: %(size)d bytes') % {'url': self.url, 'expected_size': self.expected_size, 'size': self._final_total_size})
121 except:
122 self.status = download_failed
123 raise
124 else:
125 self.status = download_complete
126
128 """Signal the current download to stop.
129 @postcondition: L{aborted_by_user}"""
130 self.status = download_failed
131
132 if self.tempfile is not None:
133 logger.info(_("Aborting download of %s"), self.url)
134
135
136
137
138
139 self.aborted_by_user = True
140 self.tempfile.close()
141 if hasattr(self.tempfile, 'delete') and not self.tempfile.delete:
142 os.remove(self.tempfile.name)
143 self.tempfile = None
144 self._aborted.trigger()
145
147 """Returns the current fraction of this download that has been fetched (from 0 to 1),
148 or None if the total size isn't known.
149 @return: fraction downloaded
150 @rtype: int | None"""
151 if self.tempfile is None:
152 return 1
153 if self.expected_size is None:
154 return None
155 current_size = self.get_bytes_downloaded_so_far()
156 return float(current_size) / self.expected_size
157
159 """Get the download progress. Will be zero if the download has not yet started.
160 @rtype: int"""
161 if self.status is download_fetching:
162 if self.tempfile.closed:
163 return 1
164 else:
165 return os.fstat(self.tempfile.fileno()).st_size
166 else:
167 return self._final_total_size or 0
168
170 """Return an alternative download URL to try, or None if we're out of options.
171 @rtype: str"""
172 mirror = self.mirror
173 self.mirror = None
174 return mirror
175
177 return _("<Download from %s>") % self.url
178