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)