From c1a455d7a4b738172324e77b7fb7f12eb39bd6ed Mon Sep 17 00:00:00 2001 From: Gnarwhal Date: Wed, 28 Aug 2024 06:43:21 +0000 Subject: [PATCH 1/5] Bump version --- pyproject.toml | 4 ++-- src/sshare/version.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b9b4472..948ad06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "sshare" -version = "1.1.0" +version = "2.0.0" authors = [ { name = "Gnarwhal", email = "git.aspect893@passmail.net" }, ] @@ -22,7 +22,7 @@ dependencies = [ [project.urls] Homepage = "https://forge.monodon.me/Gnarwhal/sshare" -Documentation= "https://forge.monodon.me/Gnarwhal/sshare/README.md#Usage" +Documentation = "https://forge.monodon.me/Gnarwhal/sshare/README.md#Usage" Repository = "https://forge.monodon.me/Gnarwhal/sshare" Issues = "https://forge.monodon.me/Gnarwhal/sshare/issues" diff --git a/src/sshare/version.py b/src/sshare/version.py index b2b60a5..1b23640 100644 --- a/src/sshare/version.py +++ b/src/sshare/version.py @@ -1 +1 @@ -version = "1.1.0" +version = "2.0.0" From 72627995a7bf32d3744ec1532dd7b5b682e612f1 Mon Sep 17 00:00:00 2001 From: Gnarwhal Date: Wed, 28 Aug 2024 16:56:05 +0000 Subject: [PATCH 2/5] Loaded a module :D --- src/sshare/cli.py | 13 +++++++++++++ src/sshare/plugins/logger_cli.py | 25 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 src/sshare/plugins/logger_cli.py diff --git a/src/sshare/cli.py b/src/sshare/cli.py index 87af8be..ddf7794 100644 --- a/src/sshare/cli.py +++ b/src/sshare/cli.py @@ -14,6 +14,8 @@ import argparse import getpass +import importlib +import importlib.util import os import os.path import pyclip @@ -89,6 +91,17 @@ def rebase(number): def main(): + module_spec = importlib.util.spec_from_file_location( + "logger_cli", + "/home/gnarwhal/meine/projects/active/sshare/src/sshare/plugins/logger_cli.py" + ) + module = importlib.util.module_from_spec(module_spec) + module_spec.loader.exec_module(module) + + module.log("test?") + module.warn("test?") + module.error("test?") + arguments = parse_arguments() config = Config() diff --git a/src/sshare/plugins/logger_cli.py b/src/sshare/plugins/logger_cli.py new file mode 100644 index 0000000..42ba723 --- /dev/null +++ b/src/sshare/plugins/logger_cli.py @@ -0,0 +1,25 @@ +# This file is part of SSHare. +# +# SSHare is free software: you can redistribute it and/or modify it under the terms of +# the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# SSHare is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# SSHare. If not, see . + +def _print_with_color(color, message): + print(f"\033[{color}m{message}\033[0m") + +def log(message): + _print_with_color(0, message) + +def warn(message): + _print_with_color(93, message) + +def error(message): + _print_with_color(91, message) From 98c490dab926b674af737f2e3a87226b30a2ef6e Mon Sep 17 00:00:00 2001 From: Gnarwhal Date: Wed, 28 Aug 2024 20:23:52 +0000 Subject: [PATCH 3/5] Plugins are loading --- pyproject.toml | 2 +- src/sshare/__main__.py | 2 +- .../logger_cli.py => command_line_logger.py} | 4 +- src/sshare/{cli.py => main.py} | 77 ++++++++++++++----- 4 files changed, 64 insertions(+), 21 deletions(-) rename src/sshare/{plugins/logger_cli.py => command_line_logger.py} (95%) rename src/sshare/{cli.py => main.py} (74%) diff --git a/pyproject.toml b/pyproject.toml index 948ad06..c937898 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,4 +27,4 @@ Repository = "https://forge.monodon.me/Gnarwhal/sshare" Issues = "https://forge.monodon.me/Gnarwhal/sshare/issues" [project.scripts] -sshare = "sshare.cli:main" +sshare = "sshare.main:main" diff --git a/src/sshare/__main__.py b/src/sshare/__main__.py index 6545e2b..2ae67cd 100644 --- a/src/sshare/__main__.py +++ b/src/sshare/__main__.py @@ -12,6 +12,6 @@ # You should have received a copy of the GNU General Public License along with # SSHare. If not, see . -from cli import main +from main import main main() diff --git a/src/sshare/plugins/logger_cli.py b/src/sshare/command_line_logger.py similarity index 95% rename from src/sshare/plugins/logger_cli.py rename to src/sshare/command_line_logger.py index 42ba723..6b86cce 100644 --- a/src/sshare/plugins/logger_cli.py +++ b/src/sshare/command_line_logger.py @@ -12,10 +12,12 @@ # You should have received a copy of the GNU General Public License along with # SSHare. If not, see . +plugin_type = "logger" + def _print_with_color(color, message): print(f"\033[{color}m{message}\033[0m") -def log(message): +def info(message): _print_with_color(0, message) def warn(message): diff --git a/src/sshare/cli.py b/src/sshare/main.py similarity index 74% rename from src/sshare/cli.py rename to src/sshare/main.py index ddf7794..30c27e7 100644 --- a/src/sshare/cli.py +++ b/src/sshare/main.py @@ -23,14 +23,12 @@ import time import tomllib import subprocess import sys -from sshare.version import version +from pathlib import Path +from version import version class Config: - def __init__(self): - config_directory = os.environ.get("XDG_CONFIG_DIR") - if config_directory == None: - config_directory = f"{os.environ["HOME"]}/.config" - _config = self._load_from_file(f"{config_directory}/sshare/config.toml") + def __init__(self, config_directory): + _config = self._load_from_file(config_directory / "config.toml") self.source_directory = _config.get("source_directory") @@ -90,21 +88,64 @@ def rebase(number): return rebased -def main(): - module_spec = importlib.util.spec_from_file_location( - "logger_cli", - "/home/gnarwhal/meine/projects/active/sshare/src/sshare/plugins/logger_cli.py" - ) - module = importlib.util.module_from_spec(module_spec) - module_spec.loader.exec_module(module) +class MetaLogger: + def __init__(self, loggers): + def info(message): + for logger in loggers: + logger.info(message) + + def warn(message): + for logger in loggers: + logger.warn(message) + + def error(message): + for logger in loggers: + logger.error(message) + + self.info = info + self.warn = warn + self.error = error + +class Plugin: + def __init__(self, name, module): + self.name = name + self.module = module + +def main(): + config_directory = Path(os.environ.get("XDG_CONFIG_DIR") or f"{os.environ["HOME"]}/.config") / "sshare" + logger = importlib.import_module("command_line_logger") + modules = { + "logger": [ Plugin("command_line_logger", logger) ], + "data": [], + } + error = False + sys.dont_write_bytecode = True + for plugin in (config_directory / "plugins").iterdir(): + if plugin.is_file(): + module_spec = importlib.util.spec_from_file_location( + plugin.stem, + plugin.as_posix(), + ) + module = importlib.util.module_from_spec(module_spec) + module_spec.loader.exec_module(module) + + modules_of_type = modules.get(module.plugin_type) + if modules_of_type == None: + logger.error(f"Error: Plugin '{plugin.stem}' has an invalid plugin type '{module.plugin_type}'") + error = True + else: + modules_of_type.append(Plugin(plugin.stem, module)) + if hasattr(module, "init"): + module.init() + if error: + sys.exit(1) + sys.dont_write_bytecode = False + logger = MetaLogger([ logger.module for logger in modules["logger"] ]) + logger.info("Successfully loaded plugins") - module.log("test?") - module.warn("test?") - module.error("test?") - arguments = parse_arguments() - config = Config() + config = Config(config_directory) contents = b'' target_file_extension = "" From 7b8fbf672b8e70bf7ae83f2449f566aa99d3227b Mon Sep 17 00:00:00 2001 From: Gnarwhal Date: Sat, 31 Aug 2024 05:42:26 +0000 Subject: [PATCH 4/5] Reorganised to use a plugin system --- src/sshare/main.py | 225 ++++++++---------- src/sshare/plugins/config.py | 27 +++ src/sshare/plugins/default/append_type.py | 34 +++ .../default/command_line.py} | 0 src/sshare/plugins/default/current_time.py | 55 +++++ src/sshare/plugins/default/file.py | 36 +++ src/sshare/plugins/default/ssh.py | 64 +++++ src/sshare/plugins/source.py | 24 ++ 8 files changed, 337 insertions(+), 128 deletions(-) create mode 100644 src/sshare/plugins/config.py create mode 100644 src/sshare/plugins/default/append_type.py rename src/sshare/{command_line_logger.py => plugins/default/command_line.py} (100%) create mode 100644 src/sshare/plugins/default/current_time.py create mode 100644 src/sshare/plugins/default/file.py create mode 100644 src/sshare/plugins/default/ssh.py create mode 100644 src/sshare/plugins/source.py diff --git a/src/sshare/main.py b/src/sshare/main.py index 30c27e7..f0c63ae 100644 --- a/src/sshare/main.py +++ b/src/sshare/main.py @@ -26,69 +26,11 @@ import sys from pathlib import Path from version import version -class Config: - def __init__(self, config_directory): - _config = self._load_from_file(config_directory / "config.toml") +from plugins.config import Default +from plugins.config import NoDefault +from plugins.config import Flags - self.source_directory = _config.get("source_directory") - - host = _config.get("host") - if host == None: - print("Error: 'host' cannot be 'None'") - sys.exit(1) - self.host_protocol = host.get("protocol") - self.host_name = host.get("name") - self.host_port = host.get("port") - self.host_path = host.get("path") - if self.host_protocol == None: - self.host_protocol = "https" - if self.host_name == None: - print("Error: 'host.name' cannot be 'None'") - sys.exit(1) - if self.host_port == None: - self.host_port = "" - else: - self.host_port = f":{self.host_port}" - if self.host_path == None: - self.host_path = "" - - ssh = _config.get("ssh") - if ssh == None: - print("Error: 'ssh' cannot be 'None'") - sys.exit(1) - self.ssh_port = ssh.get("port") - self.ssh_user = ssh.get("user") - self.ssh_path = ssh.get("path") - if self.ssh_port == None: - self.ssh_port = 22 - if self.ssh_user == None: - self.ssh_user = getpass.getuser() - if self.ssh_path == None: - print("Error: 'ssh.path' cannot be 'None'") - sys.exit(1) - - def _load_from_file(self, config_path): - with open(config_path, mode="rb") as file: - return tomllib.load(file) - - -def rebase(number): - if number == 0: - return "0" - rebased = "" - while number != 0: - digit = number % 62 - if digit < 10: - rebased = chr(digit + 48) + rebased - elif digit < 36: - rebased = chr(digit + 87) + rebased - else: - rebased = chr(digit + 29) + rebased - number = int(number / 62) - return rebased - - -class MetaLogger: +class Congloggerate: def __init__(self, loggers): def info(message): for logger in loggers: @@ -106,6 +48,14 @@ class MetaLogger: self.warn = warn self.error = error + fatalicize(self) + +def fatalicize(logger): + def fatal(message): + logger.error(message) + sys.exit(1) + setattr(logger, "fatal", fatal) + class Plugin: def __init__(self, name, module): self.name = name @@ -113,12 +63,32 @@ class Plugin: def main(): config_directory = Path(os.environ.get("XDG_CONFIG_DIR") or f"{os.environ["HOME"]}/.config") / "sshare" - logger = importlib.import_module("command_line_logger") - modules = { - "logger": [ Plugin("command_line_logger", logger) ], - "data": [], + with open(config_directory / "config.toml", mode="rb") as file: + config = tomllib.load(file) + + # Load command line early and set it as the active logger + # so that it can be used to report errors while loading and + # configuring plugins + # i.e. before other logging plugins have had a chance to be initialised + logger = importlib.import_module("plugins.default.command_line") + fatalicize(logger) + + # Load inbuilt plugins + plugins_flat = [ + Plugin("command_line", logger), + Plugin("file", importlib.import_module("plugins.default.file")), + Plugin("current_time", importlib.import_module("plugins.default.current_time")), + Plugin("append_type", importlib.import_module("plugins.default.append_type")), + Plugin("ssh", importlib.import_module("plugins.default.ssh")), + ] + plugins = { + "logger": [], + "source": [], + "name": [], + "upload": [], } - error = False + + # Load external plugins sys.dont_write_bytecode = True for plugin in (config_directory / "plugins").iterdir(): if plugin.is_file(): @@ -128,77 +98,76 @@ def main(): ) module = importlib.util.module_from_spec(module_spec) module_spec.loader.exec_module(module) + plugins_flat.append(Plugin(plugin.stem, module)) + sys.dont_write_bytecode = False - modules_of_type = modules.get(module.plugin_type) - if modules_of_type == None: - logger.error(f"Error: Plugin '{plugin.stem}' has an invalid plugin type '{module.plugin_type}'") - error = True - else: - modules_of_type.append(Plugin(plugin.stem, module)) - if hasattr(module, "init"): - module.init() + # Set plugin configurations from config file + if config.get("plugins") == None: + config["plugins"] = {} + for plugin in plugins_flat: + if hasattr(plugin.module, "config"): + plugin_config = config["plugins"].get(plugin.name) + if plugin_config != None: + for config_entry in plugin_config.items(): + plugin.module.config[config_entry[0]] = Default(config_entry[1]) + + # Flatten plugin configs + class PluginConfig: pass + error = False + for plugin in plugins_flat: + if hasattr(plugin.module, "config"): + config = plugin.module.config + plugin.module.config = PluginConfig() + for config_entry in config.items(): + if isinstance(config_entry[1], NoDefault): + logger.error(f"{plugin.name} > Error: Value '{config_entry[0]}' has no default value and must be specified explicitly") + error = True + elif isinstance(config_entry[1], Default): + setattr(plugin.module.config, config_entry[0], config_entry[1].value) + else: + setattr(plugin.module.config, config_entry[0], config_entry[1]) if error: sys.exit(1) - sys.dont_write_bytecode = False - logger = MetaLogger([ logger.module for logger in modules["logger"] ]) - logger.info("Successfully loaded plugins") - arguments = parse_arguments() + # Initialise plugins + for plugin in plugins_flat: + setattr(plugin.module, "logger", logger) + if hasattr(plugin.module, "init"): + plugin.module.init() - config = Config(config_directory) - - contents = b'' - target_file_extension = "" - if arguments.latest or arguments.file != None: - file_path = "" - if arguments.latest: - if config.source_directory == "": - print("Option 'latest' requires source directory to be specified") - sys.exit(1) - file_path = _latest(config.source_directory) - else: - file_path = arguments.file - print(f"Uploading file '{file_path}'") - - with open(file_path, mode="rb") as file: - contents = file.read() - - (_, target_file_extension) = os.path.splitext(file_path) - elif arguments.paste: - print("Uploading contents of clipboard") - contents = pyclip.paste() - target_file_extension = ".txt" - else: - print("Error: must specify one of -f FILE, -l, -p") - sys.exit(1) - - target_id = rebase(time.time_ns()) - target_file_name = f"{target_id}{target_file_extension}" - target_file = f"{config.ssh_path}/{target_file_name}" - target_destination = f"{config.ssh_user}@{config.host_name}" - print(f"Uploading to host: {target_destination}, port: {config.ssh_port}, file: {target_file}") - process = subprocess.run([ - "ssh", - f"-p {config.ssh_port}", - target_destination, - "-T", - f"cat - > {target_file}" - ], - input = contents, - ) - if process.returncode != 0: - print("Error: failed to upload file") + # Sort plugins by type + error = False + for plugin in plugins_flat: + if isinstance(plugin.module.plugin_type, str): + plugin.module.plugin_type = [ plugin.module.plugin_type ] + for plugin_type in plugin.module.plugin_type: + plugins_of_type = plugins.get(plugin_type) + if plugins_of_type == None: + logger.error(f"Error: Plugin '{plugin.name}' has an invalid plugin type '{plugin_type}'") + error = True + else: + plugins_of_type.append(plugin) + if error: sys.exit(1) - target_url = f"{config.host_protocol}://{config.host_name}{config.host_port}{config.host_path}/{target_file_name}" - print(f"File available at '{target_url}'") - if arguments.copy: - pyclip.copy(target_url) - print("URL copied to clipboard") + logger = Congloggerate([ logger.module for logger in plugins["logger"] ]) + + sources = [] + for plugin in plugins["source"]: + sources.append(plugin.module.get()) + + for index, source in enumerate(sources): + name = "" + for plugin in plugins["name"]: + name = plugin.module.name(name, source) + sources[index] = name, source + + for (name, source) in sources: + for plugin in plugins["upload"]: + plugin.module.upload(name, source) sys.exit(0) - def parse_arguments(): parser = argparse.ArgumentParser( prog = "SSHare", diff --git a/src/sshare/plugins/config.py b/src/sshare/plugins/config.py new file mode 100644 index 0000000..79199ad --- /dev/null +++ b/src/sshare/plugins/config.py @@ -0,0 +1,27 @@ +# This file is part of SSHare. +# +# SSHare is free software: you can redistribute it and/or modify it under the terms of +# the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# SSHare is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# SSHare. If not, see . + +class NoDefault: + def __init__(self, flags=None): + self.flags = flags + +class Default: + def __init__(self, value, flags=None): + self.value = value + self.flags = flags + +class Flags: + def __init__(self, short=None, long=None): + self.short = short + self.long = long diff --git a/src/sshare/plugins/default/append_type.py b/src/sshare/plugins/default/append_type.py new file mode 100644 index 0000000..8d65e3e --- /dev/null +++ b/src/sshare/plugins/default/append_type.py @@ -0,0 +1,34 @@ +# This file is part of SSHare. +# +# SSHare is free software: you can redistribute it and/or modify it under the terms of +# the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# SSHare is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# SSHare. If not, see . + +from ..source import File +from ..source import Raw + +plugin_type = "name" + +def name(name, source): + if isinstance(source, File): + if source.path.is_dir(): + return name + else: + start = 1 + components = source.path.name.split(".") + if components[0] == "": + start += 1 + if start > len(components): + return name + else: + return name + "." + ".".join(components[start:]) + elif isinstance(source, Raw): + return name + f".{source.type}" diff --git a/src/sshare/command_line_logger.py b/src/sshare/plugins/default/command_line.py similarity index 100% rename from src/sshare/command_line_logger.py rename to src/sshare/plugins/default/command_line.py diff --git a/src/sshare/plugins/default/current_time.py b/src/sshare/plugins/default/current_time.py new file mode 100644 index 0000000..1c37444 --- /dev/null +++ b/src/sshare/plugins/default/current_time.py @@ -0,0 +1,55 @@ +# This file is part of SSHare. +# +# SSHare is free software: you can redistribute it and/or modify it under the terms of +# the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# SSHare is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# SSHare. If not, see . + +import time + +from ..config import Default +from ..config import Flags +from ..source import File + +plugin_type = "name" + +config = { + "base": 62, +} + +def init(): + if not isinstance(config.base, int): + logger.fatal("Error: 'base' must be an integer") + elif config.base < 2: + logger.fatal("Error: 'base' cannot be less than 2") + elif config.base > 62: + logger.fatal("Error: 'base' cannot be greater than 62") + +def name(name, source): + return name + _rebase(config.base, time.time_ns()) + +def _rebase(base, number): + if number == 0: + return "0" + if base == 10: + return f"{number}" + rebased = "" + while number != 0: + rebased = _number_to_char(number % base) + rebased + number = int(number / base) + return rebased + +def _number_to_char(number): + if number < 10: + return chr(number + 48) + elif number < 36: + return chr(number + 87) + else: + return chr(number + 29) diff --git a/src/sshare/plugins/default/file.py b/src/sshare/plugins/default/file.py new file mode 100644 index 0000000..36bcc50 --- /dev/null +++ b/src/sshare/plugins/default/file.py @@ -0,0 +1,36 @@ +# This file is part of SSHare. +# +# SSHare is free software: you can redistribute it and/or modify it under the terms of +# the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# SSHare is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# SSHare. If not, see . + +from ..config import NoDefault +from ..config import Flags +from ..source import File + +plugin_type = "source" + +config = { + "file": NoDefault( + flags=Flags( + short="f", + long="file" + ) + ), +} + +def get(): + file = File(config.file) + if file.path.is_dir(): + logger.info(f"Uploading directory '{config.file}'") + else: + logger.info(f"Uploading file '{config.file}'") + return file diff --git a/src/sshare/plugins/default/ssh.py b/src/sshare/plugins/default/ssh.py new file mode 100644 index 0000000..b85819b --- /dev/null +++ b/src/sshare/plugins/default/ssh.py @@ -0,0 +1,64 @@ +# This file is part of SSHare. +# +# SSHare is free software: you can redistribute it and/or modify it under the terms of +# the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# SSHare is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# SSHare. If not, see . + +import getpass +import subprocess + +from ..config import Default +from ..config import NoDefault +from ..config import Flags +from ..source import File +from ..source import Raw + +plugin_type = "upload" + +config = { + "host": NoDefault(), + "path": NoDefault(), + "port": 22, + "user": getpass.getuser(), +} + +def upload(name, source): + logger.info(f"Uploading to {config.user}@{config.host}/{config.port}:{config.path}/{name}") + if isinstance(source, File): + command = [ + "scp", + ] + ([ + "-r", + ] if source.path.is_dir() else []) + [ + "-P", f"{config.port}", + source.path, + f"{config.user}@{config.host}:{config.path}/{name}", + ] + process = subprocess.run(command) + if process.returncode != 0: + if source.path.is_dir(): + logger.fatal("Error: failed to upload directory") + else: + logger.fatal("Error: failed to upload file") + elif isinstance(source, Raw): + command = [ + "ssh", + f"-p {config.port}", + f"{config.user}@{config.host}", + "-T", + f"cat - > {config.path}/{name}" + ] + process = subprocess.run( + command, + input = source.data, + ) + if process.returncode != 0: + logger.fatal("Error: failed to upload data") diff --git a/src/sshare/plugins/source.py b/src/sshare/plugins/source.py new file mode 100644 index 0000000..fcfc6a6 --- /dev/null +++ b/src/sshare/plugins/source.py @@ -0,0 +1,24 @@ +# This file is part of SSHare. +# +# SSHare is free software: you can redistribute it and/or modify it under the terms of +# the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# SSHare is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# SSHare. If not, see . + +from pathlib import Path + +class Raw: + def __init__(self, type, data): + self.type = type + self.data = data + +class File: + def __init__(self, path): + self.path = Path(path) From 0384f9080dbb0dbc6f64cb254649ddcf1e3178f9 Mon Sep 17 00:00:00 2001 From: Gnarwhal Date: Sat, 31 Aug 2024 17:29:46 +0000 Subject: [PATCH 5/5] Arguments are working :fingers_crossed: --- hello.txt | 1 + src/sshare/main.py | 136 ++++++++++++++------- src/sshare/plugins/config.py | 71 +++++++++-- src/sshare/plugins/default/current_time.py | 10 +- src/sshare/plugins/default/file.py | 16 +-- src/sshare/plugins/default/ssh.py | 8 +- 6 files changed, 173 insertions(+), 69 deletions(-) create mode 100644 hello.txt diff --git a/hello.txt b/hello.txt new file mode 100644 index 0000000..980a0d5 --- /dev/null +++ b/hello.txt @@ -0,0 +1 @@ +Hello World! diff --git a/src/sshare/main.py b/src/sshare/main.py index f0c63ae..86e97b3 100644 --- a/src/sshare/main.py +++ b/src/sshare/main.py @@ -26,9 +26,7 @@ import sys from pathlib import Path from version import version -from plugins.config import Default from plugins.config import NoDefault -from plugins.config import Flags class Congloggerate: def __init__(self, loggers): @@ -81,12 +79,9 @@ def main(): Plugin("append_type", importlib.import_module("plugins.default.append_type")), Plugin("ssh", importlib.import_module("plugins.default.ssh")), ] - plugins = { - "logger": [], - "source": [], - "name": [], - "upload": [], - } + plugins = {} + for type in [ "logger", "source", "name", "upload" ]: + plugins[type] = { "active": [], "inactive": [] } # Load external plugins sys.dont_write_bytecode = True @@ -102,6 +97,20 @@ def main(): sys.dont_write_bytecode = False # Set plugin configurations from config file + # Load plugin arguments and detect conflicts + error = False + argument_map = {} + used_arguments = {} + parser = argparse.ArgumentParser( + prog = "SSHare", + description = "Upload files to a server via ssh", + ) + parser.add_argument( + "-v", + "--version", + action="version", + version=f"%(prog)s version {version}", + ) if config.get("plugins") == None: config["plugins"] = {} for plugin in plugins_flat: @@ -109,33 +118,34 @@ def main(): plugin_config = config["plugins"].get(plugin.name) if plugin_config != None: for config_entry in plugin_config.items(): - plugin.module.config[config_entry[0]] = Default(config_entry[1]) - - # Flatten plugin configs - class PluginConfig: pass - error = False - for plugin in plugins_flat: - if hasattr(plugin.module, "config"): - config = plugin.module.config - plugin.module.config = PluginConfig() - for config_entry in config.items(): - if isinstance(config_entry[1], NoDefault): - logger.error(f"{plugin.name} > Error: Value '{config_entry[0]}' has no default value and must be specified explicitly") - error = True - elif isinstance(config_entry[1], Default): - setattr(plugin.module.config, config_entry[0], config_entry[1].value) + plugin.module.config[config_entry[0]] = config_entry[1] + else: + setattr(plugin.module, "config", {}) + if hasattr(plugin.module, "args"): + for arg_name, arg in plugin.module.args.items(): + if arg.is_valid(): + arg.set_for_plugin(plugin) + def check_flag(flag): + if flag in used_arguments: + logger.error(f"Error: Argument '{arg_name}' for plugin '{plugin.name}' has conflict. Flag '{flag}' is also used by plugin '{used_arguments[arg.short]}'") + error = True + check_flag(arg.short) + check_flag(arg.long) + arg.add(parser, used_arguments) + argument_map[arg.dest] = plugin, arg_name else: - setattr(plugin.module.config, config_entry[0], config_entry[1]) + logger.error(f"Error: Argument '{arg_name}' must set either one or both of short and long flag parameters") + error = True if error: sys.exit(1) - # Initialise plugins - for plugin in plugins_flat: - setattr(plugin.module, "logger", logger) - if hasattr(plugin.module, "init"): - plugin.module.init() + arguments = parser.parse_args() + for arg, (plugin, config) in list(argument_map.items()): + if getattr(arguments, arg): + plugin.module.config[config] = getattr(arguments, arg) + del argument_map[arg] - # Sort plugins by type + # Sort plugins by type and check activation criteria error = False for plugin in plugins_flat: if isinstance(plugin.module.plugin_type, str): @@ -146,24 +156,73 @@ def main(): logger.error(f"Error: Plugin '{plugin.name}' has an invalid plugin type '{plugin_type}'") error = True else: - plugins_of_type.append(plugin) + active = True + if hasattr(plugin.module, "activate"): + criteria = plugin.module.activate + if isinstance(plugin.module.activate, dict): + criteria = plugin.module.activate.get(plugin_type) + if criteria != None: + for criterion in criteria: + active = not plugin.module.args[criterion].dest in argument_map + if not active: + break + plugins_of_type["active" if active else "inactive"].append(plugin) + for plugin_type, plugins_of_type in plugins.items(): + if len(plugins_of_type["active"]) == 0 and plugin_type != "logger": + if len(plugins_of_type["inactive"]) == 0: + logger.error(f"No '{plugin_type}' plugins available. Atleast one must be provided") + else: + logger.error(f"No '{plugin_type}' plugins activated. Activate at least one of:") + for plugin in plugins_of_type["inactive"]: + logger.error(f"{plugin.name}:") + criteria = plugin.module.activate + if isinstance(plugin.module.activate, dict): + criteria = plugin.module.activate[plugin_type] + for criterion in criteria: + logger.error(f" {plugin.module.args[criterion].pretty()}") + error = True if error: sys.exit(1) - logger = Congloggerate([ logger.module for logger in plugins["logger"] ]) + # Flatten plugin configs + error = False + class PluginConfig: pass + for plugin in plugins_flat: + if hasattr(plugin.module, "config"): + config = plugin.module.config + plugin.module.config = PluginConfig() + for config_entry in config.items(): + if config_entry[1] == NoDefault: + logger.error(f"Error: Value 'plugins.{plugin.name}.{config_entry[0]}' has no default value and must be specified explicitly") + error = True + else: + setattr(plugin.module.config, config_entry[0], config_entry[1]) + if error: + sys.exit(1) + + # Initialise plugins + for plugin in plugins_flat: + setattr(plugin.module, "logger", logger) + if hasattr(plugin.module, "init"): + error = error or plugin.module.init() + + logger = Congloggerate([ logger.module for logger in plugins["logger"]["active"] ]) sources = [] - for plugin in plugins["source"]: + for plugin in plugins["source"]["active"]: sources.append(plugin.module.get()) + if len(sources) == 0: + logger.error("Error: No sources provided. Must activate at least one source plugin") + log_activations(logger, plugins["source"]) for index, source in enumerate(sources): name = "" - for plugin in plugins["name"]: + for plugin in plugins["name"]["active"]: name = plugin.module.name(name, source) sources[index] = name, source for (name, source) in sources: - for plugin in plugins["upload"]: + for plugin in plugins["upload"]["active"]: plugin.module.upload(name, source) sys.exit(0) @@ -174,12 +233,6 @@ def parse_arguments(): description = "Upload files to a server via ssh", ) - parser.add_argument( - "-v", - "--version", - action="version", - version=f"%(prog)s version {version}", - ) parser.add_argument( "-l", "--latest", @@ -206,7 +259,6 @@ def parse_arguments(): const=True, help="Copy the resultant URL to the clipboard", ) - arguments = parser.parse_args() return arguments diff --git a/src/sshare/plugins/config.py b/src/sshare/plugins/config.py index 79199ad..446bca2 100644 --- a/src/sshare/plugins/config.py +++ b/src/sshare/plugins/config.py @@ -12,16 +12,63 @@ # You should have received a copy of the GNU General Public License along with # SSHare. If not, see . -class NoDefault: - def __init__(self, flags=None): - self.flags = flags +class NoDefault: pass -class Default: - def __init__(self, value, flags=None): - self.value = value - self.flags = flags - -class Flags: - def __init__(self, short=None, long=None): - self.short = short - self.long = long +class Argument: + def __init__(self, + short=None, + long=None, + action='store', + nargs=None, + const=None, + default=None, + type=str, + choices=None, + required=False, + help=None, + metavar=None): + self.short = short + self.long = long + self.action = action + self.nargs = nargs + self.const = const + self.default = default + self.type = type + self.choices = choices + self.help = help + self.metavar = metavar or self.long or self.short + + def is_valid(self): + return (self.short != None and self.short != "") or (self.long != None and self.long != "") + + def pretty(self): + if self.short and self.long: + pretty = f"-{self.short}, --{self.long}" + elif self.long: + pretty = f"--{self.long}" + else: + pretty = f"-{self.short}" + return pretty + f" {self.help}" + + def set_for_plugin(self, plugin): + self.plugin = plugin + self.dest = f"{plugin.name}_{self.metavar}" + + def add(self, parser, used_arguments): + parser.add_argument( + f"-{self.short}", + f"--{self.long}", + action=self.action, + nargs=self.nargs, + const=self.const, + default=self.default, + type=self.type, + choices=self.choices, + help=self.help, + metavar=self.metavar, + dest=self.dest, + ) + if self.short: + used_arguments["short"] = self.plugin + if self.long: + used_arguments["long"] = self.plugin diff --git a/src/sshare/plugins/default/current_time.py b/src/sshare/plugins/default/current_time.py index 1c37444..72b5ffe 100644 --- a/src/sshare/plugins/default/current_time.py +++ b/src/sshare/plugins/default/current_time.py @@ -14,8 +14,7 @@ import time -from ..config import Default -from ..config import Flags +from ..config import Argument from ..source import File plugin_type = "name" @@ -23,6 +22,13 @@ plugin_type = "name" config = { "base": 62, } +args = { + "base": Argument( + short="b", + long="base", + help="Set the numeric base to use for the current time" + ) +} def init(): if not isinstance(config.base, int): diff --git a/src/sshare/plugins/default/file.py b/src/sshare/plugins/default/file.py index 36bcc50..9676bef 100644 --- a/src/sshare/plugins/default/file.py +++ b/src/sshare/plugins/default/file.py @@ -12,19 +12,19 @@ # You should have received a copy of the GNU General Public License along with # SSHare. If not, see . +from ..config import Argument from ..config import NoDefault -from ..config import Flags from ..source import File plugin_type = "source" -config = { - "file": NoDefault( - flags=Flags( - short="f", - long="file" - ) - ), +activate = [ "file" ] +args = { + "file": Argument( + short="f", + long="file", + help="Upload a file" + ) } def get(): diff --git a/src/sshare/plugins/default/ssh.py b/src/sshare/plugins/default/ssh.py index b85819b..b06325d 100644 --- a/src/sshare/plugins/default/ssh.py +++ b/src/sshare/plugins/default/ssh.py @@ -15,23 +15,21 @@ import getpass import subprocess -from ..config import Default from ..config import NoDefault -from ..config import Flags from ..source import File from ..source import Raw plugin_type = "upload" config = { - "host": NoDefault(), - "path": NoDefault(), + "host": NoDefault, + "path": NoDefault, "port": 22, "user": getpass.getuser(), } def upload(name, source): - logger.info(f"Uploading to {config.user}@{config.host}/{config.port}:{config.path}/{name}") + logger.info(f"Uploading to {config.user}@{config.host}:{config.path}/{name} on port {config.port}") if isinstance(source, File): command = [ "scp",