chiark / gitweb /
Merge branch 'master' into 'master'
authorCiaran Gultnieks <ciaran@ciarang.com>
Wed, 23 Apr 2014 07:31:56 +0000 (07:31 +0000)
committerCiaran Gultnieks <ciaran@ciarang.com>
Wed, 23 Apr 2014 07:31:56 +0000 (07:31 +0000)
support cloud storage, and assorted other improvements

I just finished support for `fdroid server update` to push to Amazon AWS S3 cloud store. It uses libcloud, so there could be lots of other cloud storage services added.  This would be used for alternative hosting locations for repos.  For example, for the Guardian Project repo, we'd include the URLs to various cloud storage options like https://s3.amazonaws.com/guardianproject/fdroid/repo
Services like https://s3.amazonaws.com/ are often not blocked when other things are.

It does not need to be Amazon-specific.  I went with libcloud because it supports like 10 cloud storage and is under active development.  What is there is just the start. I'm new to cloud stuff, so I just started based on a script that Adam Prichart of psiphon gave me. I had to do a fair amount of packaging work to get the python-libcloud Debian package updated. I almost have the 0.14.1 update done, I hope that'll be in Debian tonight.

Lots more info in the commit messages.

MANIFEST.in
README
docs/fdroid.texi
examples/config.py
fdroidserver/common.py
fdroidserver/server.py
fdroidserver/update.py
jenkins-build
setup.py

index a323c53160279950c3d99ca833da0a9cbf8f18b7..47cd528e3e668e24d65c4b3a9b13dfc8c8152ffd 100644 (file)
@@ -2,11 +2,10 @@ include README
 include COPYING
 include fd-commit
 include fdroid
-include jenkins-build.sh
+include jenkins-build
 include makebuildserver
 include updateplugin
 include buildserver/config.buildserver.py
-include buildserver/cookbooks
 include buildserver/fixpaths.sh
 include buildserver/cookbooks/android-ndk/recipes/default.rb
 include buildserver/cookbooks/android-sdk/recipes/default.rb
diff --git a/README b/README
index 4a9ac0755259fa294a4b1f9973de713617e9c4a9..218deff818099d04e9a293fa2709ad06c282cb7d 100644 (file)
--- a/README
+++ b/README
@@ -10,3 +10,21 @@ assist in creating, testing and submitting metadata to the main repository.
 For documentation, please see the docs directory.
 
 Alternatively, visit https://f-droid.org/manual/
+
+
+Installing
+----------
+
+The easiest way to install the fdroidserver tools is to use virtualenv and pip
+(if you are Debian/Ubuntu/Mint/etc, you can first try installing using
+`apt-get install fdroidserver`).  First, make sure you have virtualenv
+installed, it should be included in your OS's Python distribution or via other
+mechanisms like dnf/yum/pacman/emerge/Fink/MacPorts/Brew.  Then here's how to
+install:
+
+    git clone https://gitlab.com/fdroid/fdroidserver.git
+    cd fdroidserver
+    virtualenv env/
+    . env/bin/activate
+    pip install -e .
+    python setup.py install
index d1be922330fd3ad5bec217f0635781d8b4f657e4..6bdcb563002418e9bb8aa45ab2c3b0bc7a457c6a 100644 (file)
@@ -160,7 +160,7 @@ certainly want to work from a git clone of the tools at this stage. To
 get started:
 
 @example
-git clone git@gitlab.com:fdroid/fdroidserver.git
+git clone https://gitlab.com/fdroid/fdroidserver.git
 @end example
 
 You now have lots of stuff in the fdroidserver directory, but the most
@@ -177,7 +177,7 @@ repository management tasks. You can either create a brand new one, or
 grab a copy of the data used by the main F-Droid repository:
 
 @example
-git clone git@gitlab.com:fdroid/fdroiddata.git
+git clone https://gitlab.com/fdroid/fdroiddata.git
 @end example
 
 Regardless of the intended usage of the tools, you will always need to set
index 4556233edf8b8e3eb1fdb588e547f56edcc7b19d..5c489fc72e65a7402065a0cd1f85e22e9aa650d7 100644 (file)
@@ -100,10 +100,19 @@ keyaliases['com.example.another.plugin'] = '@com.example.another'
 # generated repo to the server that is it hosted on.  It must end in the
 # standard public repo name of "/fdroid", but can be in up to three levels of
 # sub-directories (i.e. /var/www/packagerepos/fdroid).
-serverwebroot = 'user@example:/var/www/fdroid'
+#serverwebroot = 'user@example:/var/www/fdroid'
+
+# To upload the repo to an Amazon S3 bucket using `fdroid server update`.
+# Warning, this deletes and recreates the whole fdroid/ directory each
+# time. This is based on apache-libcloud, which supports basically all cloud
+# storage services, so it should be easy to port the fdroid server tools to
+# any of them.
+#awsbucket = 'myawsfdroid'
+#awsaccesskeyid = 'SEE0CHAITHEIMAUR2USA'
+#awssecretkey = 'yourverysecretkeywordpassphraserighthere'
 
 # If you want to force 'fdroid server' to use a non-standard serverwebroot
-#nonstandardwebroot = True
+#nonstandardwebroot = False
 
 #Wiki details
 wiki_protocol = "http"
index ed567af8ee27921556ac6ca897c845f94f94ae31..f9db5506dd3229bdba9b0cb9321af2fb6ef79901 100644 (file)
@@ -115,6 +115,13 @@ def read_config(opts, config_file='config.py'):
         if k in config:
             write_password_file(k)
 
+    # since this is used with rsync, where trailing slashes have meaning,
+    # ensure there is always a trailing slash
+    if 'serverwebroot' in config:
+        if config['serverwebroot'][-1] != '/':
+            config['serverwebroot'] += '/'
+        config['serverwebroot'] = config['serverwebroot'].replace('//', '/')
+
     return config
 
 def write_password_file(pwtype, password=None):
@@ -123,7 +130,7 @@ def write_password_file(pwtype, password=None):
     command line argments
     '''
     filename = '.fdroid.' + pwtype + '.txt'
-    fd = os.open(filename, os.O_CREAT | os.O_WRONLY, 0600)
+    fd = os.open(filename, os.O_CREAT | os.O_TRUNC | os.O_WRONLY, 0600)
     if password == None:
         os.write(fd, config[pwtype])
     else:
index 83cbce8246b9f79e94488efedcd1dbe8503d1711..72c759451cad6ee870d55f9f0cae4ffa59f1b68c 100644 (file)
@@ -18,6 +18,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import sys
+import hashlib
 import os
 import subprocess
 from optparse import OptionParser
@@ -27,9 +28,112 @@ import common
 config = None
 options = None
 
+def update_awsbucket(repo_section):
+    '''
+    Upload the contents of the directory `repo_section` (including
+    subdirectories) to the AWS S3 "bucket". The contents of that subdir of the
+    bucket will first be deleted.
 
-def main():
+    Requires AWS credentials set in config.py: awsaccesskeyid, awssecretkey
+    '''
+
+    import libcloud.security
+    libcloud.security.VERIFY_SSL_CERT = True
+    from libcloud.storage.types import Provider, ContainerDoesNotExistError
+    from libcloud.storage.providers import get_driver
+
+    if 'awsaccesskeyid' not in config or 'awssecretkey' not in config:
+        logging.error('To use awsbucket, you must set awssecretkey and awsaccesskeyid in config.py!')
+        sys.exit(1)
+    awsbucket = config['awsbucket']
+
+    cls = get_driver(Provider.S3)
+    driver = cls(config['awsaccesskeyid'], config['awssecretkey'])
+    try:
+        container = driver.get_container(container_name=awsbucket)
+    except ContainerDoesNotExistError:
+        container = driver.create_container(container_name=awsbucket)
+        logging.info('Created new container "' + container.name + '"')
+
+    upload_dir = 'fdroid/' + repo_section
+    objs = dict()
+    for obj in container.list_objects():
+        if obj.name.startswith(upload_dir + '/'):
+            objs[obj.name] = obj
+
+    for root, _, files in os.walk(os.path.join(os.getcwd(), repo_section)):
+        for name in files:
+            upload = False
+            file_to_upload = os.path.join(root, name)
+            object_name = 'fdroid/' + os.path.relpath(file_to_upload, os.getcwd())
+            if not object_name in objs:
+                upload = True
+            else:
+                obj = objs.pop(object_name)
+                if obj.size != os.path.getsize(file_to_upload):
+                    upload = True
+                else:
+                    # if the sizes match, then compare by MD5
+                    md5 = hashlib.md5()
+                    with open(file_to_upload, 'rb') as f:
+                        while True:
+                            data = f.read(8192)
+                            if not data:
+                                break
+                            md5.update(data)
+                    if obj.hash != md5.hexdigest():
+                        s3url = 's3://' + awsbucket + '/' + obj.name
+                        logging.info(' deleting ' + s3url)
+                        if not driver.delete_object(obj):
+                            logging.warn('Could not delete ' + s3url)
+                        upload = True
+
+            if upload:
+                if options.verbose:
+                    logging.info(' uploading "' + file_to_upload + '"...')
+                extra = { 'acl': 'public-read' }
+                if file_to_upload.endswith('.sig'):
+                    extra['content_type'] = 'application/pgp-signature'
+                elif file_to_upload.endswith('.asc'):
+                    extra['content_type'] = 'application/pgp-signature'
+                logging.info(' uploading ' + os.path.relpath(file_to_upload)
+                             + ' to s3://' + awsbucket + '/' + obj.name)
+                obj = driver.upload_object(file_path=file_to_upload,
+                                           container=container,
+                                           object_name=object_name,
+                                           verify_hash=False,
+                                           extra=extra)
+    # delete the remnants in the bucket, they do not exist locally
+    while objs:
+        object_name, obj = objs.popitem()
+        s3url = 's3://' + awsbucket + '/' + object_name
+        if object_name.startswith(upload_dir):
+            logging.warn(' deleting ' + s3url)
+            driver.delete_object(obj)
+        else:
+            logging.info(' skipping ' + s3url)
 
+def update_serverwebroot(repo_section):
+    rsyncargs = ['rsync', '-u', '-r', '--delete']
+    if options.verbose:
+        rsyncargs += ['--verbose']
+    if options.quiet:
+        rsyncargs += ['--quiet']
+    index = os.path.join(repo_section, 'index.xml')
+    indexjar = os.path.join(repo_section, 'index.jar')
+    # serverwebroot is guaranteed to have a trailing slash in common.py
+    if subprocess.call(rsyncargs +
+                       ['--exclude', index, '--exclude', indexjar,
+                        repo_section, config['serverwebroot']]) != 0:
+        sys.exit(1)
+    if subprocess.call(rsyncargs +
+                       [index, config['serverwebroot'] + repo_section]) != 0:
+        sys.exit(1)
+    if subprocess.call(rsyncargs +
+                       [indexjar, config['serverwebroot'] + repo_section]) != 0:
+        sys.exit(1)
+
+def main():
     global config, options
 
     # Parse command line...
@@ -50,44 +154,48 @@ def main():
         logging.critical("The only commands currently supported are 'init' and 'update'")
         sys.exit(1)
 
-    serverwebroot = config['serverwebroot'].rstrip('/').replace('//', '/')
-    host, fdroiddir = serverwebroot.split(':')
-    serverrepobase = os.path.basename(fdroiddir)
     if 'nonstandardwebroot' in config and config['nonstandardwebroot'] == True:
         standardwebroot = False
     else:
         standardwebroot = True
-    if serverrepobase != 'fdroid' and standardwebroot:
-        print('ERROR: serverwebroot does not end with "fdroid", '
-              + 'perhaps you meant one of these:\n\t'
-              + serverwebroot.rstrip('/') + '/fdroid\n\t'
-              + serverwebroot.rstrip('/').rstrip(serverrepobase) + 'fdroid')
+
+    if 'serverwebroot' in config:
+        serverwebroot = config['serverwebroot']
+        host, fdroiddir = serverwebroot.rstrip('/').split(':')
+        serverrepobase = os.path.basename(fdroiddir)
+        if serverrepobase != 'fdroid' and standardwebroot:
+            logging.error('serverwebroot does not end with "fdroid", '
+                          + 'perhaps you meant one of these:\n\t'
+                          + serverwebroot.rstrip('/') + '/fdroid\n\t'
+                          + serverwebroot.rstrip('/').rstrip(serverrepobase) + 'fdroid')
+            sys.exit(1)
+    elif 'awsbucket' not in config:
+        logging.warn('No serverwebroot or awsbucket set! Edit your config.py to set one or both.')
         sys.exit(1)
 
-    repodirs = ['repo']
+    repo_sections = ['repo']
     if config['archive_older'] != 0:
-        repodirs.append('archive')
-
-    for repodir in repodirs:
-        if args[0] == 'init':
-            if subprocess.call(['ssh', '-v', host,
-                                'mkdir -p', fdroiddir + '/' + repodir]) != 0:
-                sys.exit(1)
-        elif args[0] == 'update':
-            index = os.path.join(repodir, 'index.xml')
-            indexjar = os.path.join(repodir, 'index.jar')
-            if subprocess.call(['rsync', '-u', '-v', '-r', '--delete',
-                                '--exclude', index, '--exclude', indexjar,
-                                repodir, config['serverwebroot']]) != 0:
-                sys.exit(1)
-            if subprocess.call(['rsync', '-u', '-v', '-r', '--delete',
-                                index,
-                                config['serverwebroot'] + '/' + repodir]) != 0:
-                sys.exit(1)
-            if subprocess.call(['rsync', '-u', '-v', '-r', '--delete',
-                                indexjar,
-                                config['serverwebroot'] + '/' + repodir]) != 0:
-                sys.exit(1)
+        repo_sections.append('archive')
+
+    if args[0] == 'init':
+        if serverwebroot != None:
+            sshargs = ['ssh']
+            if options.quiet:
+                sshargs += ['-q']
+            for repo_section in repo_sections:
+                cmd = sshargs + [host, 'mkdir -p', fdroiddir + '/' + repo_section]
+                if options.verbose:
+                    # ssh -v produces different output than rsync -v, so this
+                    # simulates rsync -v
+                    logging.info(' '.join(cmd))
+                if subprocess.call(cmd) != 0:
+                    sys.exit(1)
+    elif args[0] == 'update':
+        for repo_section in repo_sections:
+            if 'serverwebroot' in config:
+                update_serverwebroot(repo_section)
+            if 'awsbucket' in config:
+                update_awsbucket(repo_section)
 
     sys.exit(0)
 
index b2d490b2581e847898765307277debe005bc80ee..53c999a2f1194cfc1a7e7bb608e918ed7391a26f 100644 (file)
@@ -645,7 +645,10 @@ def make_index(apps, apks, repodir, archive, categories):
                                   '-storepass:file', config['keystorepassfile']]
                             + config['smartcardoptions'])
             if p.returncode != 0:
-                logging.critical("Failed to get repo pubkey")
+                msg = "Failed to get repo pubkey!"
+                if config['keystore'] == 'NONE':
+                    msg += ' Is your crypto smartcard plugged in?'
+                logging.critical(msg)
                 sys.exit(1)
             global repo_pubkey_fingerprint
             repo_pubkey_fingerprint = cert_fingerprint(p.stdout)
index 7cc2b4be42cb7063e56760ecc322046800848e6c..dbdea777fc849bc03f7c63114499e7ad5a759d8d 100755 (executable)
@@ -25,6 +25,14 @@ if [ -z $ANDROID_HOME ]; then
     fi
 fi
 
+
+#------------------------------------------------------------------------------#
+# cache pypi downloads
+if [ -z $PIP_DOWNLOAD_CACHE ]; then
+    export PIP_DOWNLOAD_CACHE=$HOME/.pip_download_cache
+fi
+
+
 #------------------------------------------------------------------------------#
 # required Java 7 keytool/jarsigner for :file support
 
index 35ecc8e5e6eb6f369a833553afe51315824db08b..b4dd7e5e86cee5c4ab2e79104c933140a2b3ed28 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -32,6 +32,7 @@ setup(name='fdroidserver',
         'paramiko',
         'PIL',
         'python-magic',
+        'apache-libcloud >= 0.14.1',
         ],
       classifiers=[
         'Development Status :: 3 - Alpha',