Compare commits

...

41 commits
v1.0.0 ... mia

Author SHA1 Message Date
ad51984991
Don't crash if there is no plugin directory 2024-10-05 18:42:57 +00:00
084ff6e503
Fix time plugin 2024-10-05 18:30:42 +00:00
d99e0db019
Cleanup part 2 2024-10-05 18:04:32 +00:00
2d5041659f
Yeah yeah ok GPL I get it 2024-10-05 17:06:40 +00:00
88e5aed667
Cleanup part 1 2024-09-25 04:29:49 +00:00
92f5e1f47a
Add rudimentary support for specifications 2024-09-25 01:55:30 +00:00
0f3b523d68
Oops 2024-09-18 22:17:16 +01:00
f0f49cce18
Who let the bytecode out of the bag >:( 2024-09-14 18:01:06 +00:00
1a9001e956
Added a simple plugin validator 2024-09-14 17:56:10 +00:00
552146551c
Some reorganising 2024-09-14 16:56:57 +00:00
babba91ca2
Configuration directory abstraction 2024-09-10 23:27:19 +00:00
80057f151a
LICENSE bug 2024-09-10 22:57:31 +00:00
f627664244
Update examples 2024-09-10 15:11:20 +00:00
1ee3ea73e6
Reorganised package 2024-09-10 15:05:08 +00:00
12dbe87134
Add 'stdin' plugin. Allow flags to be empty (e.g. '-' and '--') 2024-09-09 18:34:23 +00:00
fb79537707
Added some basic validation 2024-09-09 18:19:50 +00:00
349a9cbd0e
SETUPTOOLS_SCM_PRETEND_VERSION my beloved?? 2024-09-09 17:14:35 +00:00
846a33a941
Flags are now configurable. Plugins no longer provide default short flags. 2024-09-09 05:47:02 +00:00
d95038a650
Updated README.md 2024-09-08 22:50:25 +00:00
6d549d27c5
Split 'result' plugin type into 'location' and 'feedback'
> 'location' plugins generate locations the resource can be accessed
> 'feedback' plugins present that location in some way that's useful to the human user

Also don't user relative imports in default plugins
2024-09-08 21:14:22 +00:00
6a310b8ca9
Added --config flag 2024-09-08 20:28:33 +00:00
10b5a80a09
Add PluginManager class and move logic from main => PluginManager or Plugin as appropriate 2024-09-08 20:12:20 +00:00
528115691a
Use __str__ to print plugin arguments 2024-09-04 17:53:01 +00:00
86380766ac
loggers => _loggers 2024-09-04 17:51:58 +00:00
a9fa15d643
Refactoring
- Flatten plugin module into Plugin class
- Move logger to its own class and rework it slightly
- Rename source and name to get_source and get_name since name() now clashes with the plugin name
2024-09-03 19:47:49 +00:00
00f3321230
Extract Plugin class and plugin loading from main.py 2024-09-02 21:33:11 +00:00
2a0b3dcb56
Removed antiquated dependency 2024-09-01 21:06:51 +00:00
fd32d625a8
Updated examples 2024-09-01 21:05:43 +00:00
34e0527daf
https://duckduckgo.com/?t=ffab&q=how+to+run+a+setuptools+project
Send help!
2024-09-01 21:05:43 +00:00
0140759c88
Move GNOME 42 Screenshot problem to issue 2024-08-31 21:43:46 +00:00
55115a2925
Added notification example plugin 2024-08-31 21:35:57 +00:00
7e3e713b0d
Several changes
- Plugin configurations are only validated if they are active
- Fixed issue with how Argument was passing it's paramateres to add_argument
- Added new plugin type 'result'
- Added inbuilt log_result plugin
- Added clipboard plugin
- Fix issue with misidentifying non-plugins in plugins directory as plugins
2024-08-31 19:56:48 +00:00
e6a42988c6
Added examples submodule 2024-08-31 18:50:28 +00:00
dd46dbc7b7
Modifications to arguments
- Arguments now use NoArgument instead of None to identify unset flags
- Arguments can now be optional and an activation flag using nargs="?" and const=Flag
2024-08-31 18:40:46 +00:00
987eddbf00
Arguments are working :fingers_crossed: 2024-08-31 18:25:53 +00:00
7b8fbf672b
Reorganised to use a plugin system 2024-08-31 05:43:30 +00:00
98c490dab9
Plugins are loading 2024-08-28 20:23:52 +00:00
72627995a7
Loaded a module :D 2024-08-28 16:56:05 +00:00
c1a455d7a4
Bump version 2024-08-28 06:43:21 +00:00
f6cc7033b8
Bump version 2024-08-01 18:18:50 +00:00
97ea9d1028
Convert ids to base 62 (shorter url) 2024-08-01 17:56:06 +00:00
25 changed files with 1375 additions and 385 deletions

3
.gitignore vendored
View file

@ -159,3 +159,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# setuptools_scm version file
src/sshare/version.py

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "examples"]
path = examples
url = git@forge.monodon.me:Gnarwhal/sshare_plugins.git

143
LICENSE
View file

@ -1,5 +1,5 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
@ -7,15 +7,17 @@
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
@ -24,34 +26,44 @@ them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
@ -60,7 +72,7 @@ modification follow.
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
@ -537,45 +549,35 @@ to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
@ -633,29 +635,40 @@ the "copyright" line and a pointer to where the full notice is found.
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
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.
This program 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 Affero General Public License for more details.
GNU General Public License for more details.
You should have received a copy of the GNU Affero General Public License
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

372
README.md
View file

@ -2,146 +2,266 @@
Upload files to a server via ssh
## Documentation
## Installing
### Arguments
#### `-h` `--help`
Show the help message and exit
#### `-v` `--version`
Show the program's version number and exit
#### `-l` `--latest`
Upload the most recently modified file in `source_directory`
#### `-p` `--paste`
Upload the contents of the clipboard as a `.txt` file
#### `-f FILE` `--file FILE`
Upload a file
#### `-c` `--copy`
Copy the resultant URL to the clipboard
### Configuration File
The configuration file is located at `$XDG_CONFIG_DIR/sshare/config.toml`.
If `XDG_CONFIG_DIR` is not set, then it will read from `$HOME/.config/sshare/config.toml` instead.
#### `source_directory`
The directory from which `--latest` reads.
- [x] Optional - Cannot use `--latest` if it is not set
#### `host`
SSHare is not available on [PyPI](https://pypi.org).
To install it you will need to clone the repo and use pip to install it
```
https://example.com:4430/path/to/files/*
^---^ ^---------^ ^--^^------------^
| | | |
| | | +- `host.path` = "/path/to/files"
| | +----- `host.port` = 4430
| +----------------- `host.name` = "example.com"
+------------------------- `host.protocol` = "https"
https://example.com/*
^---------^
|
| `host.path` Not set
| `host.port` Not set
+----------------- `host.name` = "example.com"
`host.protocol` Not set (Defaults to `https`)
```
#### `host.protocol`
The protocol by which the file is served
- [x] Optional - Default: `https`
#### `host.name`
The name of the host (e.g. `example.com`, `1.1.1.1`)
- [ ] Not optional
#### `host.port`
The port from which the host is serving files
- [x] Optional - Default: `None` (which means the default port for the protocol)
#### `host.path`
The subpath from which the host is serving files. If set, `host.path` must start
with and **NOT** end with a `/` (e.g. `/path/to/files` not `path/to/files/`)
- [x] Optional - If the host is serving from the root
#### `ssh`
```
ssh -p 1234 example@example.com:/srv/static
^--^ ^-----^ ^---------^ ^---------^
| | | |
| | | +- `ssh.path` = "/srv/static"
| | +------------- Uses `host.name`
| +--------------------- `ssh.user` = "example"
+-------------------------- `ssh.port` = 1234"
ssh example@example.com:/srv/static
^-----^ ^---------^ ^---------^
| | |
| | +- `ssh.path` = "/srv/static"
| +------------- Uses `host.name`
+--------------------- `ssh.user` Doesn't need to be set if `example` is
the name of the user running `sshare`
`ssh.part` Not set (Defaults to 22)
git clone https://forge.monodon.me/Gnarwhal/sshare.git
cd sshare
pip install ./
```
#### `ssh.port`
The port the server is listening for ssh connections on
- [x] Optional - Default: 22
By default, the only dependency SSHare has is the system's `ssh` (and `scp`) commands.
Beyond that it only utilises the python standard library.
#### `ssh.user`
The user to connect to the host with
- [x] Optional - Default: The user that ran `sshare`
### Additional Functionality
#### `ssh.path`
The directory on the host from which static files are being hosted. If set,
`ssh.path` must **NOT** end with a path (e.g. `/srv/static` not `/srv/static/`)
- [ ] Not optional
The base install is fairly bare bones in terms of functionality.
Included in the [`examples/`](https://forge.monodon.me/Gnarwhal/sshare_plugins) submodule, there is a sampling of
plugins which provide some additional conveniences
#### Example Configuration File
Refer to the above documentation for information on installing plugins
## Documentation - End User
Getting started with SSHare is about getting familiar with configuring SSHare.
SSHare configuration is all done in the `${XDG_CONFIG_DIR}/sshare` if set, or in `~/.config/sshare` otherwise.
In this directory are two important items: the `config.toml` file and the `plugins/` directory.
### `plugins/`
The `plugins/` directory is home to all external plugins. Installing a plugin is as simple as saving the
the plugin's `.py` file to the `plugins/`.
### `config.toml`
The configuration file can be broken down into three major components: `spec`, `config`, and `flags`.
#### `spec`
```toml
# config.toml
source_directory = "/home/example/Pictures/Screenshots"
[spec.default]
name = [ "time", "extension" ]
# Host is serving static files at https://example.com:443/sshare/*
# Note: both protocol and port would be optional here as https is
# the default protocol and port 443 is the default https port
[host]
protocol = "https"
name = "example.com"
port = 443
path = "/sshare"
[spec.example]
flag = { short = "e", long = "example" }
name = [ "preserve", "extension" ]
# Host is listening for ssh connections on port 1234 and
# serving files from the `/srv/sshare` directory
[ssh]
port = 1234
user = "exampleuser2"
path = "/srv/sshare"
[spec.noshort]
flag = { long = "only" }
name = [ "time" ]
help = "No short flag, only long flag >:("
```
## Hmmm
Apparently GNOME 42 screenshot utility is called via dbus
The `spec` section is a collection of different runtime specifications. The name of a specification is
unimportant with the exception of `default`, which is run when no other spec is activated. In each specification
that isn't `default`, there is a `flag` attribute that sets which command line arguments activate the
specification. The `flag` attribute must have one or both of `short` or `long`, where `short` will be set the flag
`-{short}` and `long` will set the flag `--{long}`. The `name` attribute of a specification is an array of name type
plugins which controls the order in which said plugins are executed. Specifications can also provide a brief description
in the `help` attribute, which will be displayed when `sshare --help` is run. In the example above,
running `sshare --help` would give
```
gdbus call
--session
--dest org.gnome.Shell
--object-path /org/gnome/Shell
--method org.gnome.Shell.Eval 'Main.screenshotUI.open()'
````
but it cannot be called unless `global.context.unsafe_mode = false`
is set in gnome shell...which is unideal.
options:
-e, --example Use example spec
--only No short flag, only long flag >:(
```
#### `config`
The ideal would be to launch a desired screenshot (or other generative)
utility before running sshare, and for everything that isn't GNOME 42 screenshot,
I would not expect it to be too hard. But unfortunately I want to use
GNOME 42 screenshot ;-;
```toml
[config.ssh]
host = "example.com"
path = "/directory/to/store/files/in"
port = 22
```
The `config` section is used to set configuration options for plugins. Refer to documentation for a plugin to see
what options can be set here. Documentation for default plugins is included [below](#Default-Plugins).
#### `flags`
```toml
[flags.file]
file = { short = "f" }
[flags.stdin]
stdin = { short = "" }
[flags.example]
option = { short = "o", long = "ooooption" }
```
The `flags` section is used to bind plugin arguments to a command line argument. They are specified in the same way
as the `flag` option for `spec`s. If `long` is left unspecified, the long flag will be set to the default provided by
the plugin. In the example above, running `sshare --help` would give:
```
options:
-f file, --file file Upload a file
-, --stdin Upload from stdin
-o xmpl, --ooooption xmpl
This is not a real plugin that exists
```
### Default Plugins
#### `command_line`
A `logger` plugin which prints to the command line. No configuration needed.
#### `extension`
A `name` plugin which appends the source type to the end of the file name (i.e. `txt`, `png`, etc...). No configuration needed.
#### `file`
A `source` plugin which provides a file to be uploaded. File is specified with the `--file {file_path}` argument. No configuration needed.
#### `preserve`
A `name` plugin which preserves the name of the source in the uploaded file. No configuration needed.
#### `print_location`
A `feedback` plugin which prints the location that sources can be accessed from. No configuration needed.
#### `ssh`
An `upload` plugin which uploads the sources to the server using `ssh`.
##### Configuration
| Attribute | Type | Default | Description |
|-----------|--------|---------|------------------------------------------------------|
| host | string | None | The hostname to ssh into |
| path | string | None | The path of the directory to upload the sources into |
| port | int | 22 | The port to ssh into |
| user | string | $USER | The user to ssh as |
#### `stdin`
A `source` plugin which reads from stdin. No configuration needed.
#### `time`
A `name` plugin which adds the current unix time to the name of the of the uploaded file.
##### Configuration
| Attribute | Type | Default | Description |
|-----------|--------|---------|------------------------------------------------------|
| format | int | 62 | The base between 2 and 62 to represent the number in |
##### `uri`
A `location` plugin which constructs the resulting URI that the uploaded file can be accessed from.
##### Configuration
| Attribute | Type | Default | Description |
|-----------|--------|---------|------------------------------------------------------|
| protocol | string | https | The protocol of the URI |
| host | string | None | The hostname the file can be accessed from |
| port | int | 22 | The port the file can be accessed at |
| path | string | None | The path the file can be accessed at |
## Documentation - Plugin Development
A plugin for SSHare is just a python file (`*.py`). There are currently 6 types of plugins:
- `logger`
- `source`
- `name`
- `upload`
- `location`
- `feedback`
A plugin can specify be any combination of the above types. For examples of plugins see [here](src/sshare/plugins)
and [here](https://forge.monodon.me/Gnarwhal/sshare_plugins)
### General Attributes
Every plugin regardless of type specifies or is provided with these attributes.
#### `plugin_type`
This mandatory paramater specifies what type(s) this plugin is.
It can be either a:
- `string` - Promotes to `{ string }`
- `set` - A set containing each of the plugin's types.
#### `activate`
This optional parameter specifies what flag(s) or argument(s) must be passed for this plugin to be activated.
It can be either a:
- `string` - Promotes to `{ string }`
- `set` - Promotes to `{ plugin_type: { string, ... }, plugin_type2: { string, ... }, ... }`
- `dict` - A dictionary that maps each type the plugin is, to the flags or arguments that activate the plugin for that type
All arguments specified in `activate` must be provided by the user for the plugin to activate.
#### `config`
This optional parameter specifies configuration options for the plugin. These values are what a user is allowed to change
through `config.toml`. It is a map containing `{ option: default_value, ... }`. If there is no default value (i.e. it is mandatory
that the user set it explicitly) for the option, it can be set to `NoDefault` provided by
`from sshare.plugin.config import NoDefault`.
While `config` is initially specified as a `dict`, when the plugin is loaded it will be converted to an object with attributes.
For example, if a config is specified as
```python
config = {
"example0": 42,
}
```
it would then be accessed by `config.example` not `config["example"]`.
#### `args`
This optional parameter specifies arguments for the plugin. These values are also accessed from the `config` object,
however they are provided via program arguments as opposed to being specified in `config.toml`. An option specified
in both `config` and `args` will be loaded from the config file first and overriden by the program argument if
provided. Arguments are of type `Argument` provided by `from sshare.plugin.config import Argument`.
`Argument` takes `name` (optionally) and a list of `kwargs` equivalent to the option document [here](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_argument). As a convenience there is also `Flag` provided by
`from sshare.plugin.config import Flag`, which is for boolean arguments which take no parameters.
`Flag` only takes a `help=...` paramater.
#### `init`
If a plugin needs to do any initialisation, it can be done in the `init` method. The `init` method takes no parameters
and returns no values.
#### `logger`
Logger is not specified by the plugin developer, but is available inside the plugin if needed. The logger
has three levels: `info`, `warn` and `error`.
### Type Specific Attributes
#### `logger`
- `info(str)`
- `warn(str)`
- `error(str)`
#### `source` -> `get_source()`
The `get_source` function takes no arguments and returns a source. There are currently two types of sources provided by
`from sshare.plugin.source import (Raw | File)`.
- `Raw` - A raw data source. It has a type, a source name, and a byte array providing the data.
- `File` - A file or directory source. It has only the path to the file.
#### `name` -> `get_name(current_name, source)`
`name` plugins are chained one after the other. The first `name` plugin is provided an empty string for `current_name`.
Each subsequent `name` plugin is provided the output of the previous `name` plugin's `get_name` function. The `source`
parameter is the either `Raw` or `File` data source.
#### `upload` -> `upload(name, source)`
`upload` plugins are responsible for getting the source to the destination.
#### `location` -> `get_location(name)`
The `get_location` function takes in the name of a source and returns a location that the source can now be accessed from
(e.g. a URL).
#### `feedback` -> `give_feedback(location)`
The `give_feedback` function takes output from `location` plugins and presents it to the user (e.g. printing to console,
desktop notification. etc...).
What to do...

1
examples Submodule

@ -0,0 +1 @@
Subproject commit 99f2e9c3d2fbab02b0582dc8dcf9ed05df7789a6

View file

@ -1,10 +1,10 @@
[build-system]
requires = ["setuptools"]
requires = ["setuptools", "setuptools_scm"]
build-backend = "setuptools.build_meta"
[project]
name = "sshare"
version = "1.0.0"
dynamic = ["version"]
authors = [
{ name = "Gnarwhal", email = "git.aspect893@passmail.net" },
]
@ -16,15 +16,21 @@ classifiers = [
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
]
requires-python = ">=3.11"
dependencies = [
"pyclip",
]
[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"
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"
sshare-validate = "sshare.validator:main"
[tool.setuptools_scm]
version_file = "src/sshare/version.py"
[tool.pytest.ini_options]
addopts = [
"--import-mode=importlib"
]

View file

@ -1,183 +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 <https://www.gnu.org/licenses/>.
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 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 = 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

View file

@ -0,0 +1,36 @@
# SSHare - upload files to a server
# Copyright (c) 2024 Gnarwhal
#
# 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 <https://www.gnu.org/licenses/>.
import os
from pathlib import Path
_LOCATION = Path(os.environ.get("XDG_CONFIG_DIR", f"{os.environ["HOME"]}/.config")) / "sshare"
def default_config():
return _LOCATION / "config.toml"
def plugins():
if (_LOCATION / "plugins").is_dir():
return [
path for
path in
(_LOCATION / "plugins").iterdir()
if path.is_file() and path.suffix == ".py"
]
else:
return []

45
src/sshare/logger.py Normal file
View file

@ -0,0 +1,45 @@
# SSHare - upload files to a server
# Copyright (c) 2024 Gnarwhal
#
# 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 <https://www.gnu.org/licenses/>.
import sys
from sshare.plugin import Plugin
class Logger:
def __init__(self, *args, **kwargs):
self._loggers = []
self.add(*args)
def add(self, *args, **kwargs):
for logger in args:
self._loggers.append(logger)
def info(self, message):
for logger in self._loggers:
logger.info(message)
def warn(self, message):
for logger in self._loggers:
logger.warn(message)
def error(self, message):
for logger in self._loggers:
logger.error(message)
def fatal(self, message, error_code=1):
self.error(message)
sys.exit(error_code)

141
src/sshare/main.py Normal file
View file

@ -0,0 +1,141 @@
# SSHare - upload files to a server
# Copyright (c) 2024 Gnarwhal
#
# 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 <https://www.gnu.org/licenses/>.
import argparse
import getpass
import os
import time
import tomllib
import subprocess
import sys
from sshare import config_directory
from sshare.logger import Logger
from sshare.plugin import Plugin
from sshare.plugin import PluginManager
from sshare.version import version
def main():
arg_parser = argparse.ArgumentParser(
prog="sshare",
description="Upload files to a server via ssh",
add_help=False,
)
arg_parser.add_argument(
"--config",
metavar="config",
help="Specify location of config file to use"
)
arguments, _ = arg_parser.parse_known_args()
with open(arguments.config or config_directory.default_config(), mode="rb") as file:
config = tomllib.load(file)
config["spec"] = config.get("spec", {})
for spec_name, spec in config["spec"].items():
if spec_name != "default":
flags = []
if spec.get("flag", {}).get("short") != None:
flags = flags + [ f"-{spec["flag"]["short"]}" ]
if spec.get("flag", {}).get("long") != None:
flags = flags + [ f"--{spec["flag"]["long"]}" ]
arg_parser.add_argument(
*flags,
action="store_const",
const=True,
default=False,
help=spec.get(help, f"Use {spec_name} spec"),
dest=spec_name,
)
use_spec = config["spec"].get("default", {})
arguments, _ = arg_parser.parse_known_args()
for spec_name, spec in config["spec"].items():
if spec_name != "default":
if getattr(arguments, spec_name):
use_spec = spec
config["config"] = config.get("config", {})
config["flags" ] = config.get("flags", {})
arg_parser.add_argument(
"-h",
"--help",
action="help",
help="show this help message and exit"
)
arg_parser.add_argument(
"-v",
"--version",
action="version",
version=f"%(prog)s version {version}",
)
# Load command line early and set it as the active logger
# so that it can be used to report errors while loading and
# configuring other loggers
logger = Logger()
command_line = Plugin.internal(
"command_line",
logger,
config["config"],
config["flags"],
)
logger.add(command_line)
plugins = PluginManager(
logger,
use_spec,
config["config"],
config["flags" ],
arg_parser,
)
plugins.activate("logger")
logger.add(*plugins.logger.active)
plugins.activate()
for plugin_type in [ "source", "name", "upload" ]:
plugins_of_type = getattr(plugins, plugin_type)
if len(plugins_of_type.active) == 0:
logger.error(f"Error: No '{plugin_type}' plugins activated. Available plugins:")
for plugin in plugins_of_type.inactive:
logger.error(f" => {plugin.name}")
sys.exit(1)
if len(plugins.location.active) == 0 and len(plugins.feedback.active) > 0:
logger.warn("Warning: 'feedback' plugins activated with no active 'location' plugins")
sources = []
for plugin in plugins.source.active:
sources.append(plugin.get_source())
for index, source in enumerate(sources):
name = ""
for plugin in plugins.name.active:
name = plugin.get_name(name, source)
sources[index] = name, source
for name, source in sources:
for plugin in plugins.upload.active:
plugin.upload(name, source)
for index, (name, _) in enumerate(sources):
for plugin in plugins.location.active:
sources[index] = plugin.get_location(name)
for location in sources:
for plugin in plugins.feedback.active:
sources[index] = plugin.give_feedback(location)
sys.exit(0)

View file

@ -1,3 +1,6 @@
# SSHare - upload files to a server
# Copyright (c) 2024 Gnarwhal
#
# This file is part of SSHare.
#
# SSHare is free software: you can redistribute it and/or modify it under the terms of
@ -12,6 +15,4 @@
# You should have received a copy of the GNU General Public License along with
# SSHare. If not, see <https://www.gnu.org/licenses/>.
from cli import main
main()
from .plugin import *

View file

@ -0,0 +1,89 @@
# SSHare - upload files to a server
# Copyright (c) 2024 Gnarwhal
#
# 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 <https://www.gnu.org/licenses/>.
class NoDefault: pass
def Flag(name=None, help=None):
return Argument(
name,
action="store_const",
const=Flag,
default=False,
help=help,
)
class Argument:
def __init__(self, name=None, **kwargs):
class _None:
def __init__(self, default):
self.default = default
self._None = _None
self._short = None
self._long = name
if not "default" in kwargs:
kwargs["default"] = NoDefault
kwargs["default"] = _None(kwargs["default"])
self._kwargs = kwargs
def bind(self, plugin, argument):
self._plugin = plugin.name
if self._long == None:
self._long = argument
self._kwargs["metavar"] = argument
def set_flags(self, short, long):
if short != None:
if short == False:
self._short = None
else:
self._short = short
if long != None:
if long == False:
self._long = None
else:
self._long = long
def default(self):
value = self._kwargs["default"]
if isinstance(value, self._None):
value = value.default
return value
def dest(self):
return f"{self._plugin}_{self._kwargs["metavar"]}"
def extract(self, arguments):
value = getattr(arguments, self.dest())
was_set = True
if isinstance(value, self._None):
was_set = False
value = value.default
return was_set, value
def add(self, arg_parser):
flags = []
if self._short != None: flags.append(f"-{self._short}")
if self._long != None: flags.append(f"--{self._long}")
kwargs = self._kwargs | {
"dest": self.dest()
}
arg_parser.add_argument(
*flags,
**kwargs
)

211
src/sshare/plugin/plugin.py Normal file
View file

@ -0,0 +1,211 @@
# SSHare - upload files to a server
# Copyright (c) 2024 Gnarwhal
#
# 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 <https://www.gnu.org/licenses/>.
import importlib
import importlib.util
import sys
from pathlib import Path
from sshare.plugin.config import Flag
from sshare.plugin.config import NoDefault
from sshare import config_directory
class PluginLoader:
@staticmethod
def all(command_line=False, logger=None, config=dict(), flags=dict()):
return PluginLoader.internal(command_line, logger, config, flags) + PluginLoader.external(logger, config, flags)
@staticmethod
def internal(command_line=False, logger=None, config=dict(), flags=dict()):
return [
Plugin.internal(plugin, logger, config, flags)
for plugin
in ([ "command_line" ] if command_line else []) + [
"file",
"stdin",
"preserve",
"time",
"extension",
"ssh",
"uri",
"print_location",
]
]
@staticmethod
def external(logger=None, config=dict(), flags=dict()):
return [
Plugin.external(plugin, logger, config, flags)
for plugin
in config_directory.plugins()
]
@staticmethod
def at(*args, logger=None, config=dict(), flags=dict()):
return [
Plugin.external(Path(plugin), logger, config, flags)
for plugin
in args
]
class PluginManager:
def __init__(self, logger, spec, config, flags, arg_parser):
self._logger = logger
self._arg_parser = arg_parser
class PluginState:
def __init__(self):
self.active = []
self.inactive = []
for type in Plugin.types():
setattr(self, type, PluginState())
self._uninitialized = []
uninitialized = PluginLoader.all(
command_line=False,
logger=logger,
config=config,
flags=flags,
)
for plugin in uninitialized:
plugin.add_args(arg_parser)
if not "name" in plugin.plugin_type:
self._uninitialized.append(plugin)
for name in spec.get("name", []):
for plugin in uninitialized:
if plugin.name == name:
self._uninitialized.append(plugin)
def activate(self, activate_type=None):
args = self._arg_parser.parse_args()
for plugin in self._uninitialized.copy():
if activate_type == None or activate_type in plugin.plugin_type:
self._uninitialized.remove(plugin)
active = plugin.load_args_and_activate(args)
for type in plugin.plugin_type:
getattr(
getattr(self, type),
active[type],
).append(plugin)
class Plugin:
@staticmethod
def types(methods=False):
types = {
"logger": { "info", "warn", "error" },
"source": { "get_source" },
"name": { "get_name" },
"upload": { "upload" },
"location": { "get_location" },
"feedback": { "give_feedback"},
}
if methods:
return types
else:
return types.keys()
def __init__(self, name, module, logger, external_config, external_flags):
self.__dict__ = module.__dict__
self.name = name
if logger == None:
return
self.logger = logger
if not isinstance(self.plugin_type, set):
self.plugin_type = { self.plugin_type }
if hasattr(self, "activate"):
if not isinstance(self.activate, dict):
activate = self.activate
self.activate = dict()
for plugin_type in self.plugin_type:
self.activate[plugin_type] = activate
else:
self.activate = dict()
for plugin_type in self.plugin_type:
self.activate[plugin_type] = set()
if hasattr(self, "config"):
config = self.config
else:
config = dict()
if external_config == None:
external_config = dict()
for key in config.keys():
if key in external_config:
config[key] = external_config[key]
if hasattr(self, "args"):
for arg in self.args.items():
arg[1].bind(self, arg[0])
flags = external_flags.get(arg[0], dict())
arg[1].set_flags(flags.get("short"), flags.get("long"))
value = arg[1].default()
if value != NoDefault:
config[arg[0]] = value
else:
self.args = dict()
class Config: pass
flat_config = Config()
flat_config.__dict__ = config
self.config = flat_config
def add_args(self, arg_parser):
for arg in self.args.values():
arg.add(arg_parser)
def load_args_and_activate(self, args):
passed_args = set()
for arg_name, arg in self.args.items():
was_set, value = arg.extract(args)
if was_set:
if value != Flag:
setattr(self.config, arg_name, value)
passed_args.add(arg_name)
activate = dict()
run_init = False
for type in self.plugin_type:
if self.activate[type] <= passed_args:
activate[type] = "active"
run_init = True
else:
activate[type] = "inactive"
if run_init and hasattr(self, "init"):
self.init()
return activate
@staticmethod
def internal(name=None, logger=None, config=dict(), flags=dict()):
return Plugin(name, importlib.import_module(f"sshare.plugins.{name}"), logger, config.get(name, dict()), flags.get(name, dict()))
@staticmethod
def external(path, logger=None, config=dict(), flags=dict()):
sys.dont_write_bytecode = True
module_spec = importlib.util.spec_from_file_location(
path.stem,
path.as_posix(),
)
module = importlib.util.module_from_spec(module_spec)
module_spec.loader.exec_module(module)
sys.dont_write_bytecode = False
return Plugin(path.stem, module, logger, config.get(path.stem, dict()), flags.get(path.stem, dict()))

View file

@ -0,0 +1,28 @@
# SSHare - upload files to a server
# Copyright (c) 2024 Gnarwhal
#
# 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 <https://www.gnu.org/licenses/>.
from pathlib import Path
class Raw:
def __init__(self, name, type, data):
self.name = name
self.type = type
self.data = data
class File:
def __init__(self, path):
self.path = Path(path)

View file

@ -0,0 +1,30 @@
# SSHare - upload files to a server
# Copyright (c) 2024 Gnarwhal
#
# 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 <https://www.gnu.org/licenses/>.
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)

View file

@ -0,0 +1,37 @@
# SSHare - upload files to a server
# Copyright (c) 2024 Gnarwhal
#
# 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 <https://www.gnu.org/licenses/>.
from sshare.plugin.source import File
from sshare.plugin.source import Raw
plugin_type = "name"
def get_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}"

View file

@ -0,0 +1,35 @@
# SSHare - upload files to a server
# Copyright (c) 2024 Gnarwhal
#
# 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 <https://www.gnu.org/licenses/>.
from sshare.plugin.config import Argument
from sshare.plugin.config import NoDefault
from sshare.plugin.source import File
plugin_type = "source"
activate = { "file" }
args = {
"file": Argument(help="Upload a file")
}
def get_source():
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

View file

@ -0,0 +1,35 @@
# SSHare - upload files to a server
# Copyright (c) 2024 Gnarwhal
#
# 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 <https://www.gnu.org/licenses/>.
from sshare.plugin.source import File
from sshare.plugin.source import Raw
plugin_type = "name"
def get_name(name, source):
if name != "":
name = name + "_"
else:
name = ""
if isinstance(source, File):
components = source.path.name.split(".")
if components[0] == "":
return name + "." + components[1]
else:
return name + components[0]
else:
return name + source.name

View file

@ -0,0 +1,21 @@
# SSHare - upload files to a server
# Copyright (c) 2024 Gnarwhal
#
# 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 <https://www.gnu.org/licenses/>.
plugin_type = "feedback"
def give_feedback(location):
logger.info(f"Uploaded to '{location}'")

65
src/sshare/plugins/ssh.py Normal file
View file

@ -0,0 +1,65 @@
# SSHare - upload files to a server
# Copyright (c) 2024 Gnarwhal
#
# 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 <https://www.gnu.org/licenses/>.
import getpass
import subprocess
from sshare.plugin.config import NoDefault
from sshare.plugin.source import File
from sshare.plugin.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")

View file

@ -0,0 +1,34 @@
# SSHare - upload files to a server
# Copyright (c) 2024 Gnarwhal
#
# 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 <https://www.gnu.org/licenses/>.
import sys
from sshare.plugin.config import Flag
from sshare.plugin.source import Raw
plugin_type = "source"
activate = { "stdin" }
config = {
"suffix": "txt"
}
args = {
"stdin": Flag(help="Upload from stdin")
}
def get_source():
return Raw("stdin", config.suffix, sys.stdin.buffer.read())

View file

@ -0,0 +1,51 @@
# SSHare - upload files to a server
# Copyright (c) 2024 Gnarwhal
#
# 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 <https://www.gnu.org/licenses/>.
import time
from sshare.plugin.config import Argument
from sshare.plugin.source import File
plugin_type = "name"
config = {
"format": 62,
}
def get_name(name, source):
if name != "":
name = name + "_"
return name + _rebase(config.format, 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) # 0-9
elif number < 36:
return chr(number + 87) # a-z
else:
return chr(number + 29) # A-Z

34
src/sshare/plugins/uri.py Normal file
View file

@ -0,0 +1,34 @@
# SSHare - upload files to a server
# Copyright (c) 2024 Gnarwhal
#
# 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 <https://www.gnu.org/licenses/>.
from sshare.plugin.config import NoDefault
plugin_type = "location"
config = {
"protocol": "https",
"host": NoDefault,
"port": None,
"path": "",
}
def get_location(name):
if config.port:
config.port = f":{config.port}"
else:
config.port = ""
return f"{config.protocol}://{config.host}{config.port}{config.path}/{name}"

135
src/sshare/validator.py Normal file
View file

@ -0,0 +1,135 @@
# SSHare - upload files to a server
# Copyright (c) 2024 Gnarwhal
#
# 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 <https://www.gnu.org/licenses/>.
import argparse
from sshare import config_directory
from sshare.plugin import Plugin
from sshare.plugin import PluginLoader
from sshare.version import version
def validate(plugin, quiet=False):
def log(message):
if not quiet:
print(message)
error_count = 0
def error(message, end=False):
nonlocal error_count
nonlocal log
if error_count == 0:
log("[\033[91mFAILED\033[0m]")
error_count = error_count + 1
log(f"\033[91m => {message}\033[0m")
def success():
nonlocal log
log(f"[\033[92mOK\033[0m]")
mandatory_keys = dict()
def validate_type_and_add_keys(type):
nonlocal mandatory_keys
types = Plugin.types(methods=True)
if type in types:
mandatory_keys[type] = types[type]
else:
error(f"plugin_type '{type}' is not a valid plugin type")
if not hasattr(plugin, "plugin_type"):
error("must define plugin_type")
else:
if isinstance(plugin.plugin_type, set):
for index, type in enumerate(plugin.plugin_type):
validate_type_and_add_keys(type)
elif isinstance(plugin.plugin_type, str):
validate_type_and_add_keys(plugin.plugin_type)
else:
error("plugin_type must be either a string or a set of strings")
for type, keys in mandatory_keys.items():
for key in keys:
if not hasattr(plugin, key):
error(f"plugin type '{type}' requires definition of '{key}' ")
if hasattr(plugin, "activate"):
def validate_activate(args, type=None):
def validate_activate_arg(arg):
nonlocal plugin
nonlocal type
if hasattr(plugin, "args"):
if arg in plugin.args.keys():
return
error(f"activate arg '{arg}' does not exist in 'args'")
if isinstance(args, set):
for arg in args:
validate_activate_arg(arg)
else:
validate_activate_arg(arg)
if isinstance(plugin.activate, dict):
for type, args in plugin.activate.items():
if not type in Plugin.types():
error(f"activate specified for unrecognized plugin type '{type}'")
validate_activate(args, type)
else:
validate_activate(plugin.activate)
if error_count == 0:
success()
return error_count
def main():
arg_parser = argparse.ArgumentParser(
prog="sshare-validate",
description="Validate sshare plugins",
)
arg_parser.add_argument(
"-v",
"--version",
action="version",
version=f"%(prog)s version {version}",
)
arg_parser.add_argument(
"--dev",
action="store_const",
const=True,
help="Validate all internal and external plugins"
)
arg_parser.add_argument(
"plugins",
nargs="*",
help="plugin(s) to be validated (Default: all external plugins)",
)
arguments = arg_parser.parse_args()
if arguments.dev:
to_be_validated = PluginLoader.all(command_line=True)
elif len(arguments.plugins) > 0:
to_be_validated = PluginLoader.at(*arguments.plugins)
else:
to_be_validated = PluginLoader.external()
longest = 0
for plugin in to_be_validated:
if len(plugin.name) > longest:
longest = len(plugin.name)
for plugin in to_be_validated:
print(f"{plugin.name}{"".join([ " " for _ in range(0, longest-len(plugin.name))])} ", end="")
validate(plugin)

View file

@ -1 +0,0 @@
version = "1.0.0"