mirror of
				https://github.com/kovidgoyal/calibre.git
				synced 2025-10-31 10:37:00 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			650 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			650 lines
		
	
	
		
			25 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
 | |
| from argparse import ArgumentParser, FileType
 | |
| from subprocess import check_call, CalledProcessError
 | |
| 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','bz2':'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')
 | |
| 
 | |
| # }}}
 | |
| 
 | |
| 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('.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('..')
 | |
| 
 | |
| # }}}
 | |
| 
 | |
| def upload_to_servers(files, version):  # {{{
 | |
|     base = '/srv/download/'
 | |
|     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 = 'mirror1.fosshub.com'
 | |
|     rdir = 'release/'
 | |
|     check_call(['ssh', 'kovid@%s' % server, 'rm -f release/*'])
 | |
|     for x in files:
 | |
|         start = time.time()
 | |
|         print ('Uploading', x)
 | |
|         sys.stdout.flush()
 | |
|         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()
 | |
|     try:
 | |
|         check_call(['ssh', 'kovid@%s' % server, '/home/kovid/uploadFiles'])
 | |
|     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:
 | |
|             check_call(['ssh', 'kovid@%s' % server, '/home/kovid/uploadFiles'])
 | |
|         else:
 | |
|             raise
 | |
| # }}}
 | |
| 
 | |
| # 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)
 | |
|     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 downloadbestsoftware.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')
 | |
| 
 | |
|     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 == '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()
 | |
| # }}}
 | |
| 
 | |
| 
 | |
| 
 |