mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-05 22:54:17 -04:00
760 lines
30 KiB
Python
760 lines
30 KiB
Python
#!/usr/bin/env python
|
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
|
from __future__ import (unicode_literals, division, absolute_import,
|
|
print_function)
|
|
|
|
__license__ = 'GPL v3'
|
|
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
|
__docformat__ = 'restructuredtext en'
|
|
|
|
import os, time, sys, traceback, subprocess, urllib2, re, base64, httplib, shutil, glob, json, mimetypes
|
|
from pprint import pprint
|
|
from argparse import ArgumentParser, FileType
|
|
from subprocess import check_call, CalledProcessError, check_output
|
|
from tempfile import NamedTemporaryFile
|
|
from collections import OrderedDict
|
|
|
|
def login_to_google(username, password): # {{{
|
|
import mechanize
|
|
br = mechanize.Browser()
|
|
br.addheaders = [('User-agent',
|
|
'Mozilla/5.0 (X11; Linux x86_64; rv:9.0) Gecko/20100101 Firefox/9.0')]
|
|
br.set_handle_robots(False)
|
|
br.open('https://accounts.google.com/ServiceLogin?service=code')
|
|
br.select_form(nr=0)
|
|
br.form['Email'] = username
|
|
br.form['Passwd'] = password
|
|
raw = br.submit().read()
|
|
if re.search(br'(?i)<title>.*?Account Settings</title>', raw) is None:
|
|
x = re.search(br'(?is)<title>.*?</title>', raw)
|
|
if x is not None:
|
|
print ('Title of post login page: %s'%x.group())
|
|
# open('/tmp/goog.html', 'wb').write(raw)
|
|
raise ValueError(('Failed to login to google with credentials: %s %s'
|
|
'\nGoogle sometimes requires verification when logging in from a '
|
|
'new IP address. Use lynx to login and supply the verification, '
|
|
'at: lynx -accept_all_cookies https://accounts.google.com/ServiceLogin?service=code')
|
|
%(username, password))
|
|
return br
|
|
# }}}
|
|
|
|
class ReadFileWithProgressReporting(file): # {{{
|
|
|
|
def __init__(self, path, mode='rb'):
|
|
file.__init__(self, path, mode)
|
|
self.seek(0, os.SEEK_END)
|
|
self._total = self.tell()
|
|
self.seek(0)
|
|
self.start_time = time.time()
|
|
|
|
def __len__(self):
|
|
return self._total
|
|
|
|
def read(self, size):
|
|
data = file.read(self, size)
|
|
if data:
|
|
self.report_progress(len(data))
|
|
return data
|
|
|
|
def report_progress(self, size):
|
|
sys.stdout.write(b'\x1b[s')
|
|
sys.stdout.write(b'\x1b[K')
|
|
frac = float(self.tell())/self._total
|
|
mb_pos = self.tell()/float(1024**2)
|
|
mb_tot = self._total/float(1024**2)
|
|
kb_pos = self.tell()/1024.0
|
|
kb_rate = kb_pos/(time.time()-self.start_time)
|
|
bit_rate = kb_rate * 1024
|
|
eta = int((self._total - self.tell())/bit_rate) + 1
|
|
eta_m, eta_s = eta / 60, eta % 60
|
|
sys.stdout.write(
|
|
' %.1f%% %.1f/%.1fMB %.1f KB/sec %d minutes, %d seconds left'%(
|
|
frac*100, mb_pos, mb_tot, kb_rate, eta_m, eta_s))
|
|
sys.stdout.write(b'\x1b[u')
|
|
if self.tell() >= self._total:
|
|
sys.stdout.write('\n')
|
|
t = int(time.time() - self.start_time) + 1
|
|
print ('Upload took %d minutes and %d seconds at %.1f KB/sec' % (
|
|
t/60, t%60, kb_rate))
|
|
sys.stdout.flush()
|
|
# }}}
|
|
|
|
class Base(object): # {{{
|
|
|
|
def __init__(self):
|
|
self.d = os.path.dirname
|
|
self.j = os.path.join
|
|
self.a = os.path.abspath
|
|
self.b = os.path.basename
|
|
self.s = os.path.splitext
|
|
self.e = os.path.exists
|
|
|
|
def info(self, *args, **kwargs):
|
|
print(*args, **kwargs)
|
|
sys.stdout.flush()
|
|
|
|
def warn(self, *args, **kwargs):
|
|
print('\n'+'_'*20, 'WARNING','_'*20)
|
|
print(*args, **kwargs)
|
|
print('_'*50)
|
|
sys.stdout.flush()
|
|
|
|
# }}}
|
|
|
|
class GoogleCode(Base): # {{{
|
|
|
|
def __init__(self,
|
|
# A mapping of filenames to file descriptions. The descriptions are
|
|
# used to populate the description field for the upload on google
|
|
# code
|
|
files,
|
|
|
|
# The unix name for the application.
|
|
appname,
|
|
|
|
# The version being uploaded
|
|
version,
|
|
|
|
# Google account username
|
|
username,
|
|
|
|
# Googlecode.com password
|
|
password,
|
|
|
|
# Google account password
|
|
gmail_password,
|
|
|
|
# The name of the google code project we are uploading to
|
|
gc_project,
|
|
|
|
# Server to which to upload the mapping of file names to google
|
|
# code URLs. If not None, upload is performed via shelling out to
|
|
# ssh, so you must have ssh-agent setup with the authenticated key
|
|
# and ssh agent forwarding enabled
|
|
gpaths_server=None,
|
|
# The path on gpaths_server to which to upload the mapping data
|
|
gpaths=None,
|
|
|
|
# If True, files are replaced, otherwise existing files are skipped
|
|
reupload=False,
|
|
|
|
# The pattern to match filenames for the files being uploaded and
|
|
# extract version information from them. Must have a named group
|
|
# named version
|
|
filename_pattern=r'{appname}-(?:portable-installer-)?(?P<version>.+?)(?:-(?:i686|x86_64|32bit|64bit))?\.(?:zip|exe|msi|dmg|tar\.bz2|tar\.xz|txz|tbz2)' # noqa
|
|
|
|
):
|
|
self.username, self.password, = username, password
|
|
self.gmail_password, self.gc_project = gmail_password, gc_project
|
|
self.reupload, self.files, self.version = reupload, files, version
|
|
self.gpaths, self.gpaths_server = gpaths, gpaths_server
|
|
|
|
self.upload_host = '%s.googlecode.com'%gc_project
|
|
self.files_list = 'http://code.google.com/p/%s/downloads/list'%gc_project
|
|
self.delete_url = 'http://code.google.com/p/%s/downloads/delete?name=%%s'%gc_project
|
|
|
|
self.filename_pat = re.compile(filename_pattern.format(appname=appname))
|
|
for x in self.files:
|
|
if self.filename_pat.match(os.path.basename(x)) is None:
|
|
raise ValueError(('The filename %s does not match the '
|
|
'filename pattern')%os.path.basename(x))
|
|
|
|
def upload_one(self, fname, retries=2):
|
|
self.info('\nUploading', fname)
|
|
typ = 'Type-' + ('Source' if fname.endswith('.xz') else 'Archive' if
|
|
fname.endswith('.zip') else 'Installer')
|
|
ext = os.path.splitext(fname)[1][1:]
|
|
op = 'OpSys-'+{'msi':'Windows','exe':'Windows',
|
|
'dmg':'OSX','txz':'Linux','xz':'All'}[ext]
|
|
desc = self.files[fname]
|
|
start = time.time()
|
|
for i in range(retries):
|
|
try:
|
|
path = self.upload(os.path.abspath(fname), desc,
|
|
labels=[typ, op, 'Featured'], retry=100)
|
|
except KeyboardInterrupt:
|
|
raise SystemExit(1)
|
|
except:
|
|
traceback.print_exc()
|
|
print ('\nUpload failed, trying again in 30 secs.',
|
|
'%d retries left.'%(retries-1))
|
|
time.sleep(30)
|
|
else:
|
|
break
|
|
self.info('Uploaded to:', path, 'in', int(time.time() - start),
|
|
'seconds')
|
|
return path
|
|
|
|
def re_upload(self):
|
|
fnames = {os.path.basename(x):x for x in self.files}
|
|
existing = self.old_files.intersection(set(fnames))
|
|
br = self.login_to_google()
|
|
for x, src in fnames.iteritems():
|
|
if not os.access(src, os.R_OK):
|
|
continue
|
|
if x in existing:
|
|
self.info('Deleting', x)
|
|
br.open(self.delete_url%x)
|
|
br.select_form(predicate=lambda y: 'delete.do' in y.action)
|
|
br.form.find_control(name='delete')
|
|
br.submit(name='delete')
|
|
self.upload_one(src)
|
|
|
|
def __call__(self):
|
|
self.paths = {}
|
|
self.old_files = self.get_old_files()
|
|
if self.reupload:
|
|
return self.re_upload()
|
|
|
|
for fname in self.files:
|
|
bname = os.path.basename(fname)
|
|
if bname in self.old_files:
|
|
path = 'http://%s.googlecode.com/files/%s'%(self.gc_project,
|
|
bname)
|
|
self.info(
|
|
'%s already uploaded, skipping. Assuming URL is: %s'%(
|
|
bname, path))
|
|
self.old_files.remove(bname)
|
|
else:
|
|
path = self.upload_one(fname)
|
|
self.paths[bname] = path
|
|
self.info('Updating path map')
|
|
for k, v in self.paths.iteritems():
|
|
self.info('\t%s => %s'%(k, v))
|
|
if self.gpaths and self.gpaths_server:
|
|
raw = subprocess.Popen(['ssh', self.gpaths_server, 'cat', self.gpaths],
|
|
stdout=subprocess.PIPE).stdout.read()
|
|
paths = eval(raw) if raw else {}
|
|
paths.update(self.paths)
|
|
rem = [x for x in paths if self.version not in x]
|
|
for x in rem:
|
|
paths.pop(x)
|
|
raw = ['%r : %r,'%(k, v) for k, v in paths.items()]
|
|
raw = '{\n\n%s\n\n}\n'%('\n'.join(raw))
|
|
with NamedTemporaryFile() as t:
|
|
t.write(raw)
|
|
t.flush()
|
|
check_call(['scp', t.name, '%s:%s'%(self.gpaths_server,
|
|
self.gpaths)])
|
|
if self.old_files:
|
|
self.br = self.login_to_google()
|
|
self.delete_old_files()
|
|
|
|
def login_to_google(self):
|
|
self.info('Logging into Google')
|
|
return login_to_google(self.username, self.gmail_password)
|
|
|
|
def get_files_hosted_by_google_code(self):
|
|
from lxml import html
|
|
self.info('Getting existing files in google code:', self.gc_project)
|
|
raw = urllib2.urlopen(self.files_list).read()
|
|
root = html.fromstring(raw)
|
|
ans = {}
|
|
for a in root.xpath('//td[@class="vt id col_0"]/a[@href]'):
|
|
ans[a.text.strip()] = a.get('href')
|
|
return ans
|
|
|
|
def get_old_files(self):
|
|
ans = set()
|
|
for fname in self.get_files_hosted_by_google_code():
|
|
m = self.filename_pat.match(fname)
|
|
if m is not None:
|
|
ans.add(fname)
|
|
return ans
|
|
|
|
def delete_old_files(self):
|
|
if not self.old_files:
|
|
return
|
|
self.info('Deleting old files from Google Code...')
|
|
for fname in self.old_files:
|
|
self.info('\tDeleting', fname)
|
|
self.br.open(self.delete_url%fname)
|
|
self.br.select_form(predicate=lambda x: 'delete.do' in x.action)
|
|
self.br.form.find_control(name='delete')
|
|
self.br.submit(name='delete')
|
|
|
|
def encode_upload_request(self, fields, file_path):
|
|
BOUNDARY = '----------Googlecode_boundary_reindeer_flotilla'
|
|
|
|
body = []
|
|
|
|
# Add the metadata about the upload first
|
|
for key, value in fields:
|
|
body.extend(
|
|
['--' + BOUNDARY,
|
|
'Content-Disposition: form-data; name="%s"' % key,
|
|
'',
|
|
value,
|
|
])
|
|
|
|
# Now add the file itself
|
|
file_name = os.path.basename(file_path)
|
|
with open(file_path, 'rb') as f:
|
|
file_content = f.read()
|
|
|
|
body.extend(
|
|
['--' + BOUNDARY,
|
|
'Content-Disposition: form-data; name="filename"; filename="%s"'
|
|
% file_name,
|
|
# The upload server determines the mime-type, no need to set it.
|
|
'Content-Type: application/octet-stream',
|
|
'',
|
|
file_content,
|
|
])
|
|
|
|
# Finalize the form body
|
|
body.extend(['--' + BOUNDARY + '--', ''])
|
|
body = [x.encode('ascii') if isinstance(x, unicode) else x for x in
|
|
body]
|
|
|
|
return ('multipart/form-data; boundary=%s' % BOUNDARY,
|
|
b'\r\n'.join(body))
|
|
|
|
def upload(self, fname, desc, labels=[], retry=0):
|
|
form_fields = [('summary', desc)]
|
|
form_fields.extend([('label', l.strip()) for l in labels])
|
|
|
|
content_type, body = self.encode_upload_request(form_fields, fname)
|
|
upload_uri = '/files'
|
|
auth_token = base64.b64encode('%s:%s'% (self.username, self.password))
|
|
headers = {
|
|
'Authorization': 'Basic %s' % auth_token,
|
|
'User-Agent': 'googlecode.com uploader v1',
|
|
'Content-Type': content_type,
|
|
}
|
|
|
|
with NamedTemporaryFile(delete=False) as f:
|
|
f.write(body)
|
|
|
|
try:
|
|
body = ReadFileWithProgressReporting(f.name)
|
|
server = httplib.HTTPSConnection(self.upload_host)
|
|
server.request('POST', upload_uri, body, headers)
|
|
resp = server.getresponse()
|
|
server.close()
|
|
finally:
|
|
os.remove(f.name)
|
|
|
|
if resp.status == 201:
|
|
return resp.getheader('Location')
|
|
|
|
print ('Failed to upload with code %d and reason: %s'%(resp.status,
|
|
resp.reason))
|
|
if retry < 1:
|
|
print ('Retrying in 5 seconds....')
|
|
time.sleep(5)
|
|
return self.upload(fname, desc, labels=labels, retry=retry+1)
|
|
raise Exception('Failed to upload '+fname)
|
|
|
|
|
|
# }}}
|
|
|
|
class SourceForge(Base): # {{{
|
|
|
|
# Note that you should manually ssh once to username,project@frs.sourceforge.net
|
|
# on the staging server so that the host key is setup
|
|
|
|
def __init__(self, files, project, version, username, replace=False):
|
|
self.username, self.project, self.version = username, project, version
|
|
self.base = '/home/frs/project/c/ca/'+project
|
|
self.rdir = self.base + '/' + version
|
|
self.files = files
|
|
|
|
def __call__(self):
|
|
for x in self.files:
|
|
start = time.time()
|
|
self.info('Uploading', x)
|
|
for i in range(5):
|
|
try:
|
|
check_call(['rsync', '-h', '-z', '--progress', '-e', 'ssh -x', x,
|
|
'%s,%s@frs.sourceforge.net:%s'%(self.username, self.project,
|
|
self.rdir+'/')])
|
|
except KeyboardInterrupt:
|
|
raise SystemExit(1)
|
|
except:
|
|
print ('\nUpload failed, trying again in 30 seconds')
|
|
time.sleep(30)
|
|
else:
|
|
break
|
|
print ('Uploaded in', int(time.time() - start), 'seconds\n\n')
|
|
|
|
# }}}
|
|
|
|
class GitHub(Base): # {{{
|
|
|
|
API = 'https://api.github.com/'
|
|
|
|
def __init__(self, files, reponame, version, username, password, replace=False):
|
|
self.files, self.reponame, self.version, self.username, self.password, self.replace = (
|
|
files, reponame, version, username, password, replace)
|
|
import requests
|
|
self.requests = s = requests.Session()
|
|
s.auth = (self.username, self.password)
|
|
s.headers.update({'Accept': 'application/vnd.github.v3+json'})
|
|
|
|
def __call__(self):
|
|
release = self.create_release()
|
|
upload_url = release['upload_url'].partition('{')[0]
|
|
existing_assets = self.existing_assets(release['id'])
|
|
for path, desc in self.files.iteritems():
|
|
self.info('')
|
|
url = self.API + 'repos/%s/%s/releases/assets/{}' % (self.username, self.reponame)
|
|
fname = os.path.basename(path)
|
|
if fname in existing_assets:
|
|
self.info('Deleting %s from GitHub with id: %s' % (fname, existing_assets[fname]))
|
|
r = self.requests.delete(url.format(existing_assets[fname]))
|
|
if r.status_code != 204:
|
|
self.fail(r, 'Failed to delete %s from GitHub' % fname)
|
|
r = self.do_upload(upload_url, path, desc, fname)
|
|
if r.status_code != 201:
|
|
self.fail(r, 'Failed to upload file: %s' % fname)
|
|
r = self.requests.patch(url.format(r.json()['id']),
|
|
data=json.dumps({'name':fname, 'label':desc}))
|
|
if r.status_code != 200:
|
|
self.fail(r, 'Failed to set label for %s' % fname)
|
|
|
|
def do_upload(self, url, path, desc, fname):
|
|
mime_type = mimetypes.guess_type(fname)[0]
|
|
self.info('Uploading to GitHub: %s (%s)' % (fname, mime_type))
|
|
with ReadFileWithProgressReporting(path) as f:
|
|
return self.requests.post(
|
|
url, headers={'Content-Type': mime_type, 'Content-Length':str(f._total)}, params={'name':fname},
|
|
data=f)
|
|
|
|
def fail(self, r, msg):
|
|
print (msg, ' Status Code: %s' % r.status_code, file=sys.stderr)
|
|
print ("JSON from response:", file=sys.stderr)
|
|
pprint(dict(r.json()), stream=sys.stderr)
|
|
raise SystemExit(1)
|
|
|
|
def already_exists(self, r):
|
|
error_code = r.json().get('errors', [{}])[0].get('code', None)
|
|
return error_code == 'already_exists'
|
|
|
|
def existing_assets(self, release_id):
|
|
url = self.API + 'repos/%s/%s/releases/%s/assets' % (self.username, self.reponame, release_id)
|
|
r = self.requests.get(url)
|
|
if r.status_code != 200:
|
|
self.fail('Failed to get assets for release')
|
|
return {asset['name']:asset['id'] for asset in r.json()}
|
|
|
|
def create_release(self):
|
|
' Create a release on GitHub or if it already exists, return the existing release '
|
|
url = self.API + 'repos/%s/%s/releases' % (self.username, self.reponame)
|
|
r = self.requests.post(url, data=json.dumps({
|
|
'tag_name':'v%s' % self.version,
|
|
'target_commitish': 'master',
|
|
'name': 'version %s' % self.version,
|
|
'body': 'Release version %s' % self.version,
|
|
'draft': False, 'prerelease':False
|
|
}))
|
|
if r.status_code != 201:
|
|
if not self.already_exists(r):
|
|
self.fail(r, 'Failed to create release for version: %s' % self.version)
|
|
# Find existing release
|
|
r = self.requests.get(url)
|
|
if r.status_code != 200:
|
|
self.fail(r, 'Failed to list releases')
|
|
for release in reversed(r.json()):
|
|
if release.get('tag_name', None) == 'v' + self.version:
|
|
return release
|
|
return r.json()
|
|
|
|
# }}}
|
|
|
|
def generate_index(): # {{{
|
|
os.chdir('/srv/download')
|
|
releases = set()
|
|
for x in os.listdir('.'):
|
|
if os.path.isdir(x) and '.' in x:
|
|
releases.add(tuple((int(y) for y in x.split('.'))))
|
|
rmap = OrderedDict()
|
|
for rnum in sorted(releases, reverse=True):
|
|
series = rnum[:2] if rnum[0] == 0 else rnum[:1]
|
|
if series not in rmap:
|
|
rmap[series] = []
|
|
rmap[series].append(rnum)
|
|
|
|
template = '''<!DOCTYPE html>\n<html lang="en"> <head> <meta charset="utf-8"> <title>{title}</title> <style type="text/css"> {style} </style> </head> <body> <h1>{title}</h1> <p>{msg}</p> {body} </body> </html> ''' # noqa
|
|
style = '''
|
|
body { font-family: sans-serif; background-color: #eee; }
|
|
a { text-decoration: none; }
|
|
a:visited { color: blue }
|
|
a:hover { color: red }
|
|
ul { list-style-type: none }
|
|
li { padding-bottom: 1ex }
|
|
dd li { text-indent: 0; margin: 0 }
|
|
dd ul { padding: 0; margin: 0 }
|
|
dt { font-weight: bold }
|
|
dd { margin-bottom: 2ex }
|
|
'''
|
|
body = []
|
|
for series in rmap:
|
|
body.append('<li><a href="{0}.html" title="Releases in the {0}.x series">{0}.x</a>\xa0\xa0\xa0<span style="font-size:smaller">[{1} releases]</span></li>'.format( # noqa
|
|
'.'.join(map(type(''), series)), len(rmap[series])))
|
|
body = '<ul>{0}</ul>'.format(' '.join(body))
|
|
index = template.format(title='Previous calibre releases', style=style, msg='Choose a series of calibre releases', body=body)
|
|
with open('index.html', 'wb') as f:
|
|
f.write(index.encode('utf-8'))
|
|
|
|
for series, releases in rmap.iteritems():
|
|
sname = '.'.join(map(type(''), series))
|
|
body = [
|
|
'<li><a href="{0}/" title="Release {0}">{0}</a></li>'.format('.'.join(map(type(''), r)))
|
|
for r in releases]
|
|
body = '<ul class="release-list">{0}</ul>'.format(' '.join(body))
|
|
index = template.format(title='Previous calibre releases (%s.x)' % sname, style=style,
|
|
msg='Choose a calibre release', body=body)
|
|
with open('%s.html' % sname, 'wb') as f:
|
|
f.write(index.encode('utf-8'))
|
|
|
|
for r in releases:
|
|
rname = '.'.join(map(type(''), r))
|
|
os.chdir(rname)
|
|
try:
|
|
body = []
|
|
files = os.listdir('.')
|
|
windows = [x for x in files if x.endswith('.msi')]
|
|
if windows:
|
|
windows = ['<li><a href="{0}" title="{1}">{1}</a></li>'.format(
|
|
x, 'Windows 64-bit Installer' if '64bit' in x else 'Windows 32-bit Installer')
|
|
for x in windows]
|
|
body.append('<dt>Windows</dt><dd><ul>{0}</ul></dd>'.format(' '.join(windows)))
|
|
portable = [x for x in files if '-portable-' in x]
|
|
if portable:
|
|
body.append('<dt>Calibre Portable</dt><dd><a href="{0}" title="{1}">{1}</a></dd>'.format(
|
|
portable[0], 'Calibre Portable Installer'))
|
|
osx = [x for x in files if x.endswith('.dmg')]
|
|
if osx:
|
|
body.append('<dt>Apple Mac</dt><dd><a href="{0}" title="{1}">{1}</a></dd>'.format(
|
|
osx[0], 'OS X Disk Image (.dmg)'))
|
|
linux = [x for x in files if x.endswith('.txz') or x.endswith('tar.bz2')]
|
|
if linux:
|
|
linux = ['<li><a href="{0}" title="{1}">{1}</a></li>'.format(
|
|
x, 'Linux 64-bit binary' if 'x86_64' in x else 'Linux 32-bit binary')
|
|
for x in linux]
|
|
body.append('<dt>Linux</dt><dd><ul>{0}</ul></dd>'.format(' '.join(linux)))
|
|
source = [x for x in files if x.endswith('.xz') or x.endswith('.gz')]
|
|
if source:
|
|
body.append('<dt>Source Code</dt><dd><a href="{0}" title="{1}">{1}</a></dd>'.format(
|
|
source[0], 'Source code (all platforms)'))
|
|
|
|
body = '<dl>{0}</dl>'.format(''.join(body))
|
|
index = template.format(title='calibre release (%s)' % rname, style=style,
|
|
msg='', body=body)
|
|
with open('index.html', 'wb') as f:
|
|
f.write(index.encode('utf-8'))
|
|
finally:
|
|
os.chdir('..')
|
|
|
|
# }}}
|
|
|
|
SERVER_BASE = '/srv/download/'
|
|
|
|
def upload_to_servers(files, version): # {{{
|
|
base = SERVER_BASE
|
|
dest = os.path.join(base, version)
|
|
if not os.path.exists(dest):
|
|
os.mkdir(dest)
|
|
for src in files:
|
|
shutil.copyfile(src, os.path.join(dest, os.path.basename(src)))
|
|
cwd = os.getcwd()
|
|
try:
|
|
generate_index()
|
|
finally:
|
|
os.chdir(cwd)
|
|
|
|
for server, rdir in {'files':'/srv/download/'}.iteritems():
|
|
print('Uploading to server:', server)
|
|
server = '%s.calibre-ebook.com' % server
|
|
# Copy the generated index files
|
|
print ('Copying generated index')
|
|
check_call(['rsync', '-hza', '-e', 'ssh -x', '--include', '*.html',
|
|
'--filter', '-! */', base, 'root@%s:%s' % (server, rdir)])
|
|
# Copy the release files
|
|
rdir = '%s%s/' % (rdir, version)
|
|
for x in files:
|
|
start = time.time()
|
|
print ('Uploading', x)
|
|
for i in range(5):
|
|
try:
|
|
check_call(['rsync', '-h', '-z', '--progress', '-e', 'ssh -x', x,
|
|
'root@%s:%s'%(server, rdir)])
|
|
except KeyboardInterrupt:
|
|
raise SystemExit(1)
|
|
except:
|
|
print ('\nUpload failed, trying again in 30 seconds')
|
|
time.sleep(30)
|
|
else:
|
|
break
|
|
print ('Uploaded in', int(time.time() - start), 'seconds\n\n')
|
|
|
|
# }}}
|
|
|
|
def upload_to_dbs(files, version): # {{{
|
|
print('Uploading to fosshub.com')
|
|
sys.stdout.flush()
|
|
server = 'mirror10.fosshub.com'
|
|
rdir = 'release/'
|
|
def run_ssh(command, func=check_call):
|
|
cmd = ['ssh', '-x', 'kovid@%s' % server, command]
|
|
try:
|
|
return func(cmd)
|
|
except CalledProcessError as err:
|
|
# fosshub is being a little flaky sshing into it is failing the first
|
|
# time, needing a retry
|
|
if err.returncode != 255:
|
|
raise
|
|
return func(cmd)
|
|
|
|
old_files = set(run_ssh('ls ' + rdir, func=check_output).decode('utf-8').split())
|
|
if len(files) < 7:
|
|
existing = set(map(os.path.basename, files))
|
|
# fosshub does not support partial re-uploads
|
|
for f in glob.glob('%s/%s/calibre-*' % (SERVER_BASE, version)):
|
|
if os.path.basename(f) not in existing:
|
|
files[f] = None
|
|
|
|
for x in files:
|
|
start = time.time()
|
|
print ('Uploading', x)
|
|
sys.stdout.flush()
|
|
old_files.discard(os.path.basename(x))
|
|
for i in range(5):
|
|
try:
|
|
check_call(['rsync', '-h', '-z', '--progress', '-e', 'ssh -x', x,
|
|
'kovid@%s:%s'%(server, rdir)])
|
|
except KeyboardInterrupt:
|
|
raise SystemExit(1)
|
|
except:
|
|
print ('\nUpload failed, trying again in 30 seconds')
|
|
sys.stdout.flush()
|
|
time.sleep(30)
|
|
else:
|
|
break
|
|
print ('Uploaded in', int(time.time() - start), 'seconds\n\n')
|
|
sys.stdout.flush()
|
|
|
|
if old_files:
|
|
run_ssh('rm -f %s' % (' '.join(rdir + x for x in old_files)))
|
|
run_ssh('/home/kovid/uploadFiles')
|
|
# }}}
|
|
|
|
# CLI {{{
|
|
def cli_parser():
|
|
epilog='Copyright Kovid Goyal 2012'
|
|
|
|
p = ArgumentParser(
|
|
description='Upload project files to a hosting service automatically',
|
|
epilog=epilog
|
|
)
|
|
a = p.add_argument
|
|
a('appname', help='The name of the application, all files to'
|
|
' upload should begin with this name')
|
|
a('version', help='The version of the application, all files to'
|
|
' upload should contain this version')
|
|
a('file_map', type=FileType('rb'),
|
|
help='A file containing a mapping of files to be uploaded to '
|
|
'descriptions of the files. The descriptions will be visible '
|
|
'to users trying to get the file from the hosting service. '
|
|
'The format of the file is filename: description, with one per '
|
|
'line. filename can be a path to the file relative to the current '
|
|
'directory.')
|
|
a('--replace', action='store_true', default=False,
|
|
help='If specified, existing files are replaced, otherwise '
|
|
'they are skipped.')
|
|
|
|
subparsers = p.add_subparsers(help='Where to upload to', dest='service',
|
|
title='Service', description='Hosting service to upload to')
|
|
gc = subparsers.add_parser('googlecode', help='Upload to googlecode',
|
|
epilog=epilog)
|
|
sf = subparsers.add_parser('sourceforge', help='Upload to sourceforge',
|
|
epilog=epilog)
|
|
gh = subparsers.add_parser('github', help='Upload to GitHub',
|
|
epilog=epilog)
|
|
cron = subparsers.add_parser('cron', help='Call script from cron')
|
|
subparsers.add_parser('calibre', help='Upload to calibre file servers')
|
|
subparsers.add_parser('dbs', help='Upload to fosshub.com')
|
|
|
|
a = gc.add_argument
|
|
|
|
a('project',
|
|
help='The name of the project on google code we are uploading to')
|
|
a('username',
|
|
help='Username to log into your google account')
|
|
a('password',
|
|
help='Password to log into your google account')
|
|
a('gc_password',
|
|
help='Password for google code hosting.'
|
|
' Get it from http://code.google.com/hosting/settings')
|
|
|
|
a('--path-map-server',
|
|
help='A server to which the mapping of filenames to googlecode '
|
|
'URLs will be uploaded. The upload happens via ssh, so you must '
|
|
'have a working ssh agent')
|
|
a('--path-map-location',
|
|
help='Path on the server where the path map is placed.')
|
|
|
|
a = sf.add_argument
|
|
a('project',
|
|
help='The name of the project on sourceforge we are uploading to')
|
|
a('username',
|
|
help='Sourceforge username')
|
|
|
|
a = cron.add_argument
|
|
a('username',
|
|
help='Username to log into your google account')
|
|
a('password',
|
|
help='Password to log into your google account')
|
|
|
|
a = gh.add_argument
|
|
a('project',
|
|
help='The name of the repository on GitHub we are uploading to')
|
|
a('username',
|
|
help='Username to log into your GitHub account')
|
|
a('password',
|
|
help='Password to log into your GitHub account')
|
|
|
|
return p
|
|
|
|
def main(args=None):
|
|
cli = cli_parser()
|
|
args = cli.parse_args(args)
|
|
files = {}
|
|
if args.service != 'cron':
|
|
with args.file_map as f:
|
|
for line in f:
|
|
fname, _, desc = line.partition(':')
|
|
fname, desc = fname.strip(), desc.strip()
|
|
if fname and desc:
|
|
files[fname] = desc
|
|
|
|
ofiles = OrderedDict()
|
|
for x in sorted(files, key=lambda x:os.stat(x).st_size, reverse=True):
|
|
ofiles[x] = files[x]
|
|
|
|
if args.service == 'googlecode':
|
|
gc = GoogleCode(ofiles, args.appname, args.version, args.username,
|
|
args.gc_password, args.password, args.project,
|
|
gpaths_server=args.path_map_server,
|
|
gpaths=args.path_map_location, reupload=args.replace)
|
|
gc()
|
|
elif args.service == 'sourceforge':
|
|
sf = SourceForge(ofiles, args.project, args.version, args.username,
|
|
replace=args.replace)
|
|
sf()
|
|
elif args.service == 'github':
|
|
gh = GitHub(ofiles, args.project, args.version, args.username, args.password,
|
|
replace=args.replace)
|
|
gh()
|
|
elif args.service == 'cron':
|
|
login_to_google(args.username, args.password)
|
|
elif args.service == 'calibre':
|
|
upload_to_servers(ofiles, args.version)
|
|
elif args.service == 'dbs':
|
|
upload_to_dbs(ofiles, args.version)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
# }}}
|