#!/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 ' __docformat__ = 'restructuredtext en' import os, time, sys, traceback, subprocess, urllib2, re, base64, httplib from argparse import ArgumentParser, FileType from subprocess import check_call from tempfile import NamedTemporaryFile from collections import OrderedDict import mechanize from lxml import html def login_to_google(username, password): # {{{ 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).*?Account Settings', raw) is None: x = re.search(br'(?is).*?', 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.+?)(?:-(?: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): 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 upload_to_servers(files, version): # {{{ for server, rdir in {'files':'/usr/share/nginx/html'}.iteritems(): print('Uploading to server:', server) server = '%s.calibre-ebook.com' % server 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 downloadbestsoftware.com') server = 'www.downloadbestsoft-mirror1.com' rdir = 'release/' check_call(['ssh', 'kovid@%s' % server, 'rm -f release/*']) 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, 'kovid@%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') check_call(['ssh', 'kovid@%s' % server, '/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) 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() # }}}