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 logging import info, debug
17 from zeroinstall import _
18
19 download_starting = "starting"
20 download_fetching = "fetching"
21 download_complete = "complete"
22 download_failed = "failed"
23
24 RESULT_OK = 0
25 RESULT_FAILED = 1
26 RESULT_NOT_MODIFIED = 2
27 RESULT_REDIRECT = 3
28
30 """Download process failed."""
31 pass
32
34 """Download aborted because of a call to L{Download.abort}"""
37
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 """
57 __slots__ = ['url', 'tempfile', 'status', 'expected_size', 'downloaded',
58 'hint', '_final_total_size', 'aborted_by_user',
59 'modification_time', 'unmodified', '_aborted']
60
61 - def __init__(self, url, hint = None, modification_time = None, expected_size = None):
62 """Create a new download object.
63 @param url: the resource to download
64 @param hint: object with which this download is associated (an optional hint for the GUI)
65 @param modification_time: string with HTTP date that indicates last modification time.
66 The resource will not be downloaded if it was not modified since that date.
67 @postcondition: L{status} == L{download_fetching}."""
68 self.url = url
69 self.hint = hint
70 self.aborted_by_user = False
71 self.modification_time = modification_time
72 self.unmodified = False
73
74 self.tempfile = None
75 self.downloaded = None
76
77 self.expected_size = expected_size
78 self._final_total_size = None
79
80 self.status = download_fetching
81 self.tempfile = tempfile.TemporaryFile(prefix = 'injector-dl-data-')
82
83 self._aborted = tasks.Blocker("abort " + url)
84
86 assert self.status is download_fetching
87 assert self.tempfile is not None
88 assert not self.aborted_by_user
89
90 if status == RESULT_NOT_MODIFIED:
91 debug("%s not modified", self.url)
92 self.tempfile = None
93 self.unmodified = True
94 self.status = download_complete
95 self._final_total_size = 0
96 return
97
98 self._final_total_size = self.get_bytes_downloaded_so_far()
99
100 self.tempfile = None
101
102 try:
103 assert status == RESULT_OK
104
105
106 if self.expected_size is not None:
107 if self._final_total_size != self.expected_size:
108 raise SafeException(_('Downloaded archive has incorrect size.\n'
109 'URL: %(url)s\n'
110 'Expected: %(expected_size)d bytes\n'
111 'Received: %(size)d bytes') % {'url': self.url, 'expected_size': self.expected_size, 'size': self._final_total_size})
112 except:
113 self.status = download_failed
114 raise
115 else:
116 self.status = download_complete
117
119 """Signal the current download to stop.
120 @postcondition: L{aborted_by_user}"""
121 self.status = download_failed
122
123 if self.tempfile is not None:
124 info(_("Aborting download of %s"), self.url)
125
126
127
128
129
130 self.aborted_by_user = True
131 self.tempfile.close()
132 self.tempfile = None
133 self._aborted.trigger()
134
136 """Returns the current fraction of this download that has been fetched (from 0 to 1),
137 or None if the total size isn't known.
138 @return: fraction downloaded
139 @rtype: int | None"""
140 if self.tempfile is None:
141 return 1
142 if self.expected_size is None:
143 return None
144 current_size = self.get_bytes_downloaded_so_far()
145 return float(current_size) / self.expected_size
146
148 """Get the download progress. Will be zero if the download has not yet started.
149 @rtype: int"""
150 if self.status is download_fetching:
151 return os.fstat(self.tempfile.fileno()).st_size
152 else:
153 return self._final_total_size or 0
154
156 return _("<Download from %s>") % self.url
157