From 54b94ab3f4c1d468c96272d9de47279a5cda8c98 Mon Sep 17 00:00:00 2001 From: Michael Woods Date: Sun, 16 Mar 2025 19:21:56 -0400 Subject: [PATCH] Some basic object cli commands work. Searching and uploading files. --- src/packetserver/client/cli/__init__.py | 14 +++- src/packetserver/client/cli/object.py | 104 ++++++++++++++++++++++++ src/packetserver/client/objects.py | 3 +- 3 files changed, 116 insertions(+), 5 deletions(-) diff --git a/src/packetserver/client/cli/__init__.py b/src/packetserver/client/cli/__init__.py index f2f69ae..5087bda 100644 --- a/src/packetserver/client/cli/__init__.py +++ b/src/packetserver/client/cli/__init__.py @@ -6,6 +6,7 @@ from packetserver.common.constants import yes_values from packetserver.common import Request, Response from packetserver.client.cli.util import format_list_dicts, exit_client from packetserver.client.cli.job import job +from packetserver.client.cli.object import objects import ZODB import ZODB.FileStorage import ax25 @@ -22,10 +23,14 @@ VERSION="0.1.0-alpha" @click.group() @click.option('--conf', default=config_path(), help='path to configfile') -@click.option('--server', '-s', default='', help="server radio callsign to connect to (required)") -@click.option('--agwpe', '-a', default='', help="AGWPE TNC server address to connect to (config file)") -@click.option('--port', '-p', default=0, help="AGWPE TNC server port to connect to (config file)") -@click.option('--callsign', '-c', default='', help="radio callsign[+ssid] of this client station (config file)") +@click.option('--server', '-s', default='', help="server radio callsign to connect to (required)", + envvar='PSCLIENT_SERVER') +@click.option('--agwpe', '-a', default='', help="AGWPE TNC server address to connect to (config file)", + envvar='PSCLIENT_AGWPE') +@click.option('--port', '-p', default=0, help="AGWPE TNC server port to connect to (config file)", + envvar='PSCLIENT_PORT') +@click.option('--callsign', '-c', default='', help="radio callsign[+ssid] of this client station (config file)", + envvar='PSCLIENT_CALLSIGN') @click.option('--keep-log', '-k', is_flag=True, default=False, help="Save local copy of request log after session ends?") @click.version_option(VERSION,"--version", "-v") @click.pass_context @@ -152,6 +157,7 @@ def user(ctx, list_users, output_format, username): cli.add_command(user) cli.add_command(query_server) cli.add_command(job, name='job') +cli.add_command(objects, name='object') if __name__ == '__main__': cli() diff --git a/src/packetserver/client/cli/object.py b/src/packetserver/client/cli/object.py index e69de29..4c6273f 100644 --- a/src/packetserver/client/cli/object.py +++ b/src/packetserver/client/cli/object.py @@ -0,0 +1,104 @@ +import os +import os.path +import click +from packetserver.client.objects import ObjectWrapper, post_object, post_file, get_user_objects, get_object_by_uuid +from packetserver.client.cli.util import exit_client, format_list_dicts +from copy import deepcopy +from uuid import UUID + +@click.group() +@click.pass_context +def objects(ctx): + """Manages objects stored on the BBS.""" + pass + +@click.command() +@click.argument('file_path', required=True, type=str) +@click.option("--public", "-P", is_flag=True, default=False, help="Mark the object public for all users.") +@click.option("--binary", '-b', is_flag=True, default=False, help="Treat the file as binary instead of text.") +@click.option('--name', '-n', type=str, default=None, help="Name of object instead of source filename.") +@click.pass_context +def upload_file(ctx, file_path, public, name, binary): + """Upload file to object store. Return the assigned UUID.""" + + private = not public + client = ctx.obj['client'] + if not os.path.isfile(file_path): + click.echo(f"'{file_path}' is not a file.", err=True) + exit_client(ctx.obj, 15) + + uuid = post_file(client, ctx.obj['bbs'], file_path, private=private, name=name, binary=binary) + click.echo(str(uuid)) + exit_client(ctx.obj, 0) + +@click.command() +@click.argument('uuid', required=True, type=str) +@click.pass_context +def get(ctx, uuid): + """Get an object's data by its UUID.""" + client = ctx.obj['client'] + u = "" + try: + u = UUID(uuid) + except ValueError as e: + click.echo(f"'{uuid}' is not a valid UUID.", err=True) + exit_client(ctx.obj, 13) + + try: + obj = get_object_by_uuid(client, ctx.obj['bbs'], u, include_data=True) + click.echo(obj.data, nl=False) + exit_client(ctx.obj, 0) + except Exception as e: + click.echo(e, err=True) + exit_client(ctx.obj, 19) + + +@click.command() +@click.option('--number', '-n', type=int, default=0, help="Number of objects to list. Default 0 for all.") +@click.option('--search', '-S', type=str, default=None, help="Search string to filter objects with.") +@click.option('--reverse', '-r', is_flag=True, default=False, help="Return results in reverse order.") +@click.option('--sort-by', '-B', default='date', help="Sort objects by size, date(default), or name", + type=click.Choice(['size', 'name', 'date'], case_sensitive=False)) +@click.option("--output-format", "-f", default="table", help="Print data as table[default], list, or JSON", + type=click.Choice(['table', 'json', 'list'], case_sensitive=False)) +@click.pass_context +def list_objects(ctx, number, search, reverse, sort_by, output_format): + """Get a list of user objects without the data.""" + # def get_user_objects(client: Client, bbs_callsign: str, limit: int = 10, include_data: bool = True, search: str = None, +# reverse: bool = False, sort_date: bool = False, sort_name: bool = False, sort_size: bool = False)\ +# -> list[ObjectWrapper]: + + client = ctx.obj['client'] + sort_date = False + sort_name = False + sort_size = False + + if sort_by == "size": + sort_size = True + elif sort_by == "name": + sort_name = True + else: + sort_date = True + + object_list = get_user_objects(client, ctx.obj['bbs'], limit=number, include_data=False, search=search, + reverse=reverse, sort_date=sort_date, sort_name=sort_name, sort_size=sort_size) + + obj_dicts = [] + for x in object_list: + d = deepcopy(x.obj_data) + d['uuid'] = "" + if 'uuid_bytes' in d: + d['uuid'] = str(UUID(bytes=d['uuid_bytes'])) + del d['uuid_bytes'] + if 'data' in d: + del d['data'] + if 'includes_data' in d: + del d['includes_data'] + obj_dicts.append(d) + + click.echo(format_list_dicts(obj_dicts, output_format=output_format.lower())) + exit_client(ctx.obj, 0) + +objects.add_command(upload_file) +objects.add_command(list_objects, name='list') +objects.add_command(get) \ No newline at end of file diff --git a/src/packetserver/client/objects.py b/src/packetserver/client/objects.py index c5202e2..c00f070 100644 --- a/src/packetserver/client/objects.py +++ b/src/packetserver/client/objects.py @@ -121,7 +121,8 @@ def get_user_objects(client: Client, bbs_callsign: str, limit: int = 10, include if sort_name: req.set_var('sort', 'name') req.set_var('reverse', reverse) - req.set_var('limit', limit) + if limit != 0: + req.set_var('limit', limit) if search is not None: req.set_var('search', str(search)) req.path = "object"