1
0
mirror of https://gitlab.com/fdroid/fdroidserver.git synced 2024-11-10 17:30:11 +01:00

Merge branch 'switching-s3cmd-for-rclone' into 'master'

Adding rclone as an option to fdroid deploy

Closes #1095

See merge request fdroid/fdroidserver!1095
This commit is contained in:
Hans-Christoph Steiner 2024-05-29 14:08:07 +00:00
commit f50e5a806d
3 changed files with 255 additions and 7 deletions

View File

@ -267,10 +267,20 @@
# sync_from_local_copy_dir: true # sync_from_local_copy_dir: true
# To upload the repo to an Amazon S3 bucket using `fdroid server # To upload the repo to an Amazon S3 bucket using `fdroid deploy'
# update`. Warning, this deletes and recreates the whole fdroid/ # . rclone, s3cmd and apache libcloud are the available options.
# directory each time. This prefers s3cmd, but can also use # If rclone and s3cmd are not installed, apache libcloud is used.
# apache-libcloud. To customize how s3cmd interacts with the cloud # To use apache libcloud, add the following options to this file
# (config.yml)
#
# awsbucket: myawsfdroid
# awsaccesskeyid: SEE0CHAITHEIMAUR2USA
# awssecretkey: {env: awssecretkey}
#
# In case s3cmd is installed and rclone is not installed,
# s3cmd will be the preferred sync option.
# It will delete and recreate the whole fdroid directory each time.
# To customize how s3cmd interacts with the cloud
# provider, create a 's3cfg' file next to this file (config.yml), and # provider, create a 's3cfg' file next to this file (config.yml), and
# those settings will be used instead of any 'aws' variable below. # those settings will be used instead of any 'aws' variable below.
# Secrets can be fetched from environment variables to ensure that # Secrets can be fetched from environment variables to ensure that
@ -279,6 +289,47 @@
# awsbucket: myawsfdroid # awsbucket: myawsfdroid
# awsaccesskeyid: SEE0CHAITHEIMAUR2USA # awsaccesskeyid: SEE0CHAITHEIMAUR2USA
# awssecretkey: {env: awssecretkey} # awssecretkey: {env: awssecretkey}
#
# In case rclone is installed and s3cmd is not installed,
# rclone will be the preferred sync option.
# It will sync the local folders with remote folders without
# deleting anything in one go.
# To ensure success, install rclone as per
# the instructions at https://rclone.org/install/ and also configure for
# object storage services as detailed at https://rclone.org/s3/#configuration
# By default rclone uses the configuration file at ~/.config/rclone/rclone.conf
# To specify a custom configuration file, please add the full path to the
# configuration file as below
#
# path_to_custom_rclone_config: /home/mycomputer/somedir/example.conf
#
# This setting will ignore the default rclone config found at
# ~/.config/rclone/rclone.conf
#
# Please note that rclone_config can be assigned a string or list
#
# awsbucket: myawsfdroid
# rclone_config: aws-sample-config
#
# or
#
# awsbucket: myawsfdroid
# rclone_config: [aws-sample-config, rclone-supported-service-config]
#
# In case both rclone and s3cmd are installed, the preferred sync
# tool can be specified in this file (config.yml)
# if s3cmd is preferred, set it as below
#
# s3cmd: true
#
# if rclone is preferred, set it as below
#
# rclone: true
#
# Please note that only one can be set to true at any time
# Also, in the event that both s3cmd and rclone are installed
# and both are missing from the config.yml file, the preferred
# tool will be s3cmd.
# If you want to force 'fdroid server' to use a non-standard serverwebroot. # If you want to force 'fdroid server' to use a non-standard serverwebroot.

View File

@ -28,6 +28,7 @@ import urllib
import yaml import yaml
from argparse import ArgumentParser from argparse import ArgumentParser
import logging import logging
from shlex import split
import shutil import shutil
from . import _ from . import _
@ -44,6 +45,7 @@ BINARY_TRANSPARENCY_DIR = 'binary_transparency'
AUTO_S3CFG = '.fdroid-deploy-s3cfg' AUTO_S3CFG = '.fdroid-deploy-s3cfg'
USER_S3CFG = 's3cfg' USER_S3CFG = 's3cfg'
USER_RCLONE_CONF = None
REMOTE_HOSTNAME_REGEX = re.compile(r'\W*\w+\W+(\w+).*') REMOTE_HOSTNAME_REGEX = re.compile(r'\W*\w+\W+(\w+).*')
INDEX_FILES = [ INDEX_FILES = [
@ -89,7 +91,7 @@ def _get_index_excludes(repo_section):
return index_excludes return index_excludes
def update_awsbucket(repo_section): def update_awsbucket(repo_section, verbose=False, quiet=False):
"""Upload the contents of the directory `repo_section` (including subdirectories) to the AWS S3 "bucket". """Upload the contents of the directory `repo_section` (including subdirectories) to the AWS S3 "bucket".
The contents of that subdir of the The contents of that subdir of the
@ -101,8 +103,29 @@ def update_awsbucket(repo_section):
f'''Syncing "{repo_section}" to Amazon S3 bucket "{config['awsbucket']}"''' f'''Syncing "{repo_section}" to Amazon S3 bucket "{config['awsbucket']}"'''
) )
if common.set_command_in_config('s3cmd'): if common.set_command_in_config('s3cmd') and common.set_command_in_config('rclone'):
logging.info(
'Both rclone and s3cmd are installed. Checking config.yml for preference.'
)
if config['s3cmd'] is not True and config['rclone'] is not True:
logging.warning(
'No syncing tool set in config.yml!. Defaulting to using s3cmd'
)
update_awsbucket_s3cmd(repo_section) update_awsbucket_s3cmd(repo_section)
if config['s3cmd'] is True and config['rclone'] is True:
logging.warning(
'Both syncing tools set in config.yml!. Defaulting to using s3cmd'
)
update_awsbucket_s3cmd(repo_section)
if config['s3cmd'] is True and config['rclone'] is not True:
update_awsbucket_s3cmd(repo_section)
if config['rclone'] is True and config['s3cmd'] is not True:
update_remote_storage_with_rclone(repo_section, verbose, quiet)
elif common.set_command_in_config('s3cmd'):
update_awsbucket_s3cmd(repo_section)
elif common.set_command_in_config('rclone'):
update_remote_storage_with_rclone(repo_section, verbose, quiet)
else: else:
update_awsbucket_libcloud(repo_section) update_awsbucket_libcloud(repo_section)
@ -186,6 +209,129 @@ def update_awsbucket_s3cmd(repo_section):
raise FDroidException() raise FDroidException()
def update_remote_storage_with_rclone(repo_section, verbose=False, quiet=False):
"""
Upload fdroid repo folder to remote storage using rclone sync.
Rclone sync can send the files to any supported remote storage
service once without numerous polling.
If remote storage is s3 e.g aws s3, wasabi, filebase then path will be
bucket_name/fdroid/repo where bucket_name will be an s3 bucket
If remote storage is storage drive/sftp e.g google drive, rsync.net
the new path will be bucket_name/fdroid/repo where bucket_name
will be a folder
Better than the s3cmd command as it does the syncing in one command
Check https://rclone.org/docs/#config-config-file (optional config file)
"""
logging.debug(_('Using rclone to sync with: {url}').format(url=config['awsbucket']))
if config.get('path_to_custom_rclone_config') is not None:
USER_RCLONE_CONF = config['path_to_custom_rclone_config']
if os.path.exists(USER_RCLONE_CONF):
logging.info("'path_to_custom_rclone_config' found in config.yml")
logging.info(
_('Using "{path}" for syncing with remote storage.').format(
path=USER_RCLONE_CONF
)
)
configfilename = USER_RCLONE_CONF
else:
logging.info('Custom configuration not found.')
logging.info(
'Using default configuration at {}'.format(
subprocess.check_output('rclone config file')
)
)
configfilename = None
else:
logging.warning("'path_to_custom_rclone_config' not found in config.yml")
logging.info('Custom configuration not found.')
logging.info(
'Using default configuration at {}'.format(
subprocess.check_output('rclone config file')
)
)
configfilename = None
upload_dir = 'fdroid/' + repo_section
if not config.get('rclone_config') or not config.get('awsbucket'):
raise FDroidException(
_('To use rclone, rclone_config and awsbucket must be set in config.yml!')
)
if isinstance(config['rclone_config'], str):
rclone_sync_command = (
'rclone sync '
+ repo_section
+ ' '
+ config['rclone_config']
+ ':'
+ config['awsbucket']
+ '/'
+ upload_dir
)
rclone_sync_command = split(rclone_sync_command)
if verbose:
rclone_sync_command += ['--verbose']
elif quiet:
rclone_sync_command += ['--quiet']
if configfilename:
rclone_sync_command += split('--config=' + configfilename)
complete_remote_path = (
config['rclone_config'] + ':' + config['awsbucket'] + '/' + upload_dir
)
logging.debug(
"rclone sync all files in " + repo_section + ' to ' + complete_remote_path
)
if subprocess.call(rclone_sync_command) != 0:
raise FDroidException()
if isinstance(config['rclone_config'], list):
for remote_config in config['rclone_config']:
rclone_sync_command = (
'rclone sync '
+ repo_section
+ ' '
+ remote_config
+ ':'
+ config['awsbucket']
+ '/'
+ upload_dir
)
rclone_sync_command = split(rclone_sync_command)
if verbose:
rclone_sync_command += ['--verbose']
elif quiet:
rclone_sync_command += ['--quiet']
if configfilename:
rclone_sync_command += split('--config=' + configfilename)
complete_remote_path = (
remote_config + ':' + config['awsbucket'] + '/' + upload_dir
)
logging.debug(
"rclone sync all files in "
+ repo_section
+ ' to '
+ complete_remote_path
)
if subprocess.call(rclone_sync_command) != 0:
raise FDroidException()
def update_awsbucket_libcloud(repo_section): def update_awsbucket_libcloud(repo_section):
"""No summary. """No summary.
@ -967,7 +1113,7 @@ def main():
# update_servergitmirrors will take care of multiple mirrors so don't need a foreach # update_servergitmirrors will take care of multiple mirrors so don't need a foreach
update_servergitmirrors(config['servergitmirrors'], repo_section) update_servergitmirrors(config['servergitmirrors'], repo_section)
if config.get('awsbucket'): if config.get('awsbucket'):
update_awsbucket(repo_section) update_awsbucket(repo_section, options.verbose, options.quiet)
if config.get('androidobservatory'): if config.get('androidobservatory'):
upload_to_android_observatory(repo_section) upload_to_android_observatory(repo_section)
if config.get('virustotal_apikey'): if config.get('virustotal_apikey'):

View File

@ -1,8 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import configparser
import inspect import inspect
import logging import logging
import os import os
import shutil
import sys import sys
import tempfile import tempfile
import unittest import unittest
@ -21,6 +23,11 @@ from fdroidserver.exception import FDroidException
from testcommon import TmpCwd, mkdtemp, parse_args_for_test from testcommon import TmpCwd, mkdtemp, parse_args_for_test
class Options:
quiet = False
verbose = False
class DeployTest(unittest.TestCase): class DeployTest(unittest.TestCase):
'''fdroidserver/deploy.py''' '''fdroidserver/deploy.py'''
@ -31,6 +38,10 @@ class DeployTest(unittest.TestCase):
self._td = mkdtemp() self._td = mkdtemp()
self.testdir = self._td.name self.testdir = self._td.name
fdroidserver.deploy.options = mock.Mock()
fdroidserver.deploy.config = {}
fdroidserver.deploy.USER_RCLONE_CONF = False
def tearDown(self): def tearDown(self):
self._td.cleanup() self._td.cleanup()
@ -87,6 +98,44 @@ class DeployTest(unittest.TestCase):
with self.assertRaises(SystemExit): with self.assertRaises(SystemExit):
fdroidserver.deploy.update_serverwebroots([{'url': 'ssh://nope'}], 'repo') fdroidserver.deploy.update_serverwebroots([{'url': 'ssh://nope'}], 'repo')
@unittest.skipUnless(shutil.which('rclone'), '/usr/bin/rclone')
def test_update_remote_storage_with_rclone(self):
os.chdir(self.testdir)
repo = Path('repo')
repo.mkdir(parents=True, exist_ok=True)
fake_apk = repo / 'another_fake.apk'
with fake_apk.open('w') as fp:
fp.write('not an APK, but has the right filename')
# write out rclone config for test use
rclone_config = configparser.ConfigParser()
rclone_config.add_section("test-local-config")
rclone_config.set("test-local-config", "type", "local")
rclone_config_path = Path('rclone_config_path')
rclone_config_path.mkdir(parents=True, exist_ok=True)
rclone_file = rclone_config_path / 'rclone.conf'
with open(rclone_file, 'w') as configfile:
rclone_config.write(configfile)
# setup parameters for this test run
fdroidserver.deploy.config['awsbucket'] = 'test_bucket_folder'
fdroidserver.deploy.config['rclone'] = True
fdroidserver.deploy.config['rclone_config'] = 'test-local-config'
fdroidserver.deploy.config['path_to_custom_rclone_config'] = str(rclone_file)
fdroidserver.deploy.options = Options
# write out destination path
destination = Path('some_bucket_folder/fdroid')
destination.mkdir(parents=True, exist_ok=True)
dest_path = Path(destination) / fake_apk
self.assertFalse(dest_path.is_file())
repo_section = str(repo)
# fdroidserver.deploy.USER_RCLONE_CONF = str(rclone_file)
fdroidserver.deploy.update_remote_storage_with_rclone(repo_section)
self.assertFalse(dest_path.is_file())
def test_update_serverwebroot(self): def test_update_serverwebroot(self):
"""rsync works with file paths, so this test uses paths for the URLs""" """rsync works with file paths, so this test uses paths for the URLs"""
os.chdir(self.testdir) os.chdir(self.testdir)
@ -100,6 +149,8 @@ class DeployTest(unittest.TestCase):
dest_apk = url / fake_apk dest_apk = url / fake_apk
self.assertFalse(dest_apk.is_file()) self.assertFalse(dest_apk.is_file())
fdroidserver.deploy.options = mock.Mock()
fdroidserver.deploy.options.identity_file = None
fdroidserver.deploy.update_serverwebroot({'url': str(url)}, 'repo') fdroidserver.deploy.update_serverwebroot({'url': str(url)}, 'repo')
self.assertTrue(dest_apk.is_file()) self.assertTrue(dest_apk.is_file())