Package zeroinstall :: Package injector :: Module download
[frames] | no frames]

Source Code for Module zeroinstall.injector.download

  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  # Copyright (C) 2009, Thomas Leonard 
 10  # See the README file for details, or visit http://0install.net. 
 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"  # 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" 
 22   
 23  RESULT_OK = 0 
 24  RESULT_FAILED = 1 
 25  RESULT_NOT_MODIFIED = 2 
 26  RESULT_REDIRECT = 3 
 27   
28 -class DownloadError(SafeException):
29 """Download process failed.""" 30 pass
31
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"))
36
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 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) # XXX 72 self.url = url 73 self.hint = hint 74 self.aborted_by_user = False # replace with _aborted? 75 self.modification_time = modification_time 76 self.unmodified = False 77 78 self.tempfile = None # Stream for result 79 self.downloaded = None 80 self.mirror = None 81 82 self.expected_size = expected_size # Final size (excluding skipped bytes) 83 self._final_total_size = None # Set when download is finished 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
93 - def _finish(self, status):
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 # Check that the download has the correct size, if we know what it should be. 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
127 - def abort(self):
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 # TODO: we currently just close the output file; the thread will end when it tries to 135 # write to it. We should try harder to stop the thread immediately (e.g. by closing its 136 # socket when known), although we can never cover all cases (e.g. a stuck DNS lookup). 137 # In any case, we don't wait for the child to exit before notifying tasks that are waiting 138 # on us. 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
146 - def get_current_fraction(self):
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 # Unknown 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
169 - def get_next_mirror_url(self):
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
176 - def __str__(self):
177 return _("<Download from %s>") % self.url
178