mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Move the linux installer download script into the calibre source tree
This commit is contained in:
parent
ec2567b580
commit
ac35a5bddc
682
setup/linux-installer.py
Normal file
682
setup/linux-installer.py
Normal file
@ -0,0 +1,682 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import sys, os, shutil, subprocess, re, platform, time, signal, textwrap, tempfile, hashlib, errno
|
||||
import ssl, socket
|
||||
from contextlib import closing
|
||||
|
||||
is64bit = platform.architecture()[0] == '64bit'
|
||||
url = 'http://status.calibre-ebook.com/dist/linux'+('64' if is64bit else '32')
|
||||
signature_url = 'http://calibre-ebook.com/downloads/signatures/%s.sha512'
|
||||
url = os.environ.get('CALIBRE_INSTALLER_LOCAL_URL', url)
|
||||
py3 = sys.version_info[0] > 2
|
||||
enc = getattr(sys.stdout, 'encoding', 'UTF-8')
|
||||
calibre_version = signature = None
|
||||
urllib = __import__('urllib.request' if py3 else 'urllib', fromlist=1)
|
||||
if py3:
|
||||
unicode = str
|
||||
raw_input = input
|
||||
from urllib.parse import urlparse
|
||||
import http.client as httplib
|
||||
else:
|
||||
from urlparse import urlparse
|
||||
import httplib
|
||||
|
||||
class TerminalController: # {{{
|
||||
BOL = '' #: Move the cursor to the beginning of the line
|
||||
UP = '' #: Move the cursor up one line
|
||||
DOWN = '' #: Move the cursor down one line
|
||||
LEFT = '' #: Move the cursor left one char
|
||||
RIGHT = '' #: Move the cursor right one char
|
||||
|
||||
# Deletion:
|
||||
CLEAR_SCREEN = '' #: Clear the screen and move to home position
|
||||
CLEAR_EOL = '' #: Clear to the end of the line.
|
||||
CLEAR_BOL = '' #: Clear to the beginning of the line.
|
||||
CLEAR_EOS = '' #: Clear to the end of the screen
|
||||
|
||||
# Output modes:
|
||||
BOLD = '' #: Turn on bold mode
|
||||
BLINK = '' #: Turn on blink mode
|
||||
DIM = '' #: Turn on half-bright mode
|
||||
REVERSE = '' #: Turn on reverse-video mode
|
||||
NORMAL = '' #: Turn off all modes
|
||||
|
||||
# Cursor display:
|
||||
HIDE_CURSOR = '' #: Make the cursor invisible
|
||||
SHOW_CURSOR = '' #: Make the cursor visible
|
||||
|
||||
# Terminal size:
|
||||
COLS = None #: Width of the terminal (None for unknown)
|
||||
LINES = None #: Height of the terminal (None for unknown)
|
||||
|
||||
# Foreground colors:
|
||||
BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = ''
|
||||
|
||||
# Background colors:
|
||||
BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = ''
|
||||
BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = ''
|
||||
|
||||
_STRING_CAPABILITIES = """
|
||||
BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1
|
||||
CLEAR_SCREEN=clear CLEAR_EOL=el CLEAR_BOL=el1 CLEAR_EOS=ed BOLD=bold
|
||||
BLINK=blink DIM=dim REVERSE=rev UNDERLINE=smul NORMAL=sgr0
|
||||
HIDE_CURSOR=cinvis SHOW_CURSOR=cnorm""".split()
|
||||
_COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split()
|
||||
_ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split()
|
||||
|
||||
def __init__(self, term_stream=sys.stdout):
|
||||
# Curses isn't available on all platforms
|
||||
try:
|
||||
import curses
|
||||
except:
|
||||
return
|
||||
|
||||
# If the stream isn't a tty, then assume it has no capabilities.
|
||||
if not hasattr(term_stream, 'isatty') or not term_stream.isatty():
|
||||
return
|
||||
|
||||
# Check the terminal type. If we fail, then assume that the
|
||||
# terminal has no capabilities.
|
||||
try:
|
||||
curses.setupterm()
|
||||
except:
|
||||
return
|
||||
|
||||
# Look up numeric capabilities.
|
||||
self.COLS = curses.tigetnum('cols')
|
||||
self.LINES = curses.tigetnum('lines')
|
||||
|
||||
# Look up string capabilities.
|
||||
for capability in self._STRING_CAPABILITIES:
|
||||
(attrib, cap_name) = capability.split('=')
|
||||
setattr(self, attrib, self._escape_code(self._tigetstr(cap_name)))
|
||||
|
||||
# Colors
|
||||
set_fg = self._tigetstr('setf')
|
||||
if set_fg:
|
||||
if not isinstance(set_fg, bytes):
|
||||
set_fg = set_fg.encode('utf-8')
|
||||
for i,color in zip(range(len(self._COLORS)), self._COLORS):
|
||||
setattr(self, color,
|
||||
self._escape_code(curses.tparm((set_fg), i)))
|
||||
set_fg_ansi = self._tigetstr('setaf')
|
||||
if set_fg_ansi:
|
||||
if not isinstance(set_fg_ansi, bytes):
|
||||
set_fg_ansi = set_fg_ansi.encode('utf-8')
|
||||
for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
|
||||
setattr(self, color,
|
||||
self._escape_code(curses.tparm((set_fg_ansi),
|
||||
i)))
|
||||
set_bg = self._tigetstr('setb')
|
||||
if set_bg:
|
||||
if not isinstance(set_bg, bytes):
|
||||
set_bg = set_bg.encode('utf-8')
|
||||
for i,color in zip(range(len(self._COLORS)), self._COLORS):
|
||||
setattr(self, 'BG_'+color,
|
||||
self._escape_code(curses.tparm((set_bg), i)))
|
||||
set_bg_ansi = self._tigetstr('setab')
|
||||
if set_bg_ansi:
|
||||
if not isinstance(set_bg_ansi, bytes):
|
||||
set_bg_ansi = set_bg_ansi.encode('utf-8')
|
||||
for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
|
||||
setattr(self, 'BG_'+color,
|
||||
self._escape_code(curses.tparm((set_bg_ansi),
|
||||
i)))
|
||||
|
||||
def _escape_code(self, raw):
|
||||
if not raw:
|
||||
raw = ''
|
||||
if not isinstance(raw, unicode):
|
||||
raw = raw.decode('ascii')
|
||||
return raw
|
||||
|
||||
def _tigetstr(self, cap_name):
|
||||
# String capabilities can include "delays" of the form "$<2>".
|
||||
# For any modern terminal, we should be able to just ignore
|
||||
# these, so strip them out.
|
||||
import curses
|
||||
if isinstance(cap_name, bytes):
|
||||
cap_name = cap_name.decode('utf-8')
|
||||
cap = self._escape_code(curses.tigetstr(cap_name))
|
||||
return re.sub(r'\$<\d+>[/*]?', b'', cap)
|
||||
|
||||
def render(self, template):
|
||||
return re.sub(r'\$\$|\${\w+}', self._render_sub, template)
|
||||
|
||||
def _render_sub(self, match):
|
||||
s = match.group()
|
||||
if s == '$$':
|
||||
return s
|
||||
else:
|
||||
return getattr(self, s[2:-1])
|
||||
|
||||
class ProgressBar:
|
||||
BAR = '%3d%% ${GREEN}[${BOLD}%s%s${NORMAL}${GREEN}]${NORMAL}\n'
|
||||
HEADER = '${BOLD}${CYAN}%s${NORMAL}\n\n'
|
||||
|
||||
def __init__(self, term, header):
|
||||
self.term = term
|
||||
if not (self.term.CLEAR_EOL and self.term.UP and self.term.BOL):
|
||||
raise ValueError("Terminal isn't capable enough -- you "
|
||||
"should use a simpler progress display.")
|
||||
self.width = self.term.COLS or 75
|
||||
self.bar = term.render(self.BAR)
|
||||
self.header = self.term.render(self.HEADER % header.center(self.width))
|
||||
self.cleared = 1 # : true if we haven't drawn the bar yet.
|
||||
|
||||
def update(self, percent, message=''):
|
||||
out = (sys.stdout.buffer if py3 else sys.stdout)
|
||||
if self.cleared:
|
||||
out.write(self.header.encode(enc))
|
||||
self.cleared = 0
|
||||
n = int((self.width-10)*percent)
|
||||
msg = message.center(self.width)
|
||||
msg = (self.term.BOL + self.term.UP + self.term.CLEAR_EOL +
|
||||
(self.bar % (100*percent, '='*n, '-'*(self.width-10-n))) +
|
||||
self.term.CLEAR_EOL + msg).encode(enc)
|
||||
out.write(msg)
|
||||
out.flush()
|
||||
|
||||
def clear(self):
|
||||
out = (sys.stdout.buffer if py3 else sys.stdout)
|
||||
if not self.cleared:
|
||||
out.write((self.term.BOL + self.term.CLEAR_EOL +
|
||||
self.term.UP + self.term.CLEAR_EOL +
|
||||
self.term.UP + self.term.CLEAR_EOL).encode(enc))
|
||||
self.cleared = 1
|
||||
out.flush()
|
||||
# }}}
|
||||
|
||||
def prints(*args, **kwargs): # {{{
|
||||
f = kwargs.get('file', sys.stdout.buffer if py3 else sys.stdout)
|
||||
end = kwargs.get('end', b'\n')
|
||||
enc = getattr(f, 'encoding', 'utf-8')
|
||||
|
||||
if isinstance(end, unicode):
|
||||
end = end.encode(enc)
|
||||
for x in args:
|
||||
if isinstance(x, unicode):
|
||||
x = x.encode(enc)
|
||||
f.write(x)
|
||||
f.write(b' ')
|
||||
f.write(end)
|
||||
if py3 and f is sys.stdout.buffer:
|
||||
f.flush()
|
||||
# }}}
|
||||
|
||||
class Reporter: # {{{
|
||||
|
||||
def __init__(self, fname):
|
||||
try:
|
||||
self.pb = ProgressBar(TerminalController(), 'Downloading '+fname)
|
||||
except ValueError:
|
||||
prints('Downloading', fname)
|
||||
self.pb = None
|
||||
|
||||
def __call__(self, blocks, block_size, total_size):
|
||||
percent = (blocks*block_size)/float(total_size)
|
||||
if self.pb is None:
|
||||
prints('Downloaded {0:%}'.format(percent))
|
||||
else:
|
||||
try:
|
||||
self.pb.update(percent)
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
# }}}
|
||||
|
||||
# Downloading {{{
|
||||
|
||||
def clean_cache(cache, fname):
|
||||
for x in os.listdir(cache):
|
||||
if fname not in x:
|
||||
os.remove(os.path.join(cache, x))
|
||||
|
||||
def check_signature(dest, signature):
|
||||
if not os.path.exists(dest):
|
||||
return False
|
||||
m = hashlib.sha512()
|
||||
with open(dest, 'rb') as f:
|
||||
raw = True
|
||||
while raw:
|
||||
raw = f.read(1024*1024)
|
||||
m.update(raw)
|
||||
return m.hexdigest().encode('ascii') == signature
|
||||
|
||||
class URLOpener(urllib.FancyURLopener):
|
||||
|
||||
def http_error_206(self, url, fp, errcode, errmsg, headers, data=None):
|
||||
''' 206 means partial content, ignore it '''
|
||||
pass
|
||||
|
||||
def do_download(dest):
|
||||
prints('Will download and install', os.path.basename(dest))
|
||||
reporter = Reporter(os.path.basename(dest))
|
||||
offset = 0
|
||||
urlopener = URLOpener()
|
||||
if os.path.exists(dest):
|
||||
offset = os.path.getsize(dest)
|
||||
|
||||
# Get content length and check if range is supported
|
||||
rq = urllib.urlopen(url)
|
||||
headers = rq.info()
|
||||
size = int(headers['content-length'])
|
||||
accepts_ranges = headers.get('accept-ranges', None) == 'bytes'
|
||||
mode = 'wb'
|
||||
if accepts_ranges and offset > 0:
|
||||
rurl = rq.geturl()
|
||||
mode = 'ab'
|
||||
rq.close()
|
||||
urlopener.addheader('Range', 'bytes=%s-'%offset)
|
||||
rq = urlopener.open(rurl)
|
||||
with open(dest, mode) as f:
|
||||
while f.tell() < size:
|
||||
raw = rq.read(8192)
|
||||
if not raw:
|
||||
break
|
||||
f.write(raw)
|
||||
reporter(f.tell(), 1, size)
|
||||
rq.close()
|
||||
if os.path.getsize(dest) < size:
|
||||
print ('Download failed, try again later')
|
||||
raise SystemExit(1)
|
||||
prints('Downloaded %s bytes'%os.path.getsize(dest))
|
||||
|
||||
def download_tarball():
|
||||
fname = 'calibre-%s-i686.tar.bz2'%calibre_version
|
||||
if is64bit:
|
||||
fname = fname.replace('i686', 'x86_64')
|
||||
tdir = tempfile.gettempdir()
|
||||
cache = os.path.join(tdir, 'calibre-installer-cache')
|
||||
if not os.path.exists(cache):
|
||||
os.makedirs(cache)
|
||||
clean_cache(cache, fname)
|
||||
dest = os.path.join(cache, fname)
|
||||
if check_signature(dest, signature):
|
||||
print ('Using previously downloaded', fname)
|
||||
return dest
|
||||
cached_sigf = dest +'.signature'
|
||||
cached_sig = None
|
||||
if os.path.exists(cached_sigf):
|
||||
with open(cached_sigf, 'rb') as sigf:
|
||||
cached_sig = sigf.read()
|
||||
if cached_sig != signature and os.path.exists(dest):
|
||||
os.remove(dest)
|
||||
try:
|
||||
with open(cached_sigf, 'wb') as f:
|
||||
f.write(signature)
|
||||
except IOError as e:
|
||||
if e.errno != errno.EACCES:
|
||||
raise
|
||||
print ('The installer cache directory has incorrect permissions.'
|
||||
' Delete %s and try again.'%cache)
|
||||
raise SystemExit(1)
|
||||
do_download(dest)
|
||||
prints('Checking downloaded file integrity...')
|
||||
if not check_signature(dest, signature):
|
||||
os.remove(dest)
|
||||
print ('The downloaded files\' hash does not match. '
|
||||
'Try the download again later.')
|
||||
raise SystemExit(1)
|
||||
return dest
|
||||
# }}}
|
||||
|
||||
# Get tarball signature securely {{{
|
||||
|
||||
def get_proxies(debug=True):
|
||||
proxies = urllib.getproxies()
|
||||
for key, proxy in list(proxies.items()):
|
||||
if not proxy or '..' in proxy:
|
||||
del proxies[key]
|
||||
continue
|
||||
if proxy.startswith(key+'://'):
|
||||
proxy = proxy[len(key)+3:]
|
||||
if key == 'https' and proxy.startswith('http://'):
|
||||
proxy = proxy[7:]
|
||||
if proxy.endswith('/'):
|
||||
proxy = proxy[:-1]
|
||||
if len(proxy) > 4:
|
||||
proxies[key] = proxy
|
||||
else:
|
||||
prints('Removing invalid', key, 'proxy:', proxy)
|
||||
del proxies[key]
|
||||
|
||||
if proxies and debug:
|
||||
prints('Using proxies:', proxies)
|
||||
return proxies
|
||||
|
||||
class HTTPError(ValueError):
|
||||
|
||||
def __init__(self, url, code):
|
||||
msg = '%s returned an unsupported http response code: %d (%s)' % (
|
||||
url, code, httplib.responses.get(code, None))
|
||||
ValueError.__init__(self, msg)
|
||||
self.code = code
|
||||
self.url = url
|
||||
|
||||
class CertificateError(ValueError):
|
||||
pass
|
||||
|
||||
def _dnsname_match(dn, hostname, max_wildcards=1):
|
||||
"""Matching according to RFC 6125, section 6.4.3
|
||||
|
||||
http://tools.ietf.org/html/rfc6125#section-6.4.3
|
||||
"""
|
||||
pats = []
|
||||
if not dn:
|
||||
return False
|
||||
|
||||
parts = dn.split(r'.')
|
||||
leftmost, remainder = parts[0], parts[1:]
|
||||
|
||||
wildcards = leftmost.count('*')
|
||||
if wildcards > max_wildcards:
|
||||
# Issue #17980: avoid denials of service by refusing more
|
||||
# than one wildcard per fragment. A survery of established
|
||||
# policy among SSL implementations showed it to be a
|
||||
# reasonable choice.
|
||||
raise CertificateError(
|
||||
"too many wildcards in certificate DNS name: " + repr(dn))
|
||||
|
||||
# speed up common case w/o wildcards
|
||||
if not wildcards:
|
||||
return dn.lower() == hostname.lower()
|
||||
|
||||
# RFC 6125, section 6.4.3, subitem 1.
|
||||
# The client SHOULD NOT attempt to match a presented identifier in which
|
||||
# the wildcard character comprises a label other than the left-most label.
|
||||
if leftmost == '*':
|
||||
# When '*' is a fragment by itself, it matches a non-empty dotless
|
||||
# fragment.
|
||||
pats.append('[^.]+')
|
||||
elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
|
||||
# RFC 6125, section 6.4.3, subitem 3.
|
||||
# The client SHOULD NOT attempt to match a presented identifier
|
||||
# where the wildcard character is embedded within an A-label or
|
||||
# U-label of an internationalized domain name.
|
||||
pats.append(re.escape(leftmost))
|
||||
else:
|
||||
# Otherwise, '*' matches any dotless string, e.g. www*
|
||||
pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
|
||||
|
||||
# add the remaining fragments, ignore any wildcards
|
||||
for frag in remainder:
|
||||
pats.append(re.escape(frag))
|
||||
|
||||
pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
|
||||
return pat.match(hostname)
|
||||
|
||||
def match_hostname(cert, hostname):
|
||||
"""Verify that *cert* (in decoded format as returned by
|
||||
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125
|
||||
rules are followed, but IP addresses are not accepted for *hostname*.
|
||||
|
||||
CertificateError is raised on failure. On success, the function
|
||||
returns nothing.
|
||||
"""
|
||||
if not cert:
|
||||
raise ValueError("empty or no certificate")
|
||||
dnsnames = []
|
||||
san = cert.get('subjectAltName', ())
|
||||
for key, value in san:
|
||||
if key == 'DNS':
|
||||
if _dnsname_match(value, hostname):
|
||||
return
|
||||
dnsnames.append(value)
|
||||
if not dnsnames:
|
||||
# The subject is only checked when there is no dNSName entry
|
||||
# in subjectAltName
|
||||
for sub in cert.get('subject', ()):
|
||||
for key, value in sub:
|
||||
# XXX according to RFC 2818, the most specific Common Name
|
||||
# must be used.
|
||||
if key == 'commonName':
|
||||
if _dnsname_match(value, hostname):
|
||||
return
|
||||
dnsnames.append(value)
|
||||
if len(dnsnames) > 1:
|
||||
raise CertificateError("hostname %r "
|
||||
"doesn't match either of %s"
|
||||
% (hostname, ', '.join(map(repr, dnsnames))))
|
||||
elif len(dnsnames) == 1:
|
||||
raise CertificateError("hostname %r "
|
||||
"doesn't match %r"
|
||||
% (hostname, dnsnames[0]))
|
||||
else:
|
||||
raise CertificateError("no appropriate commonName or "
|
||||
"subjectAltName fields were found")
|
||||
|
||||
if py3:
|
||||
class HTTPSConnection(httplib.HTTPSConnection):
|
||||
|
||||
def __init__(self, ssl_version, *args, **kwargs):
|
||||
context = kwargs['context'] = ssl.SSLContext(ssl_version)
|
||||
cf = kwargs.pop('cert_file')
|
||||
context.load_verify_locations(cf)
|
||||
context.verify_mode = ssl.CERT_REQUIRED
|
||||
httplib.HTTPSConnection.__init__(self, *args, **kwargs)
|
||||
else:
|
||||
class HTTPSConnection(httplib.HTTPSConnection):
|
||||
|
||||
def __init__(self, ssl_version, *args, **kwargs):
|
||||
httplib.HTTPSConnection.__init__(self, *args, **kwargs)
|
||||
self.calibre_ssl_version = ssl_version
|
||||
|
||||
def connect(self):
|
||||
"""Connect to a host on a given (SSL) port, properly verifying the SSL
|
||||
certificate, both that it is valid and that its declared hostnames
|
||||
match the hostname we are connecting to."""
|
||||
|
||||
sock = socket.create_connection((self.host, self.port),
|
||||
self.timeout, self.source_address)
|
||||
if self._tunnel_host:
|
||||
self.sock = sock
|
||||
self._tunnel()
|
||||
self.sock = ssl.wrap_socket(sock, cert_reqs=ssl.CERT_REQUIRED, ca_certs=self.cert_file, ssl_version=self.calibre_ssl_version)
|
||||
getattr(ssl, 'match_hostname', match_hostname)(self.sock.getpeercert(), self.host)
|
||||
|
||||
CACERT = b'''\
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
-----END CERTIFICATE-----
|
||||
'''
|
||||
|
||||
def get_https_resource_securely(url, timeout=60, max_redirects=5, ssl_version=None):
|
||||
'''
|
||||
Download the resource pointed to by url using https securely (verify server
|
||||
certificate). Ensures that redirects, if any, are also downloaded
|
||||
securely. Needs a CA certificates bundle (in PEM format) to verify the
|
||||
server's certificates.
|
||||
'''
|
||||
if ssl_version is None:
|
||||
ssl_version = ssl.PROTOCOL_TLSv1
|
||||
with tempfile.NamedTemporaryFile(prefix='calibre-ca-cert-') as f:
|
||||
f.write(CACERT)
|
||||
f.flush()
|
||||
p = urlparse(url)
|
||||
if p.scheme != 'https':
|
||||
raise ValueError('URL scheme must be https, not %s' % p.scheme)
|
||||
|
||||
hostname, port = p.hostname, p.port
|
||||
proxies = get_proxies()
|
||||
has_proxy = False
|
||||
for q in ('https', 'http'):
|
||||
if q in proxies:
|
||||
try:
|
||||
h, po = proxies[q].rpartition(':')[::2]
|
||||
po = int(po)
|
||||
if h:
|
||||
hostname, port, has_proxy = h, po, True
|
||||
break
|
||||
except Exception:
|
||||
# Invalid proxy, ignore
|
||||
pass
|
||||
|
||||
c = HTTPSConnection(ssl_version, hostname, port, cert_file=f.name, timeout=timeout)
|
||||
if has_proxy:
|
||||
c.set_tunnel(p.hostname, p.port)
|
||||
|
||||
with closing(c):
|
||||
c.connect() # This is needed for proxy connections
|
||||
path = p.path or '/'
|
||||
if p.query:
|
||||
path += '?' + p.query
|
||||
c.request('GET', path)
|
||||
response = c.getresponse()
|
||||
if response.status in (httplib.MOVED_PERMANENTLY, httplib.FOUND, httplib.SEE_OTHER):
|
||||
if max_redirects <= 0:
|
||||
raise ValueError('Too many redirects, giving up')
|
||||
newurl = response.getheader('Location', None)
|
||||
if newurl is None:
|
||||
raise ValueError('%s returned a redirect response with no Location header' % url)
|
||||
return get_https_resource_securely(
|
||||
newurl, timeout=timeout, max_redirects=max_redirects-1, ssl_version=ssl_version)
|
||||
if response.status != httplib.OK:
|
||||
raise HTTPError(url, response.status)
|
||||
return response.read()
|
||||
|
||||
|
||||
def extract_tarball(tar, destdir):
|
||||
prints('Extracting application files...')
|
||||
if hasattr(tar, 'read'):
|
||||
tar = tar.name
|
||||
with open('/dev/null', 'w') as null:
|
||||
subprocess.check_call(['tar', 'xjof', tar, '-C', destdir], stdout=null,
|
||||
preexec_fn=lambda:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL))
|
||||
|
||||
def get_tarball_info():
|
||||
global signature, calibre_version
|
||||
print ('Downloading tarball signature securely...')
|
||||
raw = get_https_resource_securely('https://status.calibre-ebook.com/tarball-info/' +
|
||||
'x86_64' if is64bit else 'i686')
|
||||
signature, calibre_version = (x.decode('ascii') for x in raw.rpartition(b'@')[::2])
|
||||
if not signature or not calibre_version:
|
||||
raise ValueError('Failed to get install file signature, invalid signature returned')
|
||||
|
||||
|
||||
def download_and_extract(destdir):
|
||||
get_tarball_info()
|
||||
try:
|
||||
f = download_tarball()
|
||||
except:
|
||||
raise
|
||||
print('Failed to download, retrying in 30 seconds...')
|
||||
time.sleep(30)
|
||||
try:
|
||||
f = download_tarball()
|
||||
except:
|
||||
print('Failed to download, aborting')
|
||||
sys.exit(1)
|
||||
|
||||
if os.path.exists(destdir):
|
||||
shutil.rmtree(destdir)
|
||||
os.makedirs(destdir)
|
||||
|
||||
print('Extracting files to %s ...'%destdir)
|
||||
extract_tarball(f, destdir)
|
||||
|
||||
def check_version():
|
||||
global calibre_version
|
||||
if calibre_version == '%version':
|
||||
calibre_version = urllib.urlopen('http://status.calibre-ebook.com/latest').read()
|
||||
|
||||
def main(install_dir=None, bin_dir=None, share_dir=None, isolated=False):
|
||||
defdir = '/opt'
|
||||
autodir = os.environ.get('CALIBRE_INSTALL_DIR', install_dir)
|
||||
automated = False
|
||||
if (autodir is None or not os.path.exists(autodir) or not
|
||||
os.path.isdir(autodir)):
|
||||
destdir = raw_input('Enter the installation directory for calibre [%s]: '%defdir).strip()
|
||||
else:
|
||||
automated = True
|
||||
prints('Automatically installing to: %s'%autodir)
|
||||
destdir = autodir
|
||||
if not destdir:
|
||||
destdir = defdir
|
||||
destdir = os.path.abspath(destdir)
|
||||
if destdir == '/usr/bin':
|
||||
prints(destdir, 'is not a valid install location. Choose', end='')
|
||||
prints('a location like /opt or /usr/local')
|
||||
return 1
|
||||
destdir = os.path.join(destdir, 'calibre')
|
||||
if os.path.exists(destdir):
|
||||
if not os.path.isdir(destdir):
|
||||
prints(destdir, 'exists and is not a directory. Choose a location like /opt or /usr/local')
|
||||
return 1
|
||||
|
||||
download_and_extract(destdir)
|
||||
|
||||
if not isolated:
|
||||
pi = [os.path.join(destdir, 'calibre_postinstall')]
|
||||
if bin_dir is not None:
|
||||
pi.extend(['--bindir', bin_dir])
|
||||
if share_dir is not None:
|
||||
pi.extend(['--sharedir', share_dir])
|
||||
subprocess.call(pi, shell=len(pi) == 1)
|
||||
if not automated:
|
||||
prints()
|
||||
prints(textwrap.dedent(
|
||||
'''
|
||||
You can automate future calibre installs by specifying the
|
||||
installation directory in the install command itself, like
|
||||
this:
|
||||
|
||||
sudo python -c "import sys; from subprocess import Popen, PIPE; \
|
||||
p = Popen('curl -sfLS https://github.com/kovidgoyal/calibre/raw/master/setup/linux-installer.py'.split(),\
|
||||
stdout=PIPE); raw = p.stdout.read(); p.wait() and sys.exit(p.returncode); exec(raw); main(install_dir='/opt'%s)"
|
||||
|
||||
Change /opt above to whatever directory you want calibre to be
|
||||
automatically installed to
|
||||
''' % (', isolated=True' if isolated else '')))
|
||||
prints()
|
||||
if isolated:
|
||||
prints('Run "%s/calibre" to start calibre' % destdir)
|
||||
else:
|
||||
prints('Run "calibre" to start calibre')
|
||||
return 0
|
||||
|
||||
try:
|
||||
__file__
|
||||
from_file = True
|
||||
except NameError:
|
||||
from_file = False
|
||||
|
||||
if __name__ == '__main__' and from_file:
|
||||
main()
|
Loading…
x
Reference in New Issue
Block a user