From 000d87e7318395079a6c92bf1aa7b8f91b2171e2 Mon Sep 17 00:00:00 2001 From: Andre Anjos <andre.dos.anjos@gmail.com> Date: Tue, 13 Aug 2019 14:36:06 +0200 Subject: [PATCH] [dav] Implement easy WebDAV access --- bob/devtools/dav.py | 57 +++++++ bob/devtools/scripts/dav.py | 301 ++++++++++++++++++++++++++++++++++++ setup.py | 9 ++ 3 files changed, 367 insertions(+) create mode 100644 bob/devtools/dav.py create mode 100644 bob/devtools/scripts/dav.py diff --git a/bob/devtools/dav.py b/bob/devtools/dav.py new file mode 100644 index 00000000..cd5c3d44 --- /dev/null +++ b/bob/devtools/dav.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import configparser + +from .log import get_logger +from .deploy import _setup_webdav_client + +logger = get_logger(__name__) + + +def _get_config(): + """Returns a dictionary with server parameters, or ask them to the user""" + + # tries to figure if we can authenticate using a global configuration + cfgs = ["~/.bdt-webdav.cfg"] + cfgs = [os.path.expanduser(k) for k in cfgs] + for k in cfgs: + if os.path.exists(k): + data = configparser.ConfigParser() + data.read(k) + if 'global' not in data or \ + 'server' not in data['global'] or \ + 'username' not in data['global'] or \ + 'password' not in data['global']: + assert KeyError, 'The file %s should contain a single ' \ + '"global" section with 3 variables defined inside: ' \ + '"server", "username", "password".' % (k,) + return data['global'] + + # ask the user for the information, cache credentials for future use + retval = dict() + retval['server'] = input("The base address of the server: ") + retval['username'] = input("Username: ") + retval['password'] = input("Password: ") + + # record file for the user + data = configparser.ConfigParser() + data['global'] = retval + with open(cfgs[0], 'w') as f: + logger.warn('Recorded "%s" configuration file for next queries') + data.write(f) + os.chmod(cfgs[0], 0o600) + logger.warn('Changed mode of "%s" to be read-only to you') + + return retval + + +def setup_webdav_client(private): + """Returns a ready-to-use WebDAV client""" + + config = _get_config() + root = '/private-upload' if private else '/public-upload' + c = _setup_webdav_client(config['server'], root, config['username'], + config['password']) + return c diff --git a/bob/devtools/scripts/dav.py b/bob/devtools/scripts/dav.py new file mode 100644 index 00000000..cb66d5b7 --- /dev/null +++ b/bob/devtools/scripts/dav.py @@ -0,0 +1,301 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import sys + +import click +import pkg_resources +from click_plugins import with_plugins + +from . import bdt + +from ..dav import setup_webdav_client +from ..log import verbosity_option, get_logger, echo_normal, echo_info, \ + echo_warning + +logger = get_logger(__name__) + + +@with_plugins(pkg_resources.iter_entry_points("bdt.dav.cli")) +@click.group(cls=bdt.AliasedGroup) +def dav(): + """Commands for reading/listing/renaming/copying content to a WebDAV server + + Commands defined here may require a username and a password to operate + properly. + """ + pass + + +@dav.command( + epilog=""" +Examples: + + 1. List contents of 'public': + + $ bdt dav -vv list + + + 2. List contents of 'public/databases/latest': + + $ bdt dav -vv list databases/latest + + + 3. List contents of 'private/docs': + + $ bdt dav -vv list -p docs + +""" +) +@click.option( + "-p", + "--private/--no-private", + default=False, + help="If set, use the 'private' area instead of the public one", +) +@click.option( + "-l", + "--long-format/--no-long-format", + default=False, + help="If set, print details about each listed file", +) +@click.argument( + "path", + default="/", + required=False, +) +@verbosity_option() +@bdt.raise_on_error +def list(private, long_format, path): + """List the contents of a given WebDAV directory. + """ + + if not path.startswith('/'): path = '/' + path + cl = setup_webdav_client(private) + contents = cl.list(path) + remote_path = cl.get_url(path) + echo_info('ls %s' % (remote_path,)) + for k in contents: + if long_format: + info = cl.info('/'.join((path, k))) + echo_normal('%-20s %-10s %s' % (info['created'], info['size'], k)) + else: + echo_normal(k) + + +@dav.command( + epilog=""" +Examples: + + 1. Creates directory 'foo/bar' on the remote server: + + $ bdt dav -vv mkdir foo/bar + +""" +) +@click.option( + "-p", + "--private/--no-private", + default=False, + help="If set, use the 'private' area instead of the public one", +) +@click.argument( + "path", + required=True, +) +@verbosity_option() +@bdt.raise_on_error +def makedirs(private, path): + """Creates a given directory, recursively (if necessary) + + Gracefully exists if the directory is already there. + """ + + if not path.startswith('/'): path = '/' + path + cl = setup_webdav_client(private) + remote_path = cl.get_url(path) + + if cl.check(path): + echo_warning('directory %s already exists' % (remote_path,)) + + rpath = '' + for k in path.split('/'): + rpath = '/'.join((rpath, k)) if rpath else k + if not cl.check(rpath): + echo_info('mkdir %s' % (rpath,)) + cl.mkdir(rpath) + + +@dav.command( + epilog=""" +Examples: + + 1. Removes (recursively), everything under the 'remote/path/foo/bar' path: + + $ bdt dav -vv rmtree remote/path/foo/bar + + Notice this does not do anything for security. It just displays what it + would do. To actually run the rmtree comment pass the --execute flag (or + -x) + + + 2. Realy removes (recursively), everything under the 'remote/path/foo/bar' + path: + + $ bdt dav -vv rmtree --execute remote/path/foo/bar + + +""" +) +@click.option( + "-p", + "--private/--no-private", + default=False, + help="If set, use the 'private' area instead of the public one", +) +@click.option( + "-x", + "--execute/--no-execute", + default=False, + help="If this flag is set, then execute the removal", +) +@click.argument( + "path", + required=True, +) +@verbosity_option() +@bdt.raise_on_error +def rmtree(private, execute, path): + """Removes a whole directory tree from the WebDAV server + + ATTENTION: There is no undo! Use --execute to execute. + """ + + if not execute: + echo_warning("!!!! DRY RUN MODE !!!!") + echo_warning("Nothing is being executed on server. Use -x to execute.") + + if not path.startswith('/'): path = '/' + path + cl = setup_webdav_client(private) + remote_path = cl.get_url(path) + + if not cl.check(path): + echo_warning('resource %s does not exist' % (remote_path,)) + return + + echo_info('rm -rf %s' % (remote_path,)) + if execute: + cl.clean(path) + + +@dav.command( + epilog=""" +Examples: + + 1. Uploads a single file to a specific location: + + $ bdt dav -vv copy local/file remote + + + 2. Uploads various resources at once: + + $ bdt dav -vv copy local/file1 local/dir local/file2 remote + +""" +) +@click.option( + "-p", + "--private/--no-private", + default=False, + help="If set, use the 'private' area instead of the public one", +) +@click.option( + "-x", + "--execute/--no-execute", + default=False, + help="If this flag is set, then execute the removal", +) +@click.argument( + "local", + required=True, + type=click.Path(file_okay=True, dir_okay=True, exists=True), + nargs=-1, +) +@click.argument( + "remote", + required=True, +) +@verbosity_option() +@bdt.raise_on_error +def upload(private, execute, local, remote): + """Uploads a local resource (file or directory) to a remote destination + + If the local resource is a directory, it is uploaded recursively. If the + remote resource with the same name already exists, an error is raised (use + rmtree to remove it first). + + If the remote location does not exist, it is an error as well. As a + consequence, you cannot change the name of the resource being uploaded with + this command. + + ATTENTION: There is no undo! Use --execute to execute. + """ + + if not execute: + echo_warning("!!!! DRY RUN MODE !!!!") + echo_warning("Nothing is being executed on server. Use -x to execute.") + + if not remote.startswith('/'): remote = '/' + remote + cl = setup_webdav_client(private) + + if not cl.check(remote): + echo_warning('base remote directory for upload %s does not exist' % + (remote,)) + return 1 + + for k in local: + actual_remote = remote + os.path.basename(k) + remote_path = cl.get_url(actual_remote) + + if cl.check(actual_remote): + echo_warning('resource %s already exists' % (remote_path,)) + echo_warning('remove it first before uploading a new copy') + continue + + if os.path.isdir(k): + echo_info('cp -r %s %s' % (k, remote_path)) + if execute: + cl.upload_directory(local_path=k, remote_path=actual_remote) + else: + echo_info('cp %s %s' % (k, remote_path)) + if execute: + cl.upload_file(local_path=k, remote_path=actual_remote) + + +@dav.command( + epilog=""" +Examples: + + 1. Lists the amount of free disk space on the WebDAV server: + + $ bdt dav -vv free + +""" +) +@click.option( + "-p", + "--private/--no-private", + default=False, + help="If set, use the 'private' area instead of the public one", +) +@verbosity_option() +@bdt.raise_on_error +def free(private): + """Lists the amount of free space on the webserver disk + """ + + cl = setup_webdav_client(private) + echo_info('free') + data = cl.free() + echo_normal(data) diff --git a/setup.py b/setup.py index 995048cb..4d825c58 100644 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ setup( 'test = bob.devtools.scripts.test:test', 'caupdate = bob.devtools.scripts.caupdate:caupdate', 'ci = bob.devtools.scripts.ci:ci', + 'dav = bob.devtools.scripts.dav:dav', 'local = bob.devtools.scripts.local:local', 'gitlab = bob.devtools.scripts.gitlab:gitlab', ], @@ -86,6 +87,14 @@ setup( 'base-build = bob.devtools.scripts.local:base_build', ], + 'bdt.dav.cli': [ + 'list = bob.devtools.scripts.dav:list', + 'makedirs = bob.devtools.scripts.dav:makedirs', + 'rmtree = bob.devtools.scripts.dav:rmtree', + 'upload = bob.devtools.scripts.dav:upload', + #'free = bob.devtools.scripts.dav:free', + ], + }, classifiers=[ 'Framework :: Bob', -- GitLab