Files
entropy/lib/entropy/fetchers.py
T

1222 lines
42 KiB
Python

# -*- coding: utf-8 -*-
"""
@author: Fabio Erculiani <lxnay@sabayon.org>
@contact: lxnay@sabayon.org
@copyright: Fabio Erculiani
@license: GPL-2
B{Entropy Transceivers Fetchers submodule}.
"""
import os
import errno
import sys
import time
try:
import httplib
except ImportError:
# python 3.x
import http.client as httplib
import hashlib
import socket
import pty
import subprocess
if sys.hexversion >= 0x3000000:
import urllib.request as urlmod
import urllib.error as urlmod_error
else:
import urllib2 as urlmod
import urllib2 as urlmod_error
from entropy.exceptions import InterruptError
from entropy.tools import print_traceback, \
convert_seconds_to_fancy_output, bytes_into_human, spliturl, \
add_proxy_opener, md5sum
from entropy.const import etpConst, const_isfileobj, const_debug_write
from entropy.output import TextInterface, darkblue, darkred, purple, blue, \
brown, darkgreen, red
from entropy.i18n import _
from entropy.misc import ParallelTask
from entropy.core.settings.base import SystemSettings
class UrlFetcher(TextInterface):
"""
Entropy single URL fetcher. It supports what Python's urllib2 supports,
plus resuming, proxies and custom user agents. No external tools
dependencies are required (including wget).
"""
# this dict must be kept in sync with
# the __supported_uris variable below
# until plugins support is implemented
_supported_differential_download = {
'file': False,
'http': False,
'https': False,
'ftp': False,
'ftps': False,
'rsync': True,
'ssh': True,
}
def __init__(self, url, path_to_save, checksum = True,
show_speed = True, resume = True,
abort_check_func = None, disallow_redirect = False,
thread_stop_func = None, speed_limit = None):
"""
Entropy URL downloader constructor.
@param url: download URL (do not URL-encode it!)
@type url: string
@param path_to_save: file path where to save downloaded data
@type path_to_save: string
@keyword checksum: return md5 hash instead of status code
@type checksum: bool
@keyword show_speed: show download speed
@type show_speed: bool
@keyword resume: enable resume support
@type resume: bool
@keyword abort_check_func: callback used to stop download, it has to
raise an exception that has to be caught by provider application.
This exception will be considered an "abort" request.
@type abort_check_func: callable
@keyword disallow_redirect: disallow automatic HTTP redirects
@type disallow_redirect: bool
@keyword thread_stop_func: callback used to stop download, it has to
raise an exception that has to be caught by provider application.
This exception will be considered a "stop" request.
@type thread_stop_func: callable
@keyword speed_limit: speed limit in kb/sec
@type speed_limit: int
"""
self.__supported_uris = {
'file': self._urllib_download,
'http': self._urllib_download,
'https': self._urllib_download,
'ftp': self._urllib_download,
'ftps': self._urllib_download,
'rsync': self._rsync_download,
'ssh': self._rsync_download,
}
self.__system_settings = SystemSettings()
if speed_limit == None:
speed_limit = \
self.__system_settings['repositories']['transfer_limit']
self.__timeout = \
self.__system_settings['repositories']['timeout']
self.__th_id = 0
self.__resume = resume
self.__url = url
self.__path_to_save = path_to_save
self.__checksum = checksum
self.__show_speed = show_speed
self.__abort_check_func = abort_check_func
self.__thread_stop_func = thread_stop_func
self.__disallow_redirect = disallow_redirect
self.__speedlimit = speed_limit # kbytes/sec
self._init_vars()
self.__init_urllib()
@staticmethod
def _get_url_protocol(url):
return url.split(":")[0]
def __init_urllib(self):
# this will be moved away soon anyway
self.__localfile = None
def _init_vars(self):
self.__use_md5_checksum = False
self.__md5_checksum = hashlib.new("md5")
self.__resumed = False
self.__buffersize = 8192
self.__status = None
self.__remotefile = None
self.__downloadedsize = 0
self.__average = 0
self.__remotesize = 0
self.__oldaverage = 0.0
self.__last_output_time = time.time()
# transfer status data
self.__startingposition = 0
self.__datatransfer = 0
self.__time_remaining = "(infinite)"
self.__time_remaining_secs = 0
self.__elapsed = 0.0
self.__updatestep = 0.2
self.__starttime = time.time()
self.__last_update_time = self.__starttime
self.__last_downloadedsize = 0
self.__existed_before = False
if os.path.lexists(self.__path_to_save):
self.__existed_before = True
def __setup_urllib_resume_support(self):
# resume support
if os.path.isfile(self.__path_to_save) and \
os.access(self.__path_to_save, os.W_OK) and self.__resume:
self.__urllib_open_local_file("ab")
self.__localfile.seek(0, os.SEEK_END)
self.__startingposition = int(self.__localfile.tell())
self.__last_downloadedsize = self.__startingposition
else:
self.__urllib_open_local_file("wb")
def __urllib_open_local_file(self, mode):
# if client uses this instance more than
# once, make sure we close previously opened
# files.
if const_isfileobj(self.__localfile):
try:
if hasattr(self.__localfile, 'flush'):
self.__localfile.flush()
self.__localfile.close()
except (IOError, OSError,):
pass
self.__localfile = open(self.__path_to_save, mode)
if mode.startswith("a"):
self.__resumed = True
else:
self.__resumed = False
def __encode_url(self, url):
if sys.hexversion >= 0x3000000:
import urllib.parse as encurl
else:
import urllib as encurl
url = os.path.join(os.path.dirname(url),
encurl.quote(os.path.basename(url)))
return url
def __prepare_return(self):
if self.__checksum:
if self.__use_md5_checksum:
self.__status = self.__md5_checksum.hexdigest()
else:
# for rsync, we don't have control on the data flow, so
# we cannot calculate the md5 on the way
self.__status = md5sum(self.__path_to_save)
return self.__status
self.__status = "-2"
return self.__status
def __fork_cmd(self, args, environ, update_output_callback):
def _line_reader(std_r):
read_buf = ""
try:
char = std_r.read(1)
while char:
if (char == "\r") and read_buf:
update_output_callback(read_buf)
read_buf = ""
elif (char != "\r"):
read_buf += char
char = std_r.read(1)
except IOError:
return
try:
pid, fd = pty.fork()
except OSError as err:
const_debug_write(__name__,
"__fork_cmd(%s): status: %s" % (args, err,))
# out of pty devices
return 1
if pid == 0:
proc = subprocess.Popen(args, env = environ)
rc = proc.wait()
os._exit(rc)
elif pid == -1:
raise SystemError("cannot forkpty()")
else:
dead = False
return_code = 1
std_r = os.fdopen(fd, "r")
while not dead:
try:
dead, return_code = os.waitpid(pid, os.WNOHANG)
except OSError as e:
if e.errno != errno.ECHILD:
raise
dead = True
# wait a bit
time.sleep(0.2)
_line_reader(std_r)
if self.__abort_check_func != None:
self.__abort_check_func()
if self.__thread_stop_func != None:
self.__thread_stop_func()
std_r.close()
return return_code
@staticmethod
def supports_differential_download(url):
"""
Return whether the current protocol handler for given URL supports
differential download. It's up to UrlFetcher users to implement
the logic to provide a previously downloaded file to "path_to_save"
location (argument passed to UrlFetcher constructor).
@param url: download URL
@type url: string
@return: differential download support status
@rtype: bool
"""
protocol = UrlFetcher._get_url_protocol(url)
return UrlFetcher._supported_differential_download.get(protocol, False)
def set_id(self, th_id):
"""
Set instance id (usually the thread identifier).
@param th_id: id to set
@type th_id: int
"""
self.__th_id = th_id
def download(self):
"""
Start downloading URL given at construction time.
@return: download status in string format. "-3" or "-4" mean error.
"-2" means "ok but unable to calculate md5 of file.
Otherwise returns md5 hash.
@rtype: string
@todo: improve return data
"""
protocol = UrlFetcher._get_url_protocol(self.__url)
downloader = self.__supported_uris.get(protocol)
const_debug_write(__name__,
"UrlFetcher.download(%s), save: %s, checksum: %s, resume: %s, "
"show speed: %s, abort func: %s, thread stop func: %s, "
"disallow redir: %s, speed limit: %s, timeout: %s" % (
self.__url, self.__path_to_save, self.__checksum,
self.__resume, self.__show_speed, self.__abort_check_func,
self.__thread_stop_func, self.__disallow_redirect,
self.__speedlimit, self.__timeout)
)
if downloader is None:
# return error, protocol not supported
self._update_speed()
self.__status = "-3"
return self.__status
self._init_vars()
status = downloader()
if self.__show_speed:
self.update()
return status
def _setup_rsync_args(self):
protocol = UrlFetcher._get_url_protocol(self.__url)
url = self.__url
_rsync_exec = "/usr/bin/rsync"
_default_ssh_port = 22
list_args, args = None, None
if protocol == "rsync":
args = (_rsync_exec, "--no-motd", "--compress", "--progress",
"--stats", "--inplace", "--timeout=%d" % (self.__timeout,))
if self.__speedlimit:
args += ("--bwlimit=%d" % (self.__speedlimit,),)
if not self.__resume:
args += ("--whole-file",)
else:
args += ("--partial",)
args += (url, self.__path_to_save,)
# args to rsync to get remote file size
list_args = (_rsync_exec, "--no-motd", "--list-only", url)
elif protocol == "ssh":
def _extract_ssh_port_url(url):
start_t = url.find("<")
if start_t == -1:
return _default_ssh_port, url
start_t += 1
end_t = start_t - 1
try:
for x in url[start_t:]:
end_t += 1
if x == ">":
break
except IndexError:
return _default_ssh_port, url
try:
port = int(url[start_t:end_t])
url = url[:start_t-1] + url[end_t+1:]
except ValueError:
port = _default_ssh_port
return port, url
# SUPPORTED URL form: ssh://username@host<PORT>:/path
# strip ssh:// from url
url = url.lstrip("ssh://")
# get port
port, url = _extract_ssh_port_url(url)
args = (_rsync_exec, "--no-motd", "--compress", "--progress",
"--stats", "--inplace", "--timeout=%d" % (self.__timeout,),
"-e", "ssh -p %s" % (port,))
if self.__speedlimit:
args += ("--bwlimit=%d" % (self.__speedlimit,),)
if not self.__resume:
args += ("--whole-file",)
else:
args += ("--partial",)
args += (url, self.__path_to_save,)
# args to rsync to get remote file size
list_args = (_rsync_exec, "--no-motd", "--list-only", "-e",
"ssh -p %s" % (port,), url)
return list_args, args
def _rsync_download(self):
"""
rsync based downloader. It uses rsync executable.
"""
list_args, args = self._setup_rsync_args()
# rsync executable environment
rsync_environ = {}
# setup proxy support
proxy_data = self.__system_settings['system']['proxy']
if proxy_data['rsync']:
rsync_environ['RSYNC_PROXY'] = proxy_data['rsync']
def rsync_stats_extractor(output_line):
const_debug_write(__name__,
"rsync_stats_extractor(%s): %s" % (self.__th_id, output_line,))
data = output_line.split()
if len(data) != 4:
# it's just garbage here
self._update_speed()
return
bytes_read, pct, speed_kbsec, eta = data
try:
bytes_read = int(bytes_read)
except ValueError:
bytes_read = 0
try:
average = int(pct.strip("%"))
except ValueError:
average = 0
# update progress info
# _rsync_commit
self.__downloadedsize = bytes_read
if average > 100:
average = 100
self.__average = average
self._update_speed()
if self.__show_speed:
self.handle_statistics(self.__th_id, self.__downloadedsize,
self.__remotesize, self.__average, self.__oldaverage,
self.__updatestep, self.__show_speed, self.__datatransfer,
self.__time_remaining, self.__time_remaining_secs
)
self.update()
self.__oldaverage = self.__average
def rsync_list_extractor(output_line):
data = output_line.split()
if len(data) == 5:
try:
# perms, size, date, time, file name
self.__remotesize = float(data[1])/1024
except ValueError:
pass
const_debug_write(__name__,
"spawning rsync fetch(%s): %s, %s, %s" % (
self.__th_id, list_args, rsync_environ, rsync_list_extractor,))
sts = self.__fork_cmd(list_args, rsync_environ, rsync_list_extractor)
const_debug_write(__name__,
"spawned rsync fetch(%s): status: %s" % (self.__th_id, sts,))
if sts != 0:
self.__rsync_close(True)
self.__status = "-3"
return self.__status
const_debug_write(__name__,
"spawning rsync fetch(%s): %s, %s, %s" % (
self.__th_id, args, rsync_environ, rsync_stats_extractor,))
sts = self.__fork_cmd(args, rsync_environ, rsync_stats_extractor)
const_debug_write(__name__,
"spawned rsync fetch(%s): status: %s" % (self.__th_id, sts,))
if sts != 0:
self.__rsync_close(True)
self.__status = "-3"
return self.__status
# kill thread
self.__rsync_close(False)
return self.__prepare_return()
def __rsync_close(self, errored):
if (not self.__existed_before) and errored:
try:
os.remove(self.__path_to_save)
except OSError:
pass
def _setup_urllib_proxy(self):
"""
Setup urllib proxy data
"""
mydict = {}
proxy_data = self.__system_settings['system']['proxy']
if proxy_data['ftp']:
mydict['ftp'] = proxy_data['ftp']
if proxy_data['http']:
mydict['http'] = proxy_data['http']
if mydict:
mydict['username'] = proxy_data['username']
mydict['password'] = proxy_data['password']
add_proxy_opener(urlmod, mydict)
else:
# unset
urlmod._opener = None
def _urllib_download(self):
"""
urrlib2 based downloader. This is the default for HTTP and FTP urls.
"""
self._setup_urllib_proxy()
self.__setup_urllib_resume_support()
# we're going to feed the md5 digestor on the way.
self.__use_md5_checksum = True
url = self.__encode_url(self.__url)
url_protocol = UrlFetcher._get_url_protocol(self.__url)
uname = os.uname()
user_agent = "Entropy/%s (compatible; %s; %s: %s %s %s)" % (
etpConst['entropyversion'],
"Entropy",
os.path.basename(url),
uname[0],
uname[4],
uname[2],
)
if url_protocol in ("http", "https"):
headers = {'User-Agent': user_agent,}
req = urlmod.Request(url, headers = headers)
else:
req = url
u_agent_error = False
do_return = False
while True:
# get file size if available
try:
self.__remotefile = urlmod.urlopen(req, None, self.__timeout)
except KeyboardInterrupt:
self.__urllib_close(False)
raise
except httplib.InvalidURL:
# malformed url!
self.__urllib_close(True)
self.__status = "-3"
do_return = True
except urlmod_error.HTTPError as e:
if (e.code == 405) and not u_agent_error:
# server doesn't like our user agent
req = url
u_agent_error = True
continue
self.__urllib_close(True)
self.__status = "-3"
do_return = True
except urlmod_error.URLError as err: # timeout error
self.__urllib_close(True)
self.__status = "-3"
do_return = True
except httplib.BadStatusLine:
# obviously, something to cope with
self.__urllib_close(True)
self.__status = "-3"
do_return = True
except socket.timeout:
# arghv!!
self.__urllib_close(True)
self.__status = "-3"
do_return = True
except ValueError: # malformed, unsupported URL? raised by urllib
self.__urllib_close(True)
self.__status = "-3"
do_return = True
except Exception:
print_traceback()
raise
break
if do_return:
return self.__status
try:
self.__remotesize = int(self.__remotefile.headers.get(
"content-length"))
self.__remotefile.close()
except KeyboardInterrupt:
self.__urllib_close(False)
raise
except Exception:
pass
# handle user stupidity
try:
request = url
if ((self.__startingposition > 0) and (self.__remotesize > 0)) \
and (self.__startingposition < self.__remotesize):
try:
request = urlmod.Request(
url,
headers = {
"Range" : "bytes=" + \
str(self.__startingposition) + "-" + \
str(self.__remotesize)
}
)
except KeyboardInterrupt:
self.__urllib_close(False)
raise
except:
pass
elif self.__startingposition == self.__remotesize:
# all fine then!
self.__urllib_close(False)
return self.__prepare_return()
elif (self.__startingposition > self.__remotesize) and \
self.__resumed:
# there is something wrong
# downloaded more than the advertised size
# the HTTP server is broken or something else happened
# locally and file cannot be trusted (resumed)
self.__urllib_open_local_file("wb")
self.__remotefile = urlmod.urlopen(request, None, self.__timeout)
except KeyboardInterrupt:
self.__urllib_close(False)
raise
except:
self.__urllib_close(True)
self.__status = "-3"
return self.__status
if self.__remotesize > 0:
self.__remotesize = float(int(self.__remotesize))/1024
if url_protocol not in ("file", "ftp", "ftps"):
if self.__disallow_redirect and \
(url != self.__remotefile.geturl()):
self.__urllib_close(True)
self.__status = "-3"
return self.__status
while True:
try:
rsx = self.__remotefile.read(self.__buffersize)
if not rsx:
break
if self.__abort_check_func != None:
self.__abort_check_func()
if self.__thread_stop_func != None:
self.__thread_stop_func()
except KeyboardInterrupt:
self.__urllib_close(False)
raise
except socket.timeout:
self.__urllib_close(False)
self.__status = "-4"
return self.__status
except:
# python 2.4 timeouts go here
self.__urllib_close(True)
self.__status = "-3"
return self.__status
self.__urllib_commit(rsx)
if self.__show_speed:
self.handle_statistics(self.__th_id, self.__downloadedsize,
self.__remotesize, self.__average, self.__oldaverage,
self.__updatestep, self.__show_speed, self.__datatransfer,
self.__time_remaining, self.__time_remaining_secs
)
self.update()
self.__oldaverage = self.__average
if self.__speedlimit:
while self.__datatransfer > self.__speedlimit*1024:
time.sleep(0.1)
self._update_speed()
if self.__show_speed:
self.update()
self.__oldaverage = self.__average
# kill thread
self.__urllib_close(False)
return self.__prepare_return()
def __urllib_commit(self, mybuffer):
# writing file buffer
self.__localfile.write(mybuffer)
self.__md5_checksum.update(mybuffer)
# update progress info
self.__downloadedsize = self.__localfile.tell()
kbytecount = float(self.__downloadedsize)/1024
# avoid race condition with test and eval not being atomic
# this will always work
try:
average = int((kbytecount/self.__remotesize)*100)
except ZeroDivisionError:
average = 0
if average > 100:
average = 100
self.__average = average
self._update_speed()
def __urllib_close(self, errored):
self._update_speed()
try:
if const_isfileobj(self.__localfile):
if hasattr(self.__localfile, 'flush'):
self.__localfile.flush()
self.__localfile.close()
except IOError:
pass
if (not self.__existed_before) and errored:
try:
os.remove(self.__path_to_save)
except OSError:
pass
if self.__remotefile is not None:
try:
self.__remotefile.close()
except socket.error:
pass
def _update_speed(self):
cur_time = time.time()
self.__elapsed = cur_time - self.__starttime
last_elapsed = cur_time - self.__last_update_time
# we have the diff size
x_delta = self.__downloadedsize - self.__startingposition
x_delta_now = self.__downloadedsize - self.__last_downloadedsize
el_factor = self.__elapsed
if self.__elapsed > 1:
el_factor = 1
if (last_elapsed > 0) and (self.__elapsed > 0):
self.__datatransfer = 0.5 * self.__datatransfer + \
0.5 * (el_factor * x_delta / self.__elapsed + \
(1-el_factor) * x_delta_now / last_elapsed)
else:
self.__datatransfer = 0.0
self.__last_update_time = cur_time
self.__last_downloadedsize = self.__downloadedsize
if self.__datatransfer < 0:
self.__datatransfer = 0.0
rounded_remote = int(round(self.__remotesize*1024, 0))
rounded_downloaded = int(round(self.__downloadedsize, 0))
x_delta = rounded_remote - rounded_downloaded
if self.__datatransfer > 0:
tx_round = round(x_delta/self.__datatransfer, 0)
self.__time_remaining_secs = int(tx_round)
if self.__time_remaining_secs < 0:
self.__time_remaining = "(%s)" % (_("infinite"),)
else:
self.__time_remaining = \
convert_seconds_to_fancy_output(self.__time_remaining_secs)
def get_transfer_rate(self):
"""
Return transfer rate, in kb/sec.
@return: transfer rate
@rtype: int
"""
return self.__datatransfer
def get_average(self):
"""
Get current download percentage.
@return: download percentage
@rtype: float
"""
return self.__average
def get_seconds_remaining(self):
"""
Return remaining seconds to download completion.
@return: remaining download seconds
@rtype: int
"""
return self.__time_remaining_secs
def is_resumed(self):
"""
Return whether given download has been resumed.
"""
return self.__resumed
def handle_statistics(self, th_id, downloaded_size, total_size,
average, old_average, update_step, show_speed, data_transfer,
time_remaining, time_remaining_secs):
"""
Reimplement this callback to gather information about data currently
downloaded.
@param th_id: instance identifier
@type th_id: int
@param downloaded_size: size downloaded up to now, in bytes
@type downloaded_size: int
@param total_size: total download size, in bytes
@type total_size: int
@param average: percentage of file downloaded up to now
@type average: float
@param old_average: old percentage of file downloaded
@type: float
@param update_step: currently configured update average delta
@type update_step: int
@param show_speed: if download speed should be shown for given instance
@type show_speed: bool
@param data_transfer: current data transfer, in kb/sec
@type data_transfer: int
@param time_remaining: currently hypothesized remaining download time,
in string format (showing hours, minutes, seconds).
@type time_remaining: string
@param time_remaining_secs: currently hypothesized remaining download time,
in seconds.
@type time_remaining_secs: int
"""
return
def _push_progress_to_output(self):
mytxt = _("[F]")
eta_txt = _("ETA")
sec_txt = _("sec") # as in XX kb/sec
current_txt = darkred(" %s: " % (mytxt,)) + \
darkgreen(str(round(float(self.__downloadedsize)/1024, 1))) + "/" \
+ red(str(round(self.__remotesize, 1))) + " kB"
# create progress bar
barsize = 10
bartext = "["
curbarsize = 1
averagesize = (self.__average*barsize)/100
while averagesize > 0:
curbarsize += 1
bartext += "="
averagesize -= 1
bartext += ">"
diffbarsize = barsize - curbarsize
while diffbarsize > 0:
bartext += " "
diffbarsize -= 1
if self.__show_speed:
bartext += "] => %s" % (bytes_into_human(self.__datatransfer),)
bartext += "/%s : %s: %s" % (sec_txt, eta_txt,
self.__time_remaining,)
else:
bartext += "]"
average = str(self.__average)
if len(average) < 2:
average = " "+average
current_txt += " <-> "+average+"% "+bartext
TextInterface.output(self, current_txt, back = True)
def output(self):
""" @deprecated, remove after 2010 """
return self.update()
def update(self):
"""
Main fetch progress callback. You can reimplement this to refresh
your output devices.
"""
update_time_delta = 0.5
cur_t = time.time()
if cur_t > (self.__last_output_time + update_time_delta):
self.__last_output_time = cur_t
self._push_progress_to_output()
class MultipleUrlFetcher(TextInterface):
def __init__(self, url_path_list, checksum = True,
show_speed = True, resume = True,
abort_check_func = None, disallow_redirect = False,
url_fetcher_class = None):
"""
@param url_path_list: list of tuples composed by url and
path to save, for eg. [(url,path_to_save,),...]
@type url_path_list: list
@keyword checksum: return md5 hash instead of status code
@type checksum: bool
@keyword show_speed: show download speed
@type show_speed: bool
@keyword resume: enable resume support
@type resume: bool
@keyword abort_check_func: callback used to stop download, it has to
raise an exception that has to be caught by provider application.
This exception will be considered an "abort" request.
@type abort_check_func: callable
@keyword disallow_redirect: disallow automatic HTTP redirects
@type disallow_redirect: bool
@keyword thread_stop_func: callback used to stop download, it has to
raise an exception that has to be caught by provider application.
This exception will be considered a "stop" request.
@type thread_stop_func: callable
@param url_fetcher_class: UrlFetcher based class to use
@type url_fetcher_class: subclass of UrlFetcher
"""
self.__system_settings = SystemSettings()
self.__url_path_list = url_path_list
self.__resume = resume
self.__checksum = checksum
self.__show_speed = show_speed
self.__abort_check_func = abort_check_func
self.__disallow_redirect = disallow_redirect
# important to have a declaration here
self.__data_transfer = 0
self.__average = 0
self.__old_average = 0
self.__time_remaining_sec = 0
self.__url_fetcher = url_fetcher_class
if self.__url_fetcher == None:
self.__url_fetcher = UrlFetcher
def __handle_threads_stop(self):
if self.__stop_threads:
raise InterruptError("interrupted")
def _init_vars(self):
self.__progress_data = {}
self.__thread_pool = {}
self.__download_statuses = {}
self.__show_progress = False
self.__stop_threads = False
self.__first_refreshes = 50
self.__data_transfer = 0
self.__average = 0
self.__old_average = 0
self.__time_remaining_sec = 0
self.__progress_update_t = time.time()
self.__startup_time = time.time()
@staticmethod
def supports_differential_download(url):
"""
Return whether the current protocol handler for given URL supports
differential download. It's up to UrlFetcher users to implement
the logic to provide a previously downloaded file to "path_to_save"
location (argument passed to UrlFetcher constructor).
@param url: download URL
@type url: string
@return: differential download support status
@rtype: bool
"""
return UrlFetcher.supports_differential_download(url)
def download(self):
"""
Start downloading URL given at construction time.
@return: dict containing:
download status in string format. "-3" or "-4" mean error.
"-2" means "ok but unable to calculate md5 of file.
Key is UrlFetcher.get_id() and value is UrlFetcher.download() return
code.
@rtype: dict
"""
self._init_vars()
th_id = 0
speed_limit = 0
dsl = self.__system_settings['repositories']['transfer_limit']
if isinstance(dsl, int) and self.__url_path_list:
speed_limit = dsl/len(self.__url_path_list)
class MyFetcher(self.__url_fetcher):
def __init__(self, klass, multiple, *args, **kwargs):
klass.__init__(self, *args, **kwargs)
self.__multiple_fetcher = multiple
def update(self):
return self.__multiple_fetcher.update()
def _push_progress_to_output(self, *args):
return
def handle_statistics(self, *args, **kwargs):
return self.__multiple_fetcher.handle_statistics(*args,
**kwargs)
for url, path_to_save in self.__url_path_list:
th_id += 1
downloader = MyFetcher(self.__url_fetcher, self, url, path_to_save,
checksum = self.__checksum, show_speed = self.__show_speed,
resume = self.__resume,
abort_check_func = self.__abort_check_func,
disallow_redirect = self.__disallow_redirect,
thread_stop_func = self.__handle_threads_stop,
speed_limit = speed_limit
)
downloader.set_id(th_id)
def do_download(ds, th_id, downloader):
ds[th_id] = downloader.download()
t = ParallelTask(do_download, self.__download_statuses, th_id,
downloader)
t.daemon = True
self.__thread_pool[th_id] = t
t.start()
self._push_progress_to_output(force = True)
self.__show_download_files_info()
self.__show_progress = True
while len(self.__url_path_list) != len(self.__download_statuses):
try:
time.sleep(0.5)
except (SystemExit, KeyboardInterrupt,):
self.__stop_threads = True
raise
return self.__download_statuses
def get_transfer_rate(self):
"""
Return transfer rate, in kb/sec.
@return: transfer rate
@rtype: int
"""
return self.__data_transfer
def get_average(self):
"""
Get current download percentage.
@return: download percentage
@rtype: float
"""
return self.__average
def get_seconds_remaining(self):
"""
Return remaining seconds to download completion.
@return: remaining download seconds
@rtype: int
"""
return self.__time_remaining_sec
def __show_download_files_info(self):
count = 0
pl = self.__url_path_list[:]
TextInterface.output(self,
"%s: %s %s" % (
darkblue(_("Aggregated download")),
darkred(str(len(pl))),
darkblue(_("items")),
),
importance = 0,
level = "info",
header = purple(" ## ")
)
for url, save_path in pl:
count += 1
fname = os.path.basename(url)
uri = spliturl(url)[1]
TextInterface.output(self,
"[%s] %s => %s" % (
darkblue(str(count)),
darkgreen(uri),
blue(fname),
),
importance = 0,
level = "info",
header = brown(" # ")
)
def handle_statistics(self, th_id, downloaded_size, total_size,
average, old_average, update_step, show_speed, data_transfer,
time_remaining, time_remaining_secs):
"""
Reimplement this callback to gather information about data currently
downloaded.
@param th_id: instance identifier
@type th_id: int
@param downloaded_size: size downloaded up to now, in bytes
@type downloaded_size: int
@param total_size: total download size, in bytes
@type total_size: int
@param average: percentage of file downloaded up to now
@type average: float
@param old_average: old percentage of file downloaded
@type: float
@param update_step: currently configured update average delta
@type update_step: int
@param show_speed: if download speed should be shown for given instance
@type show_speed: bool
@param data_transfer: current data transfer, in kb/sec
@type data_transfer: int
@param time_remaining: currently hypothesized remaining download time,
in string format (showing hours, minutes, seconds).
@type time_remaining: string
@param time_remaining_secs: currently hypothesized remaining download time,
in seconds.
@type time_remaining_secs: int
"""
data = {
'th_id': th_id,
'downloaded_size': downloaded_size,
'total_size': total_size,
'average': average,
'old_average': old_average,
'update_step': update_step,
'show_speed': show_speed,
'data_transfer': data_transfer,
'time_remaining': time_remaining,
'time_remaining_secs': time_remaining_secs,
}
self.__progress_data[th_id] = data
def _push_progress_to_output(self, force = False):
downloaded_size = 0
total_size = 0
time_remaining = 0
update_step = 0
pd = self.__progress_data.copy()
pdlen = len(pd)
# calculation
for th_id in sorted(pd):
data = pd.get(th_id)
downloaded_size += data.get('downloaded_size', 0)
total_size += data.get('total_size', 0)
# data_transfer from Python threading bullshit is not reliable
# with multiple threads and causes inaccurate informations to be
# printed
# data_transfer += data.get('data_transfer', 0)
tr = data.get('time_remaining_secs', 0)
if tr > 0:
time_remaining += tr
update_step += data.get('update_step', 0)
elapsed_t = time.time() - self.__startup_time
if elapsed_t < 0.1:
elapsed_t = 0.1
data_transfer = downloaded_size / elapsed_t
self.__data_transfer = data_transfer
average = 100
# total_size is in kbytes
# downloaded_size is in bytes
if total_size > 0:
average = int(float(downloaded_size/1024)/total_size * 100)
self.__average = average
if pdlen > 0:
update_step = update_step/pdlen
else:
update_step = 0
time_remaining = convert_seconds_to_fancy_output(time_remaining)
self.__time_remaining_sec = time_remaining
update_time_delta = 0.5
cur_t = time.time()
if ((cur_t > (self.__progress_update_t + update_time_delta)) \
or force or (self.__first_refreshes > 0)) and self.__show_progress:
self.__first_refreshes -= 1
self.__progress_update_t = cur_t
eta_txt = _("ETA")
sec_txt = _("sec") # as in XX kb/sec
down_size_txt = str(round(float(downloaded_size)/1024, 1))
total_size_txt = str(round(total_size, 1))
current_txt = darkgreen(down_size_txt) + "/" + red(total_size_txt)
current_txt += " kB"
# create progress bar
barsize = 10
bartext = "["
curbarsize = 1
averagesize = (average*barsize)/100
while averagesize > 0:
curbarsize += 1
bartext += "="
averagesize -= 1
bartext += ">"
diffbarsize = barsize-curbarsize
while diffbarsize > 0:
bartext += " "
diffbarsize -= 1
if self.__show_speed:
bartext += "] => %s" % (bytes_into_human(data_transfer),)
bartext += "/%s : %s: %s" % (sec_txt, eta_txt, time_remaining,)
else:
bartext += "]"
myavg = str(average)
if len(myavg) < 2:
myavg = " "+myavg
current_txt += " <-> "+myavg+"% "+bartext+" "
TextInterface.output(self, current_txt, back = True)
self.__old_average = average
def update(self):
"""
Main fetch progress callback. You can reimplement this to refresh
your output devices.
"""
return self._push_progress_to_output()