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
9 # Copyright (C) 2009, Thomas Leonard
10 # See the README file for details, or visit http://0install.net.
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"
25 RESULT_NOT_MODIFIED
= 2
28 class DownloadError(SafeException
):
29 """Download process failed."""
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
41 @ivar tempfile: the file storing the downloaded data
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
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
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}."""
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
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
84 self
.tempfile
= tempfile
.TemporaryFile(prefix
= 'injector-dl-data-', mode
= 'w+b')
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
)
98 self
.unmodified
= True
99 self
.status
= download_complete
100 self
._final
_total
_size
= 0
103 self
._final
_total
_size
= self
.get_bytes_downloaded_so_far()
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'
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
})
118 self
.status
= download_failed
121 self
.status
= download_complete
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
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
)
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:
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.
157 if self
.status
is download_fetching
:
158 return os
.fstat(self
.tempfile
.fileno()).st_size
160 return self
._final
_total
_size
or 0
162 def get_next_mirror_url(self
):
163 """Return an alternative download URL to try, or None if we're out of options."""
169 return _("<Download from %s>") % self
.url