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.
12 import tempfile
, os
, sys
, threading
, gobject
14 from zeroinstall
import SafeException
15 from zeroinstall
.support
import tasks
16 from zeroinstall
.injector
import wget
17 from logging
import info
, debug
18 from zeroinstall
import _
20 download_starting
= "starting" # Waiting for UI to start it (no longer used)
21 download_fetching
= "fetching" # In progress
22 download_complete
= "complete" # Downloaded and cached OK
23 download_failed
= "failed"
25 # NB: duplicated in _download_child.py
28 RESULT_NOT_MODIFIED
= 2
30 class DownloadError(SafeException
):
31 """Download process failed."""
34 class DownloadAborted(DownloadError
):
35 """Download aborted because of a call to L{Download.abort}"""
36 def __init__(self
, message
= None):
37 SafeException
.__init
__(self
, message
or _("Download aborted at user's request"))
39 class Download(gobject
.GObject
):
40 """A download of a single resource to a temporary file.
41 @ivar url: the URL of the resource being fetched
43 @ivar tempfile: the file storing the downloaded data
45 @ivar status: the status of the download
46 @type status: (download_fetching | download_failed | download_complete)
47 @ivar expected_size: the expected final size of the file
48 @type expected_size: int | None
49 @ivar downloaded: triggered when the download ends (on success or failure)
50 @type downloaded: L{tasks.Blocker}
51 @ivar hint: hint passed by and for caller
53 @ivar child: the child process
54 @type child: subprocess.Popen
55 @ivar aborted_by_user: whether anyone has called L{abort}
56 @type aborted_by_user: bool
57 @ivar unmodified: whether the resource was not modified since the modification_time given at construction
58 @type unmodified: bool
60 __slots__
= ['url', 'tempfile', 'status', 'expected_size', 'downloaded',
62 'hint', '_final_total_size', 'aborted_by_user',
63 'modification_time', 'unmodified']
67 gobject
.SIGNAL_RUN_FIRST
, gobject
.TYPE_NONE
,
68 [object, object, object]),
71 def __init__(self
, url
, hint
= None, modification_time
= None, expected_size
= None):
72 """Create a new download object.
73 @param url: the resource to download
74 @param hint: object with which this download is associated (an optional hint for the GUI)
75 @param modification_time: string with HTTP date that indicates last modification time.
76 The resource will not be downloaded if it was not modified since that date.
77 @postcondition: L{status} == L{download_fetching}."""
78 gobject
.GObject
.__init
__(self
)
82 self
.aborted_by_user
= False
83 self
.modification_time
= modification_time
84 self
.unmodified
= False
87 self
.tempfile
= None # Stream for result
88 self
.downloaded
= None
90 self
.expected_size
= expected_size
# Final size (excluding skipped bytes)
91 self
._final
_total
_size
= None # Set when download is finished
93 self
.status
= download_fetching
94 self
.tempfile
= tempfile
.TemporaryFile(prefix
= 'injector-dl-data-')
95 self
.downloaded
= tasks
.Blocker('download %s' % self
.url
)
97 self
._done
_hid
= self
.connect('done', self
.__done
_cb
)
98 # Let the caller to read tempfile before closing the connection
99 # TODO eliminate such unreliable workflow
100 gobject
.idle_add(wget
.start
, self
.url
, self
.modification_time
,
101 self
.tempfile
.fileno(), self
)
103 def __done_cb(self
, *args
):
104 if self
._done
_hid
is not None:
105 self
.disconnect(self
._done
_hid
)
106 self
._done
_hid
= None
107 gobject
.idle_add(self
._done
_cb
, *args
)
109 def _done_cb(self
, sender
, status
, reason
, exception
):
111 self
._final
_total
_size
= 0
112 if self
.aborted_by_user
:
113 raise DownloadAborted()
115 debug("No need to download not modified %s", self
.url
)
116 self
.unmodified
= True
118 self
._final
_total
_size
= self
.get_bytes_downloaded_so_far()
119 # Check that the download has the correct size,
120 # if we know what it should be.
121 if self
.expected_size
is not None and \
122 self
.expected_size
!= self
._final
_total
_size
:
124 _('Downloaded archive has incorrect size.\n'
126 'Expected: %(expected_size)d bytes\n'
127 'Received: %(size)d bytes') % {
129 'expected_size': self
.expected_size
,
130 'size': self
._final
_total
_size
})
131 elif exception
is None:
132 raise DownloadError(_('Download %s failed: %s') % \
134 except Exception, error
:
135 __
, ex
, tb
= sys
.exc_info()
138 if exception
is None:
139 self
.status
= download_complete
140 self
.downloaded
.trigger()
142 self
.status
= download_failed
143 self
.downloaded
.trigger(exception
=exception
)
146 """Signal the current download to stop.
147 @postcondition: L{aborted_by_user}"""
148 self
.status
= download_failed
150 if self
.tempfile
is not None:
151 info(_("Aborting download of %s"), self
.url
)
152 # TODO: we currently just close the output file; the thread will end when it tries to
153 # write to it. We should try harder to stop the thread immediately (e.g. by closing its
154 # socket when known), although we can never cover all cases (e.g. a stuck DNS lookup).
155 # In any case, we don't wait for the child to exit before notifying tasks that are waiting
157 self
.aborted_by_user
= True
158 info(_("Killing download process %s"), self
.url
)
159 self
.__done
_cb
(None, None, None, None)
162 def get_current_fraction(self
):
163 """Returns the current fraction of this download that has been fetched (from 0 to 1),
164 or None if the total size isn't known.
165 @return: fraction downloaded
166 @rtype: int | None"""
167 if self
.tempfile
is None:
169 if self
.expected_size
is None:
170 return None # Unknown
171 current_size
= self
.get_bytes_downloaded_so_far()
172 return float(current_size
) / self
.expected_size
174 def get_bytes_downloaded_so_far(self
):
175 """Get the download progress. Will be zero if the download has not yet started.
177 if self
.status
is download_fetching
:
178 return os
.fstat(self
.tempfile
.fileno()).st_size
180 return self
._final
_total
_size
or 0
183 return _("<Download from %s>") % self
.url