Gitlab Community Edition Instance

Commit 9998974c authored by Marcel Hellkamp's avatar Marcel Hellkamp
Browse files

Work on cli commands.

parent cb666a15
......@@ -8,12 +8,7 @@ import sys
__ALL__ = ["main", "register_subcommand", "printer"]
#: Commands to load automatically. The module pycdstar.cli.NAME must have register() defined.
BUILDIN_COMMANDS = {"init", "put"}
#: Maps a subcommand name to a callable
__subcommands = dict()
#: Subcommands that only take a single argument and no cdstar instance
__subcommands_noconfig = set()
BUILDIN_COMMANDS = {"init", "put", "get", "acl"}
parser = argparse.ArgumentParser(description='CDSTAR command-line client')
parser.add_argument("-C", default=".", help="Change the current working directory before executing the command")
......@@ -25,19 +20,9 @@ _grp.add_argument("-q", "--quiet", action="store_true", help="Be quiet. Only pri
subparsers = parser.add_subparsers(help='sub-command help')
def register_subcommand(name, command, parser_builder=None, aliases=None, help=None, description=None, noconfig=False):
def register_subcommand(name, aliases=None, help=None, description=None):
""" Register a sub-command and return an argparse parser for command-specific parameters. """
if name in __subcommands:
raise ImportError("Subcommand {} registered twice".format(name))
__subcommands[name] = command
if noconfig:
__subcommands_noconfig.add(name)
subparser = subparsers.add_parser(name, aliases=aliases or [], help=help, description=description)
if parser_builder:
parser_builder(subparser)
subparser.set_defaults(subcommand=name)
return subparser
return subparsers.add_parser(name, aliases=aliases or [], help=help, description=description)
class Printer:
......@@ -73,14 +58,14 @@ class Printer:
def error(self, msg, *args, **kwargs):
""" Print an error message (if not quiet) and optionally (-vv or higher) a stacktrace."""
self(msg.format(*args), file=self.file, **kwargs)
print("ERROR: " + msg.format(*args), file=self.file, **kwargs)
if self.verbosity >= 2:
import traceback
traceback.print_exc(file=self.file)
def fatal(self, msg, *args, **kwargs):
""" Print an error message (even if quiet) and optionally (-vv or higher) a stacktrace."""
print(msg.format(*args), file=self.file, **kwargs)
print("FATAL: " + msg.format(*args), file=self.file, **kwargs)
if self.verbosity >= 2:
import traceback
traceback.print_exc(file=self.file)
......@@ -111,23 +96,20 @@ def main(args=None):
printer.v("Changing working directory to: {}", opts.C)
os.chdir(opts.C)
if not hasattr(opts, "subcommand"):
if not hasattr(opts, "main"):
parser.print_help()
return 1
cmd = opts.subcommand
cmdmain = __subcommands[cmd]
try:
ctx = CliContext(workdir=".")
if opts.config:
ctx.load_config(opts.config)
return cmdmain(ctx, opts) or 0
return opts.main(ctx, opts) or 0
except KeyboardInterrupt:
printer("Exiting...")
return 0
except CliError as e:
printer.fatal(*e.args)
printer.error(str(e))
return e.return_code
except Exception as e:
printer.fatal("Uncaught exception ({}). Exiting...", e)
......
"""
Manage access control lists (ACL)
"""
import os
from ...cdstar import FormUpdate
from .. import register_subcommand, printer, CliError
def register():
parser = register_subcommand("acl", help=__doc__.splitlines()[0], description=__doc__)
sub = parser.add_subparsers()
pset = sub.add_parser("set")
pset.add_argument("TARGET", help="Archive ID")
pset.add_argument("SUBJECT", help="Subject do modify")
pset.add_argument("PERMISSIONS", nargs="*", help="Permissions to set for this subject.")
pset.set_defaults(main=acl_set)
def acl_set(ctx, args):
client, vault, archive, rfile = ctx.resolve(args.TARGET)
subject = args.SUBJECT
permissions = args.PERMISSIONS
printer("Set ACL for {!r} on /{}/{} -> [{}]", subject, vault, archive, ', '.join(permissions))
with client.begin(autocommit=True):
update = FormUpdate()
update.acl(subject, *permissions)
client.update_archive(vault, archive, form=update)
printer("Done")
"""
Get (download) a single file from an archive
"""
import os
import sys
from .._utils import hbytes
from .. import register_subcommand, printer, CliError
def register():
parser = register_subcommand("get", help=__doc__.splitlines()[0], description=__doc__)
parser.add_argument("-%", "-p", "--progress", action="store_true",
help="Show progress bar for large files or slow downloads")
parser.add_argument("--resume", action="store_true",
help="If a <DST>.part file exists, try to resume an interrupted download."
"Also, keep the *.part file on any errors.")
parser.add_argument("-f", "--force", action="store_true", help="Overwrite existing files without asking")
parser.add_argument("SRC", help="File identifier")
parser.add_argument("DST", nargs="?", help="Destination filename, directory or '-' for stdout. (default '-')")
parser.set_defaults(main=get)
def get(ctx, args):
client, vault, archive, file = ctx.resolve(args.SRC)
resume = args.resume
progress = args.progress
dst = args.DST or '-'
force = args.force
if not file:
raise CliError("The <SRC> parameter must reference a file, not an archive")
if dst.endswith("/") or os.path.isdir(dst):
dst = os.path.join(dst, os.path.basename(file))
ispipe = dst == '-'
partfile = dst + ".part" if not ispipe else None
offset = 0
if not ispipe and not force and os.path.exists(dst):
raise CliError("File exists: " + dst)
if partfile and os.path.exists(partfile):
if resume:
offset = os.path.getsize(partfile)
printer("Resuming download at: {}", hbytes(offset))
else:
raise CliError("Found partial download, but --resume is not set: " + partfile)
if ispipe:
out = sys.stdout.buffer
else:
out = open(partfile, 'ab')
write = out.write
close = out.close
try:
rs = client.get_file(vault, archive, file, offset=offset)
size = int(rs.headers["Content-Length"])
if ispipe and out.isatty() and not rs.headers["Content-Type"].startswith("text/"):
raise CliError("Not printing binary data ({}) a terminal".format(rs.headers["Content-Type"]))
if progress and not printer.quiet:
from tqdm import tqdm
pbar = tqdm(total=size+offset, initial=offset, unit='b', unit_scale=True, unit_divisor=1024, dynamic_ncols=True,
file=printer.file)
def write(chunk):
pbar.update(len(chunk))
out.write(chunk)
def close():
pbar.close()
out.close()
printer.v("Downloading {!r} from /{}/{} ({})", file, vault, archive, hbytes(size + offset))
for chunk in rs.iter_content(chunk_size=64*1024):
write(chunk)
close()
if not ispipe:
if force:
os.replace(partfile, dst)
else:
os.rename(partfile, dst)
printer.v("Done!", file, vault, archive, hbytes(size + offset))
except (KeyboardInterrupt, Exception) as e:
if close:
close()
if partfile and os.path.exists(partfile) and not resume:
try:
os.remove(partfile)
except OSError:
printer("Failed to delete partial download: {}", partfile)
raise
raise
......@@ -15,13 +15,15 @@ import re
from .. import register_subcommand, printer
from ..context import CONFIG_NAMES
def register():
parser = register_subcommand("init", command, help=__doc__.splitlines()[0], description=__doc__, noconfig=True)
parser = register_subcommand("init", help=__doc__.splitlines()[0], description=__doc__)
parser.add_argument("--server", help="CDSTAR server URI (usually ends in '/v3')")
parser.add_argument("--vault", help="Name of the default vault")
grp = parser.add_mutually_exclusive_group()
grp.add_argument("--token", help="Auth token to use")
grp.add_argument("--auth", help="Login credentials as a username:password string")
parser.set_defaults(main=command, noconfig=True)
def command(ctx, args):
......
......@@ -8,7 +8,7 @@ from .._utils import compile_glob, hbytes
def register():
parser = register_subcommand("put", command, help=__doc__.splitlines()[0], description=__doc__)
parser = register_subcommand("put", help=__doc__.splitlines()[0], description=__doc__)
parser.add_argument("-a", "--all", action="store_true", help="Include hidden files (skipped by default)")
parser.add_argument("-n", "--dry-run", action="store_true",
help="Do not upload anything, just print what would have been uploaded")
......@@ -18,12 +18,12 @@ def register():
help="Exclude files by glob pattern")
parser.add_argument("-i", "--include", metavar="GLOB", action="append",
help="Include files by glob pattern (default: all)")
parser.add_argument("-%", "--progress", action="store_true",
parser.add_argument("-%", "-p", "--progress", action="store_true",
help="Show progress bar for large files or slow uploads")
parser.add_argument("TARGET", help="Archive ID, or 'new' to create a new archive")
parser.add_argument("PATH", nargs='+', help="Files or directories to upload")
parser.set_defaults(cmd=command)
parser.set_defaults(main=command)
def findfiles(flist, include_hidden=False):
......@@ -72,7 +72,7 @@ def command(ctx, args):
target = rfile
if target != os.path.normpath(target):
raise ValueError("Unable to create remote file with relative or empty path segments: {}".format(target))
raise ValueError("Unable to upload relative path: {!r}".format(target))
if target in uploads and uploads[target][0] != file:
raise ValueError("File included twice: {} and {} both map to {}".format(file, uploads[target][0], target))
stat = os.stat(file)
......@@ -89,9 +89,9 @@ def command(ctx, args):
with client.begin(autocommit=True):
if archive == 'new':
archive = client.create_archive(vault)['id']
printer("Uploading {} files ({}) to new archive: {}/{}", len(uploads), hbytes(total), vault, archive)
printer("Uploading {} files ({}) to new archive: /{}/{}", len(uploads), hbytes(total), vault, archive)
else:
printer("Uploading {} files ({}) to archive: {}/{}", len(uploads), hbytes(total), vault, archive)
printer("Uploading {} files ({}) to archive: /{}/{}", len(uploads), hbytes(total), vault, archive)
pbar = None
if progress and not printer.quiet:
......@@ -118,5 +118,4 @@ def command(ctx, args):
if pbar:
pbar.close()
printer("Upload complete!")
printer("Uploaded {} files ({}) to archive: {}/{}", len(uploads), hbytes(total), vault, archive)
printer("Done! Uploaded {} files ({}) to archive: /{}/{}", len(uploads), hbytes(total), vault, archive)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment