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/pyproject.toml b/pyproject.toml
index b9b4472..c937898 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,9 +22,9 @@ 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"
[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/cli.py b/src/sshare/cli.py
deleted file mode 100644
index 87af8be..0000000
--- a/src/sshare/cli.py
+++ /dev/null
@@ -1,200 +0,0 @@
-# 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 argparse
-import getpass
-import os
-import os.path
-import pyclip
-import time
-import tomllib
-import subprocess
-import sys
-from sshare.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")
-
- 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
-
-
-def main():
- arguments = parse_arguments()
-
- config = Config()
-
- 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")
- 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")
-
- sys.exit(0)
-
-
-def parse_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}",
- )
- parser.add_argument(
- "-l",
- "--latest",
- action="store_const",
- const=True,
- help="Upload the latest image from the source directory",
- )
- parser.add_argument(
- "-p",
- "--paste",
- action="store_const",
- const=True,
- help="Upload the contents of the clipboard as a .txt file",
- )
- parser.add_argument(
- "-f",
- "--file",
- help="Upload a file",
- )
- parser.add_argument(
- "-c",
- "--copy",
- action="store_const",
- const=True,
- help="Copy the resultant URL to the clipboard",
- )
- arguments = parser.parse_args()
-
- return arguments
-
-
-def _latest(directory, key=os.path.getmtime):
- files = map(lambda file: f"{directory}/{file}", os.listdir(directory))
- selection = next(files)
- selection_key = key(selection)
- for file in files:
- new_key = key(file)
- if new_key > selection_key:
- selection = file
- selection_key = key
- return selection
diff --git a/src/sshare/main.py b/src/sshare/main.py
new file mode 100644
index 0000000..86e97b3
--- /dev/null
+++ b/src/sshare/main.py
@@ -0,0 +1,275 @@
+# 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 argparse
+import getpass
+import importlib
+import importlib.util
+import os
+import os.path
+import pyclip
+import time
+import tomllib
+import subprocess
+import sys
+from pathlib import Path
+from version import version
+
+from plugins.config import NoDefault
+
+class Congloggerate:
+ 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
+
+ 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
+ self.module = module
+
+def main():
+ config_directory = Path(os.environ.get("XDG_CONFIG_DIR") or f"{os.environ["HOME"]}/.config") / "sshare"
+ 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 = {}
+ for type in [ "logger", "source", "name", "upload" ]:
+ plugins[type] = { "active": [], "inactive": [] }
+
+ # Load external plugins
+ 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)
+ plugins_flat.append(Plugin(plugin.stem, module))
+ 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:
+ 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]] = 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:
+ 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)
+
+ 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 and check activation criteria
+ 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:
+ 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)
+
+ # 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"]["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"]["active"]:
+ name = plugin.module.name(name, source)
+ sources[index] = name, source
+
+ for (name, source) in sources:
+ for plugin in plugins["upload"]["active"]:
+ plugin.module.upload(name, source)
+
+ sys.exit(0)
+
+def parse_arguments():
+ parser = argparse.ArgumentParser(
+ prog = "SSHare",
+ description = "Upload files to a server via ssh",
+ )
+
+ parser.add_argument(
+ "-l",
+ "--latest",
+ action="store_const",
+ const=True,
+ help="Upload the latest image from the source directory",
+ )
+ parser.add_argument(
+ "-p",
+ "--paste",
+ action="store_const",
+ const=True,
+ help="Upload the contents of the clipboard as a .txt file",
+ )
+ parser.add_argument(
+ "-f",
+ "--file",
+ help="Upload a file",
+ )
+ parser.add_argument(
+ "-c",
+ "--copy",
+ action="store_const",
+ const=True,
+ help="Copy the resultant URL to the clipboard",
+ )
+
+ return arguments
+
+
+def _latest(directory, key=os.path.getmtime):
+ files = map(lambda file: f"{directory}/{file}", os.listdir(directory))
+ selection = next(files)
+ selection_key = key(selection)
+ for file in files:
+ new_key = key(file)
+ if new_key > selection_key:
+ selection = file
+ selection_key = key
+ return selection
diff --git a/src/sshare/plugins/config.py b/src/sshare/plugins/config.py
new file mode 100644
index 0000000..446bca2
--- /dev/null
+++ b/src/sshare/plugins/config.py
@@ -0,0 +1,74 @@
+# 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: pass
+
+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/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/plugins/default/command_line.py b/src/sshare/plugins/default/command_line.py
new file mode 100644
index 0000000..6b86cce
--- /dev/null
+++ b/src/sshare/plugins/default/command_line.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 .
+
+plugin_type = "logger"
+
+def _print_with_color(color, message):
+ print(f"\033[{color}m{message}\033[0m")
+
+def info(message):
+ _print_with_color(0, message)
+
+def warn(message):
+ _print_with_color(93, message)
+
+def error(message):
+ _print_with_color(91, message)
diff --git a/src/sshare/plugins/default/current_time.py b/src/sshare/plugins/default/current_time.py
new file mode 100644
index 0000000..72b5ffe
--- /dev/null
+++ b/src/sshare/plugins/default/current_time.py
@@ -0,0 +1,61 @@
+# 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 Argument
+from ..source import File
+
+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):
+ 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..9676bef
--- /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 Argument
+from ..config import NoDefault
+from ..source import File
+
+plugin_type = "source"
+
+activate = [ "file" ]
+args = {
+ "file": Argument(
+ short="f",
+ long="file",
+ help="Upload a 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..b06325d
--- /dev/null
+++ b/src/sshare/plugins/default/ssh.py
@@ -0,0 +1,62 @@
+# 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 NoDefault
+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.path}/{name} on port {config.port}")
+ 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)
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"