Compare commits
1 Commits
master
...
plugin-sup
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a6a56a11c7 |
18
Cargo.toml
18
Cargo.toml
@ -1,30 +1,19 @@
|
||||
[package]
|
||||
name="microbin"
|
||||
version="1.1.0"
|
||||
version="0.2.0"
|
||||
edition="2021"
|
||||
authors = ["Daniel Szabo <daniel.szabo99@outlook.com>"]
|
||||
license = "BSD-3-Clause"
|
||||
description = "Simple, performant, configurable, entirely self-contained Pastebin and URL shortener."
|
||||
readme = "README.md"
|
||||
homepage = "https://github.com/szabodanika/microbin"
|
||||
repository = "https://github.com/szabodanika/microbin"
|
||||
keywords = ["pastebin", "pastabin", "microbin", "actix", "selfhosted"]
|
||||
categories = ["pastebins"]
|
||||
|
||||
|
||||
|
||||
[dependencies]
|
||||
actix-web="4"
|
||||
actix-files="0.6.0"
|
||||
serde={ version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.80"
|
||||
bytesize = { version = "1.1", features = ["serde"] }
|
||||
askama="0.10"
|
||||
askama-filters={ version = "0.1.3", features = ["chrono"] }
|
||||
chrono="0.4.19"
|
||||
rand="0.8.5"
|
||||
linkify="0.8.1"
|
||||
clap={ version = "3.1.12", features = ["derive", "env"] }
|
||||
clap={ version = "3.1.12", features = ["derive"] }
|
||||
actix-multipart = "0.4.0"
|
||||
futures = "0.3"
|
||||
sanitize-filename = "0.3.0"
|
||||
@ -32,5 +21,4 @@ log = "0.4"
|
||||
env_logger = "0.9.0"
|
||||
actix-web-httpauth = "0.6.0"
|
||||
lazy_static = "1.4.0"
|
||||
syntect = "5.0"
|
||||
|
||||
rutie = "0.8.4"
|
10
Dockerfile
10
Dockerfile
@ -19,5 +19,13 @@ WORKDIR /usr/local/bin
|
||||
# copy built exacutable
|
||||
COPY --from=builder /usr/src/microbin/target/release/microbin /usr/local/bin/microbin
|
||||
|
||||
# copy /static folder containing the stylesheets
|
||||
COPY --from=builder /usr/src/microbin/static /usr/local/bin/static
|
||||
|
||||
# Install Ruby (no need if you're disabling all plugins)
|
||||
RUN \
|
||||
apt-get update && \
|
||||
apt-get install -y ruby
|
||||
|
||||
# run the binary
|
||||
CMD ["microbin"]
|
||||
CMD ["microbin"]
|
333
README.MD
333
README.MD
@ -1,172 +1,26 @@
|
||||
# MicroBin
|
||||
|
||||

|
||||
|
||||
# MicroBin
|
||||
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
MicroBin is a super tiny, feature rich, configurable, self-contained and self-hosted paste bin web application. It is very easy to set up and use, and will only require a few megabytes of memory and disk storage. It takes only a couple minutes to set it up, why not give it a try now?
|
||||
|
||||
[](https://render.com/deploy?repo=https://github.com/szabodanika/microbin)
|
||||
|
||||
Or install from Cargo:
|
||||
|
||||
`cargo install microbin`
|
||||
|
||||
And run with your custom configuration:
|
||||
|
||||
`microbin --port 8080 --highlightsyntax --editable`
|
||||
MicroBin is a super tiny and simple self hosted pastebin app written in Rust. The executable is around 6MB and it uses 2MB memory (plus your pastas, because they are all stored in the memory at the moment).
|
||||
|
||||
### Features
|
||||
- Is very small
|
||||
- Animal names instead of random numbers for pasta identifiers (64 animals)
|
||||
- File uploads (eg. server.com/file/pig-dog-cat)
|
||||
- Raw pasta text (eg. server.com/raw/pig-dog-cat)
|
||||
- File uploads
|
||||
- Raw pasta content (/raw/[animals])
|
||||
- URL shortening and redirection
|
||||
- Very simple database (JSON + files) for portability, easy backups and integration
|
||||
- Listing and manually removing pastas (/pastalist)
|
||||
- Private and public pastas
|
||||
- Editable and final pastas
|
||||
- Never expiring pastas
|
||||
- Automatically expiring pastas
|
||||
- Syntax highlighting
|
||||
- Entirely self-contained executable, MicroBin is a single file!
|
||||
- Automatic dark mode (follows system preferences)
|
||||
- Very simple database (json + files) for portability and easy backups
|
||||
- Animal names instead of random numbers for pasta identifiers (64 animals)
|
||||
- Automatically expiring pastas
|
||||
- Never expiring pastas
|
||||
- Listing and manually removing pastas (/pastalist)
|
||||
- Very little CSS and absolutely no JS (see [water.css](https://github.com/kognise/water.css))
|
||||
- Most of the above can be toggled on and off!
|
||||
|
||||
## 1 Usage
|
||||
|
||||
### What is a "pasta" anyway?
|
||||
### Installation
|
||||
Simply clone the repository, build it with `cargo build --release` and run the `microbin` executable in the created `target/release/` directory. It will start on port 8080. You can change the port with `-p` or `--port` CL arguments. For other arguments see [the Wiki](https://github.com/szabodanika/microbin/wiki).
|
||||
|
||||
In microbin, a pasta can be:
|
||||
- A text that you want to paste from one machine to another, eg. some code,
|
||||
- A file that you want to share, eg. a video that is too large for Discord, a zip with a code project in it or an image,
|
||||
- A URL redirect.
|
||||
|
||||
### When is MicroBin useful?
|
||||
|
||||
You can use MicroBin
|
||||
- As a URL shortener/redirect service,
|
||||
- To send long texts to other people,
|
||||
- To send large files to other people,
|
||||
- To serve content on the web, eg. configuration files for testing, images, or any other file content using the Raw functionality,
|
||||
- To move files between your desktop and a server you access from the console,
|
||||
- As a "postbox" service where people can upload their files or texts, but they cannot see or remove what others sent you - just disable the pastalist page
|
||||
- To take notes! Simply create an editable pasta.
|
||||
|
||||
...and many other things, why not get creative?
|
||||
|
||||
### Creating a Pasta
|
||||
|
||||
Navigate to the root of your server, for example https://microbin.myserver.com/. This should show you a form where you will at the very least see an expiration selector, a file attachment input, a content text field and a green save button. Depending on your configuration there miight also be a syntax highlight selector, an editable checkbox and a private ceckbox.
|
||||
|
||||
Use the expiration dropdown to choose how long you want your pasta to exist. When the selected time has expired, it will be removed from the server. The content can be any text, including plain text, code, html, even a URL. A URL is a special case, because when you open the pasta again, it will redirect you to that URL instead of showing it as a text. Entering content is optional, and so is the file attachment. If you want, you can even submit a pasta completely empty.
|
||||
|
||||
You will be redirected to the URL of the pasta, which will end with a few animal names. If you remember those animals, you can simply type them in on another machine and open your pasta elsewhere.
|
||||
|
||||
If you have editable pastas enabled and you check the editable checkbox, then later on there will be an option to change the text content of your pasta. Selecting the private checkbox will simply prevent your pasta to show up on the pasta list page, if that is enabled.
|
||||
|
||||
If you have syntax higlighting enabled, then select your language from the dropdown, or leave it as none if you just want to upload plain with no highlighting.
|
||||
|
||||
### Listing Pastas
|
||||
|
||||
If you have pasta listing enabled, then there is a pasta list option in the navigation bar, which will list all the pastas on the server in two groups: regular pastas and URL redirects (pastas containing nothing but a URL). If you have private pastas enabled, they will not show up here at all.
|
||||
|
||||
From the pasta list page, you will be able to view individual pastas by clicking on their animal identifiers on the lest, view their raw contrent by clicking on the Raw button, remove them, and if you have editable pastas enabled, then open them in edit view.
|
||||
|
||||
### Use MicroBin from the console with cURL
|
||||
|
||||
Simple text Pasta: `curl -d "expiration=10min&content=This is a test pasta" -X POST https://microbin.myserver.com/create`
|
||||
|
||||
File contents: `curl -d "expiration=10min&content=$( < mypastafile.txt )" -X POST https://microbin.myserver.com/create`
|
||||
|
||||
Available expiration options:
|
||||
`1min`, `10min`, `1hour`, `24hour`, `1week`, `never`
|
||||
|
||||
Use cURL to read the pasta: `curl https://microbin.myserver.com/rawpasta/fish-pony-crow`,
|
||||
|
||||
or to download the pasta: `curl https://microbin.myserver.com/rawpasta/fish-pony-crow > output.txt` (use /file instead of /rawpasta to download attached file).
|
||||
|
||||
## 2 Installation
|
||||
|
||||
### From Cargo
|
||||
|
||||
Install from Cargo:
|
||||
|
||||
`cargo install microbin`
|
||||
|
||||
Remember, MicroBin will create your database and file storage wherever you execute it. I recommend that you create a folder for it first and execute it there:
|
||||
|
||||
`mkdir ~/microbin/`
|
||||
|
||||
`cd ~/microbin/`
|
||||
|
||||
`microbin --highlightsyntax --editable`
|
||||
|
||||
### From AUR (for any Arch-based distro)
|
||||
Install `microbin` package from AUR and start/enable microbin systemd service. Systemd will start server on 127.0.0.1:8080 with almost all features enabled, but this can be changed in `/etc/microbin.conf`.
|
||||
|
||||
### Building MicroBin
|
||||
|
||||
Simply clone the repository, build it with `cargo build --release` and run the `microbin` executable in the created `target/release/` directory. It will start listening on 0.0.0.0:8080. You can change the port or bind address with CL arguments `-p (--port)` or `-b (--bind)` respectively . For other arguments see [the Wiki](https://github.com/szabodanika/microbin/wiki).
|
||||
|
||||
```
|
||||
git clone https://github.com/szabodanika/microbin.git
|
||||
cd microbin
|
||||
cargo build --release
|
||||
./target/release/microbin -p 80
|
||||
```
|
||||
|
||||
### Building Docker Image
|
||||
|
||||
MicroBin includes a Dockerfile. To build the image, follow these steps:
|
||||
|
||||
```
|
||||
git clone https://github.com/szabodanika/microbin.git
|
||||
cd microbin
|
||||
docker build -t microbin-docker .
|
||||
```
|
||||
|
||||
Then, for `docker compose` you can repurpose the following example in your compose file:
|
||||
|
||||
```
|
||||
services:
|
||||
paste:
|
||||
image: microbin-docker
|
||||
restart: always
|
||||
ports:
|
||||
- "80:8080"
|
||||
volumes:
|
||||
- ./microbin-data:/usr/local/bin/pasta_data
|
||||
```
|
||||
To pass command line arguments you must edit the Dockerfile and change the CMD line. In this example we add the syntax highlighting option and enable private pastas:
|
||||
|
||||
```
|
||||
CMD ["microbin", "--highlightsyntax", "--private"]
|
||||
```
|
||||
You then need to rebuild the image and recreate your container.
|
||||
|
||||
|
||||
**Note:** If you are getting the following error about domain name resolution:
|
||||
|
||||
```
|
||||
warning: spurious network error (2 tries remaining): failed to resolve address for github.com: Temporary failure in name resolution; class=Net (12)
|
||||
warning: spurious network error (1 tries remaining): failed to resolve address for github.com: Temporary failure in name resolution; class=Net (12)
|
||||
```
|
||||
|
||||
You might need to run `docker build` with the `--network` option:
|
||||
|
||||
```
|
||||
docker build --network host -t microbin-docker .
|
||||
```
|
||||
|
||||
### MicroBin as a service
|
||||
|
||||
To install it as a service on your Linux machine, create a file called `/etc/systemd/system/microbin.service`, paste this into it with the `[username]` and `[path to installation directory]` replaced with the actual values. If you installed MicroBin from cargo, your executable will be in your cargo directory, e.g. `/Users/daniel/.cargo/bin/microbin`.
|
||||
To install it as a service on your Linux machine, create a file called `/etc/systemd/system/microbin.service`, paste this into it with the `[username]` and `[path to installation directory]` replaced with the actual values.
|
||||
|
||||
```
|
||||
[Unit]
|
||||
@ -185,147 +39,28 @@ ExecStart=[path to installation directory]/target/release/microbin
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Here is my `microbin.service` for example, with some optional arguments:
|
||||
Then start the service with `systemctl start microbin` and enable it on boot with `systemctl enable microbin`.
|
||||
|
||||
### Create Pasta with cURL
|
||||
|
||||
Simple text Pasta: `curl -d "expiration=10min&content=This is a test pasta" -X POST https://microbin.myserver.com/create`
|
||||
|
||||
File contents: `curl -d "expiration=10min&content=$( < mypastafile.txt )" -X POST https://microbin.myserver.com/create`
|
||||
|
||||
Available expiration options:
|
||||
`1min`, `10min`, `1hour`, `24hour`, `1week`, `never`
|
||||
|
||||
Use cURL to read the pasta: `curl https://microbin.myserver.com/rawpasta/fish-pony-crow`,
|
||||
|
||||
or to download the pasta: `curl https://microbin.myserver.com/rawpasta/fish-pony-crow > output.txt` (use /file instead of /rawpasta to download attached file).
|
||||
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=MicroBin
|
||||
After=network.target
|
||||
### Needed improvements
|
||||
- ~~Persisting pastas on disk (currently they are lost on restart)~~ (added on 2 May 2022)
|
||||
- ~~Configuration with command line arguments (ports, enable-disable pasta list, footer, etc)~~ (added on 7 May 2022)
|
||||
- ~~File uploads~~ (added on 2 May 2022)
|
||||
- ~~URL shortening~~ (added on 23 April 2022)
|
||||
- Removing pasta after N reads
|
||||
- CLI tool (beyond wget)
|
||||
- Better instructions and documentation - on GitHub and built in
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=always
|
||||
User=ubuntu
|
||||
RootDirectory=/
|
||||
|
||||
# This is the directory where I want to run microbin. It will store all the pastas here.
|
||||
WorkingDirectory=/home/ubuntu/server/microbin
|
||||
|
||||
# This is the location of my executable - I also have 2 optional features enabled
|
||||
ExecStart=/home/ubuntu/server/microbin/target/release/microbin --editable --highlightsyntax
|
||||
|
||||
# I keep my installation in the home directory, so I need to add this
|
||||
ProtectHome=off
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Then start the service with `systemctl start microbin` and enable it on boot with `systemctl enable microbin`. To update your MicroBin, simply update or clone the repository again, build it again, and then restart the service with `systemctl restart microbin`. An update will never affect your existing pastas, unless there is a breaking change in the data model (in which case MicroBin just won't be able to import your DB), which will always be mentioned explicitly.
|
||||
|
||||
### NGINX configuration
|
||||
|
||||
```
|
||||
server {
|
||||
# I have HTTPS enabled using certbot - you can use HTTP of course if you want!
|
||||
listen 443 ssl; # managed by Certbot
|
||||
|
||||
server_name microbin.myserver.com;
|
||||
|
||||
location / {
|
||||
# Make sure to change the port if you are not running MicroBin at 8080!
|
||||
proxy_pass http://127.0.0.1:8080$request_uri;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
# Limit content size - I have 1GB because my MicroBin server is private, no one else will use it.
|
||||
client_max_body_size 1024M;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/microbin.myserver.com/fullchain.pem; # managed by Certbot
|
||||
ssl_certificate_key /etc/letsencrypt/live/microbin.myserver.com/privkey.pem; # managed by Certbot
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 3 Command Line Arguments
|
||||
|
||||
There is an ever expanding list of customisations built into MicroBin so you can use it the way you want. Instead of a configuration file, we simply use arguments that you pass to the executable, making the workflow even simpler. Read the following options and if you cannot find what you need, you can always open an issue at our GitHub repository and request a new feature!
|
||||
|
||||
### --auth-username [AUTH_USERNAME]
|
||||
|
||||
Require username for HTTP Basic Authentication when visiting the service. If `--auth-username` is set but `--auth-password ` is not, just leave the password field empty when logging in. You can also just go to https://username:password@yourserver.net or https://username@yourserver.net if password is not set instead of typing into the password
|
||||
|
||||
### --auth-password [AUTH_PASSWORD]
|
||||
|
||||
Require password for HTTP Basic Authentication when visiting the service. Will not have any affect unless `--auth-username` is also set. If `--auth-username` is set but `--auth-password ` is not, just leave the password field empty when logging in. You can also just go to https://username:password@yourserver.net or https://username@yourserver.net if password is not set instead of typing into the password prompt.
|
||||
|
||||
### --editable
|
||||
|
||||
Enables editable pastas. You will still be able to make finalised pastas but there will be an extra checkbox to make your new pasta editable from the pasta list or the pasta view page.
|
||||
|
||||
### --footer_text [TEXT]
|
||||
|
||||
Replaces the default footer text with your own. If you want to hide the footer, use --hide-footer instead.
|
||||
|
||||
### -h, --help
|
||||
|
||||
Show all commands in the terminal.
|
||||
|
||||
### --hide-footer
|
||||
|
||||
Hides the footer on every page.
|
||||
|
||||
### --hide-header
|
||||
|
||||
Hides the navigation bar on every page.
|
||||
|
||||
### --hide-logo
|
||||
|
||||
Hides the MicroBin logo from the navigation bar on every page.
|
||||
|
||||
### --no-listing
|
||||
|
||||
Disables the /pastalist endpoint, essentially making all pastas private.
|
||||
|
||||
### --highlightsyntax
|
||||
|
||||
Enables syntax highlighting support. When creating a new pasta, a new dropdown selector will be added where you can select your pasta's syntax, or just leave it empty for no highlighting.
|
||||
|
||||
### -p, --port [PORT]
|
||||
|
||||
Default value: 8080
|
||||
|
||||
Sets the port for the server will be listening on.
|
||||
|
||||
|
||||
### -b, --bind [ADDRESS]
|
||||
|
||||
Default value: 0.0.0.0
|
||||
|
||||
Sets the bind address for the server will be listening on. Both ipv4 and ipv6 are supported.
|
||||
|
||||
### --private
|
||||
|
||||
Enables private pastas. Adds a new checkbox to make your pasta private, which then won't show up on the pastalist page. With the URL to your pasta, it will still be accessible.
|
||||
|
||||
### --pure-html
|
||||
|
||||
Disables main CSS styling, just uses a few in-line stylings for the layout. With this option you will lose dark-mode support.
|
||||
|
||||
### --readonly
|
||||
|
||||
Disables adding/editing/removing pastas entirely.
|
||||
|
||||
### --title [TITLE]
|
||||
|
||||
Replaces "MicroBin" with your title of choice in the navigation bar.
|
||||
|
||||
### -t, --threads [THREADS]
|
||||
|
||||
Default value: 1
|
||||
|
||||
Number of workers MicroBin is allowed to have. Increase this to the number of CPU cores you have if you want to go beast mode, but for personal use one worker is enough.
|
||||
|
||||
### -V, --version
|
||||
|
||||
Displays your MicroBin's version information.
|
||||
|
||||
### --wide
|
||||
|
||||
Changes the maximum width of the UI from 720 pixels to 1080 pixels.
|
||||
|
19
SECURITY.md
19
SECURITY.md
@ -1,19 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 1.1.* | :white_check_mark: |
|
||||
| < 1.1.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Security vulnerabilities can be reported directly to
|
||||
the developer/maintainer at d@szab.eu.
|
||||
|
||||
Sensitive information may be GPG encrypted with my public key available at
|
||||
https://szab.eu/assets/files/daniel-szabo-pub.asc.
|
BIN
git/index.png
BIN
git/index.png
Binary file not shown.
Before Width: | Height: | Size: 791 KiB After Width: | Height: | Size: 480 KiB |
78
plugins/BasicSyntaxHighlighter.rb
Normal file
78
plugins/BasicSyntaxHighlighter.rb
Normal file
@ -0,0 +1,78 @@
|
||||
module MBP
|
||||
module BasicSyntaxHighlighter
|
||||
|
||||
# Plugin Properties
|
||||
|
||||
def self.get_id()
|
||||
"BasicSyntaxHighlighter"
|
||||
end
|
||||
|
||||
def self.get_name()
|
||||
"Basic Syntax Highlighter Plugin"
|
||||
end
|
||||
|
||||
def self.get_version()
|
||||
"1.0.0"
|
||||
end
|
||||
|
||||
def self.get_author()
|
||||
"Daniel Szabo"
|
||||
end
|
||||
|
||||
def self.get_webpage()
|
||||
"https://github.com/szabodanika/microbin"
|
||||
end
|
||||
|
||||
def self.get_description()
|
||||
"This plugin will simply color keywords and special characters in four different colors based on some very basic RegEx - it is meant to univesally make code pastas more readable but is not a robust syntax highlighter solution."
|
||||
end
|
||||
|
||||
# Plugin Event Hooks
|
||||
|
||||
def self.init()
|
||||
# Ignore event
|
||||
"OK"
|
||||
end
|
||||
|
||||
def self.on_pasta_created(content)
|
||||
# We do not modify stored content
|
||||
return content
|
||||
end
|
||||
|
||||
def self.on_pasta_read(content)
|
||||
|
||||
tokens = {
|
||||
|
||||
"orchid" => [/([0-9])/, /([t|T][r|R][u|U][e|E]|[f|F][a|A][l|L][s|S][e|E])/],
|
||||
|
||||
"palevioletred" => ['(', ')', '{', '}', '[', ']'],
|
||||
|
||||
"royalblue" => [/(\s(for|while|do|select|async|await|mut|break|continue|in|as|switch|let|fn|async|if|else|elseif|new|switch|match|case|default|public|protected|private|return|class|interface|static|final|const|var|int|integer|boolean|float|double|module|def|end|void))(?![a-z])/],
|
||||
|
||||
"mediumorchid" => [/(:|\.|;|=|>|<|\?|!|#|%|@|\^|&|\*|\|)/],
|
||||
|
||||
"mediumseagreen" => [/(\".*\")/, /(\'.*\')/]
|
||||
|
||||
};
|
||||
|
||||
tokens.each { | color, tokens |
|
||||
for token in tokens do
|
||||
if(token.class == String)
|
||||
content.gsub!(token, "$$#{color}$$" + token + "$$/#{color}$$")
|
||||
elsif
|
||||
content.gsub!(token, "$$#{color}$$" + '\1' + "$$/#{color}$$")
|
||||
end
|
||||
end
|
||||
};
|
||||
|
||||
tokens.each { | color, tokens |
|
||||
content.gsub!("$$#{color}$$", "<span style='color:#{color}'>");
|
||||
content.gsub!("$$/#{color}$$", "</span>");
|
||||
};
|
||||
|
||||
return content
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
78
plugins/MBPlugin.rb
Normal file
78
plugins/MBPlugin.rb
Normal file
@ -0,0 +1,78 @@
|
||||
require 'rutie'
|
||||
|
||||
module MB
|
||||
|
||||
class MBPlugin
|
||||
# Plugin Properties
|
||||
|
||||
def self.get_id()
|
||||
"[Enter Plugin ID]"
|
||||
end
|
||||
|
||||
def self.get_name()
|
||||
"[Eenter Plugin Name]"
|
||||
end
|
||||
|
||||
def self.get_version()
|
||||
"1.0.0"
|
||||
end
|
||||
|
||||
def self.get_author()
|
||||
"[Enter Author name]"
|
||||
end
|
||||
|
||||
def self.get_webpage()
|
||||
"[Enter Web URL]"
|
||||
end
|
||||
|
||||
def self.get_description()
|
||||
"[Enter Description]"
|
||||
end
|
||||
|
||||
# Plugin Event Hooks
|
||||
|
||||
def self.init()
|
||||
raise "Operation not supported";
|
||||
end
|
||||
|
||||
def self.on_pasta_deleted(id, content, created, expiration, file)
|
||||
raise "Operation not supported";
|
||||
end
|
||||
|
||||
def self.on_pasta_expired(id, content, created, expiration, file)
|
||||
raise "Operation not supported";
|
||||
end
|
||||
|
||||
def self.on_pasta_created(id, content, created, expiration, file)
|
||||
raise "Operation not supported";
|
||||
end
|
||||
|
||||
def self.on_pasta_read(id, content, created, expiration, file)
|
||||
raise "Operation not supported";
|
||||
end
|
||||
|
||||
# Rust Function Calls
|
||||
|
||||
def self.init()
|
||||
raise "Operation not supported";
|
||||
end
|
||||
|
||||
def self.P=on_pasta_deleted(id, content, created, expiration, file)
|
||||
raise "Operation not supported";
|
||||
end
|
||||
|
||||
def self.on_pasta_expired(id, content, created, expiration, file)
|
||||
raise "Operation not supported";
|
||||
end
|
||||
|
||||
def self.on_pasta_created(id, content, created, expiration, file)
|
||||
raise "Operation not supported";
|
||||
end
|
||||
|
||||
def self.on_pasta_read(id, content, created, expiration, file)
|
||||
raise "Operation not supported";
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
42
plugins/helloWorld.rb
Normal file
42
plugins/helloWorld.rb
Normal file
@ -0,0 +1,42 @@
|
||||
module MBP
|
||||
class HelloWorld < MBPlugin
|
||||
|
||||
def self.get_id()
|
||||
"HelloWorld"
|
||||
end
|
||||
|
||||
def self.get_name()
|
||||
"Hello World Plugin"
|
||||
end
|
||||
|
||||
def self.get_version()
|
||||
"1.0.0"
|
||||
end
|
||||
|
||||
def self.get_description()
|
||||
"This is just a demo plugin. It does not do anything."
|
||||
end
|
||||
|
||||
def self.get_author()
|
||||
"Daniel Szabo"
|
||||
end
|
||||
|
||||
def self.get_webpage()
|
||||
"https://github.com/szabodanika/microbin"
|
||||
end
|
||||
|
||||
def self.init()
|
||||
# Ignore event
|
||||
"OK"
|
||||
end
|
||||
|
||||
def self.on_pasta_created(content)
|
||||
return content
|
||||
end
|
||||
|
||||
def self.on_pasta_read(content)
|
||||
return content
|
||||
end
|
||||
|
||||
end
|
||||
end
|
@ -1,9 +0,0 @@
|
||||
services:
|
||||
- type: web
|
||||
name: microbin
|
||||
plan: free
|
||||
numInstances: 1
|
||||
env: rust
|
||||
repo: https://github.com/szabodanika/microbin.git
|
||||
buildCommand: cargo build --release
|
||||
startCommand: ./target/release/microbin --editable --highlightsyntax
|
@ -14,8 +14,8 @@ pub fn to_animal_names(mut number: u64) -> String {
|
||||
return ANIMAL_NAMES[0].parse().unwrap();
|
||||
}
|
||||
|
||||
// max 4 animals so 6 * 6 = 64 bits
|
||||
let mut power = 6;
|
||||
|
||||
loop {
|
||||
let digit = number / ANIMAL_NAMES.len().pow(power) as u64;
|
||||
if !(result.is_empty() && digit == 0) {
|
||||
@ -24,7 +24,7 @@ pub fn to_animal_names(mut number: u64) -> String {
|
||||
number -= digit * ANIMAL_NAMES.len().pow(power) as u64;
|
||||
if power > 0 {
|
||||
power -= 1;
|
||||
} else if power <= 0 || number == 0 {
|
||||
} else if power == 0 || number == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -32,7 +32,7 @@ pub fn to_animal_names(mut number: u64) -> String {
|
||||
result.join("-")
|
||||
}
|
||||
|
||||
pub fn to_u64(animal_names: &str) -> Result<u64, &str> {
|
||||
pub fn to_u64(animal_names: &str) -> u64 {
|
||||
let mut result: u64 = 0;
|
||||
|
||||
let animals: Vec<&str> = animal_names.split("-").collect();
|
||||
@ -40,14 +40,9 @@ pub fn to_u64(animal_names: &str) -> Result<u64, &str> {
|
||||
let mut pow = animals.len();
|
||||
for i in 0..animals.len() {
|
||||
pow -= 1;
|
||||
let animal_index = ANIMAL_NAMES.iter().position(|&r| r == animals[i]);
|
||||
match animal_index {
|
||||
None => return Err("Failed to convert animal name to u64!"),
|
||||
Some(_) => {
|
||||
result += (animal_index.unwrap() * ANIMAL_NAMES.len().pow(pow as u32)) as u64
|
||||
}
|
||||
}
|
||||
result += (ANIMAL_NAMES.iter().position(|&r| r == animals[i]).unwrap()
|
||||
* ANIMAL_NAMES.len().pow(pow as u32)) as u64;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
result
|
||||
}
|
62
src/args.rs
62
src/args.rs
@ -1,62 +0,0 @@
|
||||
use std::net::IpAddr;
|
||||
use clap::Parser;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ARGS: Args = Args::parse();
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
pub struct Args {
|
||||
#[clap(long, env="MICROBIN_AUTH_USERNAME")]
|
||||
pub auth_username: Option<String>,
|
||||
|
||||
#[clap(long, env="MICROBIN_AUTH_PASSWORD")]
|
||||
pub auth_password: Option<String>,
|
||||
|
||||
#[clap(long, env="MICROBIN_EDITABLE")]
|
||||
pub editable: bool,
|
||||
|
||||
#[clap(long, env="MICROBIN_FOOTER_TEXT")]
|
||||
pub footer_text: Option<String>,
|
||||
|
||||
#[clap(long, env="MICROBIN_HIDE_FOOTER")]
|
||||
pub hide_footer: bool,
|
||||
|
||||
#[clap(long, env="MICROBIN_HIDE_HEADER")]
|
||||
pub hide_header: bool,
|
||||
|
||||
#[clap(long, env="MICROBIN_HIDE_LOGO")]
|
||||
pub hide_logo: bool,
|
||||
|
||||
#[clap(long, env="MICROBIN_NO_LISTING")]
|
||||
pub no_listing: bool,
|
||||
|
||||
#[clap(long, env="MICROBIN_HIGHLIGHTSYNTAX")]
|
||||
pub highlightsyntax: bool,
|
||||
|
||||
#[clap(short, long, env="MICROBIN_PORT", default_value_t = 8080)]
|
||||
pub port: u16,
|
||||
|
||||
#[clap(short, long, env="MICROBIN_BIND", default_value_t = IpAddr::from([0, 0, 0, 0]))]
|
||||
pub bind: IpAddr,
|
||||
|
||||
#[clap(long, env="MICROBIN_PRIVATE")]
|
||||
pub private: bool,
|
||||
|
||||
#[clap(long, env="MICROBIN_PURE_HTML")]
|
||||
pub pure_html: bool,
|
||||
|
||||
#[clap(long, env="MICROBIN_READONLY")]
|
||||
pub readonly: bool,
|
||||
|
||||
#[clap(long, env="MICROBIN_TITLE")]
|
||||
pub title: Option<String>,
|
||||
|
||||
#[clap(short, long, env="MICROBIN_THREADS", default_value_t = 1)]
|
||||
pub threads: u8,
|
||||
|
||||
#[clap(long, env="MICROBIN_WIDE")]
|
||||
pub wide: bool,
|
||||
}
|
@ -39,10 +39,7 @@ pub fn load_from_file() -> io::Result<Vec<Pasta>> {
|
||||
match file {
|
||||
Ok(_) => {
|
||||
let reader = BufReader::new(file.unwrap());
|
||||
let data: Vec<Pasta> = match serde_json::from_reader(reader) {
|
||||
Ok(t) => t,
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let data: Vec<Pasta> = serde_json::from_reader(reader).unwrap();
|
||||
Ok(data)
|
||||
}
|
||||
Err(_) => {
|
@ -1,159 +0,0 @@
|
||||
use crate::dbio::save_to_file;
|
||||
use crate::pasta::PastaFile;
|
||||
use crate::util::animalnumbers::to_animal_names;
|
||||
use crate::util::misc::is_valid_url;
|
||||
use crate::{AppState, Pasta, ARGS};
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::{get, web, Error, HttpResponse, Responder};
|
||||
use askama::Template;
|
||||
use bytesize::ByteSize;
|
||||
use futures::TryStreamExt;
|
||||
use log::warn;
|
||||
use rand::Rng;
|
||||
use std::io::Write;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.html")]
|
||||
struct IndexTemplate<'a> {
|
||||
args: &'a ARGS,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
pub async fn index() -> impl Responder {
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(IndexTemplate { args: &ARGS }.render().unwrap())
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
data: web::Data<AppState>,
|
||||
mut payload: Multipart,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
if ARGS.readonly {
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header(("Location", "/"))
|
||||
.finish());
|
||||
}
|
||||
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||
Ok(n) => n.as_secs(),
|
||||
Err(_) => {
|
||||
log::error!("SystemTime before UNIX EPOCH!");
|
||||
0
|
||||
}
|
||||
} as i64;
|
||||
|
||||
let mut new_pasta = Pasta {
|
||||
id: rand::thread_rng().gen::<u16>() as u64,
|
||||
content: String::from("No Text Content"),
|
||||
file: None,
|
||||
extension: String::from(""),
|
||||
private: false,
|
||||
editable: false,
|
||||
created: timenow,
|
||||
pasta_type: String::from(""),
|
||||
expiration: 0,
|
||||
};
|
||||
|
||||
while let Some(mut field) = payload.try_next().await? {
|
||||
match field.name() {
|
||||
"editable" => {
|
||||
// while let Some(_chunk) = field.try_next().await? {}
|
||||
new_pasta.editable = true;
|
||||
continue;
|
||||
}
|
||||
"private" => {
|
||||
// while let Some(_chunk) = field.try_next().await? {}
|
||||
new_pasta.private = true;
|
||||
continue;
|
||||
}
|
||||
"expiration" => {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
new_pasta.expiration = match std::str::from_utf8(&chunk).unwrap() {
|
||||
"1min" => timenow + 60,
|
||||
"10min" => timenow + 60 * 10,
|
||||
"1hour" => timenow + 60 * 60,
|
||||
"24hour" => timenow + 60 * 60 * 24,
|
||||
"1week" => timenow + 60 * 60 * 24 * 7,
|
||||
"never" => 0,
|
||||
_ => {
|
||||
log::error!("{}", "Unexpected expiration time!");
|
||||
0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
"content" => {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
new_pasta.content = std::str::from_utf8(&chunk).unwrap().to_string();
|
||||
new_pasta.pasta_type = if is_valid_url(new_pasta.content.as_str()) {
|
||||
String::from("url")
|
||||
} else {
|
||||
String::from("text")
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
"syntax-highlight" => {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
new_pasta.extension = std::str::from_utf8(&chunk).unwrap().to_string();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
"file" => {
|
||||
let path = field.content_disposition().get_filename();
|
||||
|
||||
let path = match path {
|
||||
Some("") => continue,
|
||||
Some(p) => p,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let mut file = match PastaFile::from_unsanitized(&path) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
warn!("Unsafe file name: {e:?}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
std::fs::create_dir_all(format!("./pasta_data/public/{}", &new_pasta.id_as_animals()))
|
||||
.unwrap();
|
||||
|
||||
let filepath = format!(
|
||||
"./pasta_data/public/{}/{}",
|
||||
&new_pasta.id_as_animals(),
|
||||
&file.name()
|
||||
);
|
||||
|
||||
let mut f = web::block(|| std::fs::File::create(filepath)).await??;
|
||||
let mut size = 0;
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
size += chunk.len();
|
||||
f = web::block(move || f.write_all(&chunk).map(|_| f)).await??;
|
||||
}
|
||||
|
||||
file.size = ByteSize::b(size as u64);
|
||||
|
||||
new_pasta.file = Some(file);
|
||||
new_pasta.pasta_type = String::from("text");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let id = new_pasta.id;
|
||||
|
||||
pastas.push(new_pasta);
|
||||
|
||||
save_to_file(&pastas);
|
||||
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("Location", format!("/pasta/{}", to_animal_names(id))))
|
||||
.finish())
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
use crate::args::Args;
|
||||
use crate::dbio::save_to_file;
|
||||
use crate::endpoints::errors::ErrorTemplate;
|
||||
use crate::util::animalnumbers::to_u64;
|
||||
use crate::util::misc::remove_expired;
|
||||
use crate::{AppState, Pasta, ARGS};
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::{get, post, web, Error, HttpResponse};
|
||||
use askama::Template;
|
||||
use futures::TryStreamExt;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "edit.html", escape = "none")]
|
||||
struct EditTemplate<'a> {
|
||||
pasta: &'a Pasta,
|
||||
args: &'a Args,
|
||||
}
|
||||
|
||||
#[get("/edit/{id}")]
|
||||
pub async fn get_edit(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
let id = to_u64(&*id.into_inner()).unwrap_or(0);
|
||||
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
for pasta in pastas.iter() {
|
||||
if pasta.id == id {
|
||||
if !pasta.editable {
|
||||
return HttpResponse::Found()
|
||||
.append_header(("Location", "/"))
|
||||
.finish();
|
||||
}
|
||||
return HttpResponse::Ok().content_type("text/html").body(
|
||||
EditTemplate {
|
||||
pasta: &pasta,
|
||||
args: &ARGS,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
|
||||
}
|
||||
|
||||
#[post("/edit/{id}")]
|
||||
pub async fn post_edit(
|
||||
data: web::Data<AppState>,
|
||||
id: web::Path<String>,
|
||||
mut payload: Multipart,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
if ARGS.readonly {
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header(("Location", "/"))
|
||||
.finish());
|
||||
}
|
||||
|
||||
let id = to_u64(&*id.into_inner()).unwrap_or(0);
|
||||
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
let mut new_content = String::from("");
|
||||
|
||||
while let Some(mut field) = payload.try_next().await? {
|
||||
match field.name() {
|
||||
"content" => {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
new_content = std::str::from_utf8(&chunk).unwrap().to_string();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
for (i, pasta) in pastas.iter().enumerate() {
|
||||
if pasta.id == id {
|
||||
if pasta.editable {
|
||||
pastas[i].content.replace_range(.., &*new_content);
|
||||
save_to_file(&pastas);
|
||||
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header(("Location", format!("/pasta/{}", pastas[i].id_as_animals())))
|
||||
.finish());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap()))
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
use actix_web::{Error, HttpResponse};
|
||||
use askama::Template;
|
||||
|
||||
use crate::args::{Args, ARGS};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "error.html")]
|
||||
pub struct ErrorTemplate<'a> {
|
||||
pub args: &'a Args,
|
||||
}
|
||||
|
||||
pub async fn not_found() -> Result<HttpResponse, Error> {
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap()))
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
use crate::args::{Args, ARGS};
|
||||
use actix_web::{get, HttpResponse};
|
||||
use askama::Template;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "help.html")]
|
||||
struct Help<'a> {
|
||||
args: &'a Args,
|
||||
_marker: PhantomData<&'a ()>,
|
||||
}
|
||||
|
||||
#[get("/help")]
|
||||
pub async fn help() -> HttpResponse {
|
||||
HttpResponse::Ok().content_type("text/html").body(
|
||||
Help {
|
||||
args: &ARGS,
|
||||
_marker: Default::default(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
use actix_web::{get, web, HttpResponse};
|
||||
use askama::Template;
|
||||
|
||||
use crate::args::{Args, ARGS};
|
||||
use crate::endpoints::errors::ErrorTemplate;
|
||||
use crate::pasta::Pasta;
|
||||
use crate::util::animalnumbers::to_u64;
|
||||
use crate::util::misc::remove_expired;
|
||||
use crate::AppState;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "pasta.html", escape = "none")]
|
||||
struct PastaTemplate<'a> {
|
||||
pasta: &'a Pasta,
|
||||
args: &'a Args,
|
||||
}
|
||||
|
||||
#[get("/pasta/{id}")]
|
||||
pub async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
let id = to_u64(&*id.into_inner()).unwrap_or(0);
|
||||
|
||||
println!("{}", id);
|
||||
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
for pasta in pastas.iter() {
|
||||
if pasta.id == id {
|
||||
return HttpResponse::Ok().content_type("text/html").body(
|
||||
PastaTemplate {
|
||||
pasta: &pasta,
|
||||
args: &ARGS,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
|
||||
}
|
||||
|
||||
#[get("/url/{id}")]
|
||||
pub async fn redirecturl(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
let id = to_u64(&*id.into_inner()).unwrap_or(0);
|
||||
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
for pasta in pastas.iter() {
|
||||
if pasta.id == id {
|
||||
if pasta.pasta_type == "url" {
|
||||
return HttpResponse::Found()
|
||||
.append_header(("Location", String::from(&pasta.content)))
|
||||
.finish();
|
||||
} else {
|
||||
return HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
|
||||
}
|
||||
|
||||
#[get("/raw/{id}")]
|
||||
pub async fn getrawpasta(data: web::Data<AppState>, id: web::Path<String>) -> String {
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
let id = to_u64(&*id.into_inner()).unwrap_or(0);
|
||||
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
for pasta in pastas.iter() {
|
||||
if pasta.id == id {
|
||||
return pasta.content.to_owned();
|
||||
}
|
||||
}
|
||||
|
||||
String::from("Pasta not found! :-(")
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
use actix_web::{get, web, HttpResponse};
|
||||
use askama::Template;
|
||||
|
||||
use crate::args::{Args, ARGS};
|
||||
use crate::pasta::Pasta;
|
||||
use crate::util::misc::remove_expired;
|
||||
use crate::AppState;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "pastalist.html")]
|
||||
struct PastaListTemplate<'a> {
|
||||
pastas: &'a Vec<Pasta>,
|
||||
args: &'a Args,
|
||||
}
|
||||
|
||||
#[get("/pastalist")]
|
||||
pub async fn list(data: web::Data<AppState>) -> HttpResponse {
|
||||
if ARGS.no_listing {
|
||||
return HttpResponse::Found()
|
||||
.append_header(("Location", "/"))
|
||||
.finish();
|
||||
}
|
||||
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
HttpResponse::Ok().content_type("text/html").body(
|
||||
PastaListTemplate {
|
||||
pastas: &pastas,
|
||||
args: &ARGS,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
use actix_web::{get, web, HttpResponse};
|
||||
|
||||
use crate::args::ARGS;
|
||||
use crate::endpoints::errors::ErrorTemplate;
|
||||
use crate::pasta::PastaFile;
|
||||
use crate::util::animalnumbers::to_u64;
|
||||
use crate::util::misc::remove_expired;
|
||||
use crate::AppState;
|
||||
use askama::Template;
|
||||
use std::fs;
|
||||
|
||||
#[get("/remove/{id}")]
|
||||
pub async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
|
||||
if ARGS.readonly {
|
||||
return HttpResponse::Found()
|
||||
.append_header(("Location", "/"))
|
||||
.finish();
|
||||
}
|
||||
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
let id = to_u64(&*id.into_inner()).unwrap_or(0);
|
||||
|
||||
for (i, pasta) in pastas.iter().enumerate() {
|
||||
if pasta.id == id {
|
||||
// remove the file itself
|
||||
if let Some(PastaFile { name, .. }) = &pasta.file {
|
||||
if fs::remove_file(format!("./pasta_data/public/{}/{}", pasta.id_as_animals(), name))
|
||||
.is_err()
|
||||
{
|
||||
log::error!("Failed to delete file {}!", name)
|
||||
}
|
||||
|
||||
// and remove the containing directory
|
||||
if fs::remove_dir(format!("./pasta_data/public/{}/", pasta.id_as_animals())).is_err() {
|
||||
log::error!("Failed to delete directory {}!", name)
|
||||
}
|
||||
}
|
||||
// remove it from in-memory pasta list
|
||||
pastas.remove(i);
|
||||
return HttpResponse::Found()
|
||||
.append_header(("Location", "/pastalist"))
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
use actix_web::{get, web, HttpResponse};
|
||||
use askama::Template;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "water.css", escape = "none")]
|
||||
struct WaterCSS<'a> {
|
||||
_marker: PhantomData<&'a ()>,
|
||||
}
|
||||
|
||||
#[get("/static/{resource}")]
|
||||
pub async fn static_resources(resource_id: web::Path<String>) -> HttpResponse {
|
||||
match resource_id.into_inner().as_str() {
|
||||
"water.css" => HttpResponse::Ok().content_type("text/css").body(
|
||||
WaterCSS {
|
||||
_marker: Default::default(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
),
|
||||
_ => HttpResponse::NotFound().content_type("text/html").finish(),
|
||||
}
|
||||
}
|
437
src/main.rs
437
src/main.rs
@ -1,49 +1,353 @@
|
||||
extern crate core;
|
||||
|
||||
use crate::args::ARGS;
|
||||
use crate::endpoints::{
|
||||
create, edit, errors, help, pasta as pasta_endpoint, pastalist, remove, static_resources,
|
||||
};
|
||||
use crate::pasta::Pasta;
|
||||
use crate::util::dbio;
|
||||
use actix_web::middleware::Condition;
|
||||
use actix_web::{middleware, web, App, HttpServer};
|
||||
use actix_web_httpauth::middleware::HttpAuthentication;
|
||||
use chrono::Local;
|
||||
use env_logger::Builder;
|
||||
use log::LevelFilter;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub mod args;
|
||||
pub mod pasta;
|
||||
use actix_files;
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::dev::ServiceRequest;
|
||||
use actix_web::middleware::Condition;
|
||||
use actix_web::{error, get, middleware, web, App, Error, HttpResponse, HttpServer, Responder};
|
||||
use actix_web_httpauth::extractors::basic::BasicAuth;
|
||||
use actix_web_httpauth::middleware::HttpAuthentication;
|
||||
use askama::Template;
|
||||
use chrono::Local;
|
||||
use clap::Parser;
|
||||
use futures::TryStreamExt as _;
|
||||
use lazy_static::lazy_static;
|
||||
use linkify::{LinkFinder, LinkKind};
|
||||
use log::LevelFilter;
|
||||
use rand::Rng;
|
||||
use std::fs;
|
||||
|
||||
pub mod util {
|
||||
pub mod animalnumbers;
|
||||
pub mod auth;
|
||||
pub mod dbio;
|
||||
pub mod misc;
|
||||
pub mod syntaxhighlighter;
|
||||
use crate::animalnumbers::{to_animal_names, to_u64};
|
||||
use crate::dbio::save_to_file;
|
||||
use crate::pasta::Pasta;
|
||||
|
||||
mod animalnumbers;
|
||||
mod dbio;
|
||||
mod pasta;
|
||||
mod plugins;
|
||||
|
||||
lazy_static! {
|
||||
static ref ARGS: Args = Args::parse();
|
||||
}
|
||||
|
||||
pub mod endpoints {
|
||||
pub mod create;
|
||||
pub mod edit;
|
||||
pub mod errors;
|
||||
pub mod help;
|
||||
pub mod pasta;
|
||||
pub mod pastalist;
|
||||
pub mod remove;
|
||||
pub mod static_resources;
|
||||
struct AppState {
|
||||
pastas: Mutex<Vec<Pasta>>,
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
pub pastas: Mutex<Vec<Pasta>>,
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
#[clap(short, long, default_value_t = 8080)]
|
||||
port: u32,
|
||||
|
||||
#[clap(short, long, default_value_t = 1)]
|
||||
threads: u8,
|
||||
|
||||
#[clap(short, long)]
|
||||
wide: bool,
|
||||
|
||||
#[clap(short, long, default_value_t = 3)]
|
||||
animals: u8,
|
||||
|
||||
#[clap(long)]
|
||||
hide_header: bool,
|
||||
|
||||
#[clap(long)]
|
||||
hide_footer: bool,
|
||||
|
||||
#[clap(long)]
|
||||
pure_html: bool,
|
||||
|
||||
#[clap(long)]
|
||||
no_listing: bool,
|
||||
|
||||
#[clap(long)]
|
||||
auth_username: Option<String>,
|
||||
|
||||
#[clap(long)]
|
||||
auth_password: Option<String>,
|
||||
}
|
||||
|
||||
async fn auth_validator(
|
||||
req: ServiceRequest,
|
||||
credentials: BasicAuth,
|
||||
) -> Result<ServiceRequest, Error> {
|
||||
// check if username matches
|
||||
if credentials.user_id().as_ref() == ARGS.auth_username.as_ref().unwrap() {
|
||||
return match ARGS.auth_password.as_ref() {
|
||||
Some(cred_pass) => match credentials.password() {
|
||||
None => Err(error::ErrorBadRequest("Invalid login details.")),
|
||||
Some(arg_pass) => {
|
||||
if arg_pass == cred_pass {
|
||||
Ok(req)
|
||||
} else {
|
||||
Err(error::ErrorBadRequest("Invalid login details."))
|
||||
}
|
||||
}
|
||||
},
|
||||
None => Ok(req),
|
||||
};
|
||||
} else {
|
||||
Err(error::ErrorBadRequest("Invalid login details."))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.html")]
|
||||
struct IndexTemplate<'a> {
|
||||
args: &'a Args,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "error.html")]
|
||||
struct ErrorTemplate<'a> {
|
||||
args: &'a Args,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "pasta.html", escape = "none")]
|
||||
struct PastaTemplate<'a> {
|
||||
pasta: &'a Pasta,
|
||||
args: &'a Args,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "pastalist.html")]
|
||||
struct PastaListTemplate<'a> {
|
||||
pastas: &'a Vec<Pasta>,
|
||||
args: &'a Args,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn index() -> impl Responder {
|
||||
HttpResponse::Found()
|
||||
.content_type("text/html")
|
||||
.body(IndexTemplate { args: &ARGS }.render().unwrap())
|
||||
}
|
||||
|
||||
async fn not_found() -> Result<HttpResponse, Error> {
|
||||
Ok(HttpResponse::Found()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap()))
|
||||
}
|
||||
|
||||
async fn create(data: web::Data<AppState>, mut payload: Multipart) -> Result<HttpResponse, Error> {
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||
Ok(n) => n.as_secs(),
|
||||
Err(_) => panic!("SystemTime before UNIX EPOCH!"),
|
||||
} as i64;
|
||||
|
||||
let mut new_pasta = Pasta {
|
||||
id: rand::thread_rng().gen::<u16>() as u64,
|
||||
content: String::from("No Text Content"),
|
||||
file: String::from("no-file"),
|
||||
created: timenow,
|
||||
pasta_type: String::from(""),
|
||||
expiration: 0,
|
||||
};
|
||||
|
||||
while let Some(mut field) = payload.try_next().await? {
|
||||
match field.name() {
|
||||
"expiration" => {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
new_pasta.expiration = match std::str::from_utf8(&chunk).unwrap() {
|
||||
"1min" => timenow + 60,
|
||||
"10min" => timenow + 60 * 10,
|
||||
"1hour" => timenow + 60 * 60,
|
||||
"24hour" => timenow + 60 * 60 * 24,
|
||||
"1week" => timenow + 60 * 60 * 24 * 7,
|
||||
"never" => 0,
|
||||
_ => panic!("Unexpected expiration time!"),
|
||||
};
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
"content" => {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
new_pasta.content =
|
||||
plugins::on_pasta_created(std::str::from_utf8(&chunk).unwrap());
|
||||
new_pasta.pasta_type = if is_valid_url(new_pasta.content.as_str()) {
|
||||
String::from("url")
|
||||
} else {
|
||||
String::from("text")
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
"file" => {
|
||||
let content_disposition = field.content_disposition();
|
||||
|
||||
let filename = match content_disposition.get_filename() {
|
||||
Some("") => continue,
|
||||
Some(filename) => filename.replace(' ', "_").to_string(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
std::fs::create_dir_all(format!("./pasta_data/{}", &new_pasta.id_as_animals()))
|
||||
.unwrap();
|
||||
|
||||
let filepath = format!("./pasta_data/{}/{}", &new_pasta.id_as_animals(), &filename);
|
||||
|
||||
new_pasta.file = filename;
|
||||
|
||||
let mut f = web::block(|| std::fs::File::create(filepath)).await??;
|
||||
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
f = web::block(move || f.write_all(&chunk).map(|_| f)).await??;
|
||||
}
|
||||
|
||||
new_pasta.pasta_type = String::from("text");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let id = new_pasta.id;
|
||||
|
||||
pastas.push(new_pasta);
|
||||
|
||||
save_to_file(&pastas);
|
||||
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("Location", format!("/pasta/{}", to_animal_names(id))))
|
||||
.finish())
|
||||
}
|
||||
|
||||
#[get("/pasta/{id}")]
|
||||
async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
let id = to_u64(&*id.into_inner());
|
||||
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
for pasta in pastas.iter() {
|
||||
if pasta.id == id {
|
||||
let pasta_copy = Pasta {
|
||||
id: pasta.id,
|
||||
content: plugins::on_pasta_read(&pasta.content),
|
||||
file: pasta.file.to_string(),
|
||||
created: pasta.created,
|
||||
pasta_type: pasta.pasta_type.to_string(),
|
||||
expiration: pasta.expiration,
|
||||
};
|
||||
|
||||
return HttpResponse::Found().content_type("text/html").body(
|
||||
PastaTemplate {
|
||||
pasta: &pasta_copy,
|
||||
args: &ARGS,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse::Found()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
|
||||
}
|
||||
|
||||
#[get("/url/{id}")]
|
||||
async fn redirecturl(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
let id = to_u64(&*id.into_inner());
|
||||
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
for pasta in pastas.iter() {
|
||||
if pasta.id == id {
|
||||
if pasta.pasta_type == "url" {
|
||||
return HttpResponse::Found()
|
||||
.append_header(("Location", String::from(&pasta.content)))
|
||||
.finish();
|
||||
} else {
|
||||
return HttpResponse::Found()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse::Found()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
|
||||
}
|
||||
|
||||
#[get("/raw/{id}")]
|
||||
async fn getrawpasta(data: web::Data<AppState>, id: web::Path<String>) -> String {
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
let id = to_u64(&*id.into_inner());
|
||||
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
for pasta in pastas.iter() {
|
||||
if pasta.id == id {
|
||||
return pasta.content.to_owned();
|
||||
}
|
||||
}
|
||||
|
||||
String::from("Pasta not found! :-(")
|
||||
}
|
||||
|
||||
#[get("/remove/{id}")]
|
||||
async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
let id = to_u64(&*id.into_inner());
|
||||
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
for (i, pasta) in pastas.iter().enumerate() {
|
||||
if pasta.id == id {
|
||||
pastas.remove(i);
|
||||
return HttpResponse::Found()
|
||||
.append_header(("Location", "/pastalist"))
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse::Found()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
|
||||
}
|
||||
|
||||
#[get("/pastalist")]
|
||||
async fn list(data: web::Data<AppState>) -> HttpResponse {
|
||||
if ARGS.no_listing {
|
||||
return HttpResponse::Found()
|
||||
.append_header(("Location", "/"))
|
||||
.finish();
|
||||
}
|
||||
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
HttpResponse::Found().content_type("text/html").body(
|
||||
PastaListTemplate {
|
||||
pastas: &pastas,
|
||||
args: &ARGS,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let args: Args = Args::parse();
|
||||
|
||||
Builder::new()
|
||||
.format(|buf, record| {
|
||||
writeln!(
|
||||
@ -58,16 +362,15 @@ async fn main() -> std::io::Result<()> {
|
||||
.init();
|
||||
|
||||
log::info!(
|
||||
"MicroBin starting on http://{}:{}",
|
||||
ARGS.bind.to_string(),
|
||||
ARGS.port.to_string()
|
||||
"MicroBin starting on http://127.0.0.1:{}",
|
||||
args.port.to_string()
|
||||
);
|
||||
|
||||
match fs::create_dir_all("./pasta_data/public") {
|
||||
match std::fs::create_dir_all("./pasta_data") {
|
||||
Ok(dir) => dir,
|
||||
Err(error) => {
|
||||
log::error!("Couldn't create data directory ./pasta_data/public/: {:?}", error);
|
||||
panic!("Couldn't create data directory ./pasta_data/public/: {:?}", error);
|
||||
log::error!("Couldn't create data directory ./pasta_data: {:?}", error);
|
||||
panic!("Couldn't create data directory ./pasta_data: {:?}", error);
|
||||
}
|
||||
};
|
||||
|
||||
@ -79,27 +382,53 @@ async fn main() -> std::io::Result<()> {
|
||||
App::new()
|
||||
.app_data(data.clone())
|
||||
.wrap(middleware::NormalizePath::trim())
|
||||
.service(create::index)
|
||||
.service(help::help)
|
||||
.service(pasta_endpoint::getpasta)
|
||||
.service(pasta_endpoint::getrawpasta)
|
||||
.service(pasta_endpoint::redirecturl)
|
||||
.service(edit::get_edit)
|
||||
.service(edit::post_edit)
|
||||
.service(static_resources::static_resources)
|
||||
.service(actix_files::Files::new("/file", "./pasta_data/public/"))
|
||||
.service(web::resource("/upload").route(web::post().to(create::create)))
|
||||
.default_service(web::route().to(errors::not_found))
|
||||
.service(index)
|
||||
.service(getpasta)
|
||||
.service(redirecturl)
|
||||
.service(getrawpasta)
|
||||
.service(actix_files::Files::new("/static", "./static"))
|
||||
.service(actix_files::Files::new("/file", "./pasta_data"))
|
||||
.service(web::resource("/upload").route(web::post().to(create)))
|
||||
.default_service(web::route().to(not_found))
|
||||
.wrap(middleware::Logger::default())
|
||||
.service(remove::remove)
|
||||
.service(pastalist::list)
|
||||
.service(remove)
|
||||
.service(list)
|
||||
.wrap(Condition::new(
|
||||
ARGS.auth_username.is_some(),
|
||||
HttpAuthentication::basic(util::auth::auth_validator),
|
||||
args.auth_username.is_some(),
|
||||
HttpAuthentication::basic(auth_validator),
|
||||
))
|
||||
})
|
||||
.bind((ARGS.bind, ARGS.port))?
|
||||
.workers(ARGS.threads as usize)
|
||||
.bind(format!("0.0.0.0:{}", args.port.to_string()))?
|
||||
.workers(args.threads as usize)
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
fn remove_expired(pastas: &mut Vec<Pasta>) {
|
||||
// get current time - this will be needed to check which pastas have expired
|
||||
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||
Ok(n) => n.as_secs(),
|
||||
Err(_) => panic!("SystemTime before UNIX EPOCH!"),
|
||||
} as i64;
|
||||
|
||||
pastas.retain(|p| {
|
||||
// expiration is `never` or not reached
|
||||
if p.expiration == 0 || p.expiration > timenow {
|
||||
// keep
|
||||
true
|
||||
} else {
|
||||
// remove the file itself
|
||||
fs::remove_file(format!("./pasta_data/{}/{}", p.id_as_animals(), p.file));
|
||||
// and remove the containing directory
|
||||
fs::remove_dir(format!("./pasta_data/{}/", p.id_as_animals()));
|
||||
// remove
|
||||
false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn is_valid_url(url: &str) -> bool {
|
||||
let finder = LinkFinder::new();
|
||||
let spans: Vec<_> = finder.spans(url).collect();
|
||||
spans[0].as_str() == url && Some(&LinkKind::Url) == spans[0].kind()
|
||||
}
|
||||
|
52
src/pasta.rs
52
src/pasta.rs
@ -1,42 +1,15 @@
|
||||
use bytesize::ByteSize;
|
||||
use chrono::{Datelike, Local, TimeZone, Timelike};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::util::animalnumbers::to_animal_names;
|
||||
use crate::util::syntaxhighlighter::html_highlight;
|
||||
use chrono::{DateTime, Datelike, NaiveDateTime, Timelike, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct PastaFile {
|
||||
pub name: String,
|
||||
pub size: ByteSize,
|
||||
}
|
||||
|
||||
impl PastaFile {
|
||||
pub fn from_unsanitized(path: &str) -> Result<Self, &'static str> {
|
||||
let path = Path::new(path);
|
||||
let name = path.file_name().ok_or("Path did not contain a file name")?;
|
||||
let name = name.to_string_lossy().replace(' ', "_");
|
||||
Ok(Self {
|
||||
name,
|
||||
size: ByteSize::b(0),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
use crate::to_animal_names;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Pasta {
|
||||
pub id: u64,
|
||||
pub content: String,
|
||||
pub file: Option<PastaFile>,
|
||||
pub extension: String,
|
||||
pub private: bool,
|
||||
pub editable: bool,
|
||||
pub file: String,
|
||||
pub created: i64,
|
||||
pub expiration: i64,
|
||||
pub pasta_type: String,
|
||||
@ -48,9 +21,9 @@ impl Pasta {
|
||||
}
|
||||
|
||||
pub fn created_as_string(&self) -> String {
|
||||
let date = Local.timestamp(self.created, 0);
|
||||
let date = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(self.created, 0), Utc);
|
||||
format!(
|
||||
"{:02}-{:02} {:02}:{:02}",
|
||||
"{:02}-{:02} {}:{}",
|
||||
date.month(),
|
||||
date.day(),
|
||||
date.hour(),
|
||||
@ -62,9 +35,10 @@ impl Pasta {
|
||||
if self.expiration == 0 {
|
||||
String::from("Never")
|
||||
} else {
|
||||
let date = Local.timestamp(self.expiration, 0);
|
||||
let date =
|
||||
DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(self.expiration, 0), Utc);
|
||||
format!(
|
||||
"{:02}-{:02} {:02}:{:02}",
|
||||
"{:02}-{:02} {}:{}",
|
||||
date.month(),
|
||||
date.day(),
|
||||
date.hour(),
|
||||
@ -72,14 +46,6 @@ impl Pasta {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn content_syntax_highlighted(&self) -> String {
|
||||
html_highlight(&self.content, &self.extension)
|
||||
}
|
||||
|
||||
pub fn content_not_highlighted(&self) -> String {
|
||||
html_highlight(&self.content, "txt")
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Pasta {
|
||||
|
134
src/plugins.rs
Normal file
134
src/plugins.rs
Normal file
@ -0,0 +1,134 @@
|
||||
extern crate rutie;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use log::{error, log};
|
||||
use rutie::{AnyException, AnyObject, Object, RString, VM};
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, Read};
|
||||
use std::{fs, io};
|
||||
|
||||
const CACHE_PLUGINS: bool = false;
|
||||
|
||||
lazy_static! {
|
||||
static ref PLUGIN_IDENTIFIERS: Vec<String> = init();
|
||||
}
|
||||
|
||||
fn init() -> Vec<String> {
|
||||
VM::init();
|
||||
|
||||
let plugin_paths = load_plugin_paths();
|
||||
|
||||
let plugin_codes = read_plugins(plugin_paths.clone());
|
||||
|
||||
feed_plugins(plugin_codes);
|
||||
|
||||
let identifiers = get_plugin_identifiers(plugin_paths);
|
||||
|
||||
init_plugins(&identifiers);
|
||||
|
||||
identifiers
|
||||
}
|
||||
|
||||
pub fn pasta_filter(s: &str) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn on_pasta_read(s: &str) -> String {
|
||||
let mut processed_content: String = String::from(s);
|
||||
|
||||
for PLUGIN_IDENTIFIER in PLUGIN_IDENTIFIERS.iter() {
|
||||
processed_content = eval_for_string(PLUGIN_IDENTIFIER, "on_pasta_read", s);
|
||||
}
|
||||
|
||||
processed_content
|
||||
}
|
||||
|
||||
pub fn on_pasta_created(s: &str) -> String {
|
||||
let mut processed_content: String = String::from(s);
|
||||
|
||||
for PLUGIN_IDENTIFIER in PLUGIN_IDENTIFIERS.iter() {
|
||||
processed_content = eval_for_string(PLUGIN_IDENTIFIER, "on_pasta_created", s);
|
||||
}
|
||||
|
||||
processed_content
|
||||
}
|
||||
|
||||
pub fn init_plugins(plugin_identifiers: &Vec<String>) {
|
||||
for PLUGIN_IDENTIFIER in plugin_identifiers.iter() {
|
||||
eval_for_string(PLUGIN_IDENTIFIER, "init", "");
|
||||
|
||||
let init_result = eval_for_string(&PLUGIN_IDENTIFIER, "init", "");
|
||||
let id = eval_for_string(&PLUGIN_IDENTIFIER, "get_id", "");
|
||||
let name = eval_for_string(&id, "get_name", "");
|
||||
let version = eval_for_string(&id, "get_version", "");
|
||||
|
||||
log::info!("Initialised plugin {id} - {name} ({version})");
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_for_string(plugin_id: &str, function: &str, parameter: &str) -> String {
|
||||
match VM::eval(&*format!("MBP::{}::{}({})", plugin_id, function, parameter)) {
|
||||
Ok(result) => match result.try_convert_to::<RString>() {
|
||||
Ok(ruby_string) => ruby_string.to_string(),
|
||||
Err(err) => err.to_string(),
|
||||
},
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Failed to run function '{}' on plugin {}: {}",
|
||||
function,
|
||||
plugin_id,
|
||||
err
|
||||
);
|
||||
err.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_plugin_paths() -> Vec<String> {
|
||||
let paths = fs::read_dir("./plugins").expect("Failed to access ./plugins library.");
|
||||
|
||||
let mut plugin_paths: Vec<String> = Vec::new();
|
||||
|
||||
for path in paths {
|
||||
plugin_paths.push(path.unwrap().path().to_str().unwrap().parse().unwrap());
|
||||
}
|
||||
|
||||
plugin_paths
|
||||
}
|
||||
|
||||
fn read_plugins(plugin_paths: Vec<String>) -> Vec<String> {
|
||||
let mut plugin_codes: Vec<String> = Vec::new();
|
||||
|
||||
for plugin_path in plugin_paths {
|
||||
let plugin_code = match fs::read_to_string(&plugin_path) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
log::error!("Failed to read plugin file {}: {}", plugin_path, err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
plugin_codes.push(plugin_code);
|
||||
}
|
||||
|
||||
plugin_codes
|
||||
}
|
||||
|
||||
fn feed_plugins(plugin_codes: Vec<String>) {
|
||||
for plugin_code in plugin_codes {
|
||||
match VM::eval(plugin_code.as_str()) {
|
||||
Ok(result) => {}
|
||||
Err(error) => {
|
||||
log::error!("Failed to initialise plugin: {}", error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_plugin_identifiers(plugin_paths: Vec<String>) -> Vec<String> {
|
||||
let mut plugin_ids: Vec<String> = Vec::new();
|
||||
for plugin_path in plugin_paths {
|
||||
plugin_ids.push(plugin_path.replace("./plugins/", "").replace(".rb", ""))
|
||||
}
|
||||
plugin_ids
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
use actix_web::dev::ServiceRequest;
|
||||
use actix_web::{error, Error};
|
||||
use actix_web_httpauth::extractors::basic::BasicAuth;
|
||||
|
||||
use crate::args::ARGS;
|
||||
|
||||
pub async fn auth_validator(
|
||||
req: ServiceRequest,
|
||||
credentials: BasicAuth,
|
||||
) -> Result<ServiceRequest, Error> {
|
||||
// check if username matches
|
||||
if credentials.user_id().as_ref() == ARGS.auth_username.as_ref().unwrap() {
|
||||
return match ARGS.auth_password.as_ref() {
|
||||
Some(cred_pass) => match credentials.password() {
|
||||
None => Err(error::ErrorBadRequest("Invalid login details.")),
|
||||
Some(arg_pass) => {
|
||||
if arg_pass == cred_pass {
|
||||
Ok(req)
|
||||
} else {
|
||||
Err(error::ErrorBadRequest("Invalid login details."))
|
||||
}
|
||||
}
|
||||
},
|
||||
None => Ok(req),
|
||||
};
|
||||
} else {
|
||||
Err(error::ErrorBadRequest("Invalid login details."))
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use linkify::{LinkFinder, LinkKind};
|
||||
use std::fs;
|
||||
|
||||
use crate::{dbio, Pasta};
|
||||
|
||||
pub fn remove_expired(pastas: &mut Vec<Pasta>) {
|
||||
// get current time - this will be needed to check which pastas have expired
|
||||
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||
Ok(n) => n.as_secs(),
|
||||
Err(_) => {
|
||||
log::error!("SystemTime before UNIX EPOCH!");
|
||||
0
|
||||
}
|
||||
} as i64;
|
||||
|
||||
pastas.retain(|p| {
|
||||
// expiration is `never` or not reached
|
||||
if p.expiration == 0 || p.expiration > timenow {
|
||||
// keep
|
||||
true
|
||||
} else {
|
||||
// remove the file itself
|
||||
if let Some(file) = &p.file {
|
||||
if fs::remove_file(format!(
|
||||
"./pasta_data/public/{}/{}",
|
||||
p.id_as_animals(),
|
||||
file.name()
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
log::error!("Failed to delete file {}!", file.name())
|
||||
}
|
||||
|
||||
// and remove the containing directory
|
||||
if fs::remove_dir(format!("./pasta_data/public/{}/", p.id_as_animals())).is_err() {
|
||||
log::error!("Failed to delete directory {}!", file.name())
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
dbio::save_to_file(pastas);
|
||||
}
|
||||
|
||||
pub fn is_valid_url(url: &str) -> bool {
|
||||
let finder = LinkFinder::new();
|
||||
let spans: Vec<_> = finder.spans(url).collect();
|
||||
spans[0].as_str() == url && Some(&LinkKind::Url) == spans[0].kind()
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
use syntect::easy::HighlightLines;
|
||||
use syntect::highlighting::{Style, ThemeSet};
|
||||
use syntect::html::append_highlighted_html_for_styled_line;
|
||||
use syntect::html::IncludeBackground::No;
|
||||
use syntect::parsing::SyntaxSet;
|
||||
use syntect::util::LinesWithEndings;
|
||||
|
||||
pub fn html_highlight(text: &str, extension: &str) -> String {
|
||||
let ps = SyntaxSet::load_defaults_newlines();
|
||||
let ts = ThemeSet::load_defaults();
|
||||
|
||||
let syntax = ps
|
||||
.find_syntax_by_extension(extension)
|
||||
.or(Option::from(ps.find_syntax_plain_text()))
|
||||
.unwrap();
|
||||
let mut h = HighlightLines::new(syntax, &ts.themes["InspiredGitHub"]);
|
||||
|
||||
let mut highlighted_content: String = String::from("");
|
||||
|
||||
for line in LinesWithEndings::from(text) {
|
||||
let ranges: Vec<(Style, &str)> = h.highlight_line(line, &ps).unwrap();
|
||||
append_highlighted_html_for_styled_line(&ranges[..], No, &mut highlighted_content)
|
||||
.expect("Failed to append highlighted line!");
|
||||
}
|
||||
|
||||
let mut highlighted_content2: String = String::from("");
|
||||
for line in highlighted_content.lines() {
|
||||
highlighted_content2 += &*format!("<code-line>{}</code-line>\n", line);
|
||||
}
|
||||
|
||||
// Rewrite colours to ones that are compatible with water.css and both light/dark modes
|
||||
highlighted_content2 = highlighted_content2.replace("style=\"color:#323232;\"", "");
|
||||
highlighted_content2 =
|
||||
highlighted_content2.replace("style=\"color:#183691;\"", "style=\"color:blue;\"");
|
||||
|
||||
return highlighted_content2;
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
{% include "header.html" %}
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<h4>
|
||||
Editing pasta '{{ pasta.id_as_animals() }}'
|
||||
</h4>
|
||||
<label>Content</label>
|
||||
<br>
|
||||
<textarea style="width: 100%; min-height: 100px" name="content" id="content" autofocus>{{ pasta.content }}</textarea>
|
||||
<br>
|
||||
|
||||
{% if args.readonly %}
|
||||
<input style="width: 140px; background-color: limegreen" disabled type="submit" value="Read Only"/>
|
||||
{%- else %}
|
||||
<input style="width: 140px; background-color: limegreen" type="submit" value="Save"/>
|
||||
{%- endif %}
|
||||
</td>
|
||||
|
||||
<br>
|
||||
</form>
|
||||
{% include "footer.html" %}
|
@ -3,12 +3,8 @@
|
||||
|
||||
<hr>
|
||||
<p style="font-size: smaller">
|
||||
{% if args.footer_text.as_ref().is_none() %}
|
||||
MicroBin by Dániel Szabó. Fork me on <a href="https://github.com/szabodanika/microbin">GitHub</a>!
|
||||
Let's keep the Web <b>compact</b>, <b>accessible</b> and <b>humane</b>!
|
||||
{%- else %}
|
||||
{{ args.footer_text.as_ref().unwrap() }}
|
||||
{%- endif %}
|
||||
MicroBin by Daniel Szabo. Fork me on <a href="https://github.com/szabodanika/microbin">GitHub</a>!
|
||||
Let's keep the Web <b>compact</b>, <b>accessible</b> and <b>humane</b>!
|
||||
</p>
|
||||
|
||||
{%- endif %}
|
||||
|
@ -1,60 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{% if args.title.as_ref().is_none() %}
|
||||
<title>MicroBin</title>
|
||||
{%- else %}
|
||||
<title>{{ args.title.as_ref().unwrap() }}</title>
|
||||
{%- endif %}
|
||||
|
||||
<title>MicroBin</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{% if !args.pure_html %}
|
||||
<link rel="stylesheet" href="/static/water.css">
|
||||
{%- endif %}
|
||||
</head>
|
||||
{% if args.wide %}
|
||||
<body style="
|
||||
max-width: 1080;
|
||||
max-width: 720px;
|
||||
margin: auto;
|
||||
padding-left:0.5rem;
|
||||
padding-right:0.5rem;
|
||||
line-height: 1.5;
|
||||
font-size: 1.1em;
|
||||
">
|
||||
{%- else %}
|
||||
<body style="
|
||||
max-width: 720;
|
||||
margin: auto;
|
||||
padding-left:0.5rem;
|
||||
padding-right:0.5rem;
|
||||
line-height: 1.5;
|
||||
font-size: 1.1em;
|
||||
">
|
||||
{%- endif %}
|
||||
|
||||
<br>
|
||||
|
||||
{% if !args.hide_header %}
|
||||
|
||||
<b style="margin-right: 0.5rem">
|
||||
|
||||
{% if !args.hide_logo %}
|
||||
<i><span style="font-size:2.2rem; margin-right:1rem">μ</span></i>
|
||||
{%- endif %}
|
||||
|
||||
{% if args.title.as_ref().is_none() %}
|
||||
MicroBin
|
||||
{%- else %}
|
||||
{{ args.title.as_ref().unwrap() }}
|
||||
{%- endif %}
|
||||
<i><span style="font-size:2.2rem; margin-right:1rem">μ</span></i> MicroBin
|
||||
</b>
|
||||
|
||||
<a href="/" style="margin-right: 0.5rem; margin-left: 0.5rem">New Pasta</a>
|
||||
|
||||
<a href="/pastalist" style="margin-right: 0.5rem; margin-left: 0.5rem">Pasta List</a>
|
||||
|
||||
<a href="/help" style="margin-right: 0.5rem; margin-left: 0.5rem">Help</a>
|
||||
|
||||
<a href="https://github.com/szabodanika/microbin" style="margin-right: 0.5rem; margin-left: 0.5rem">GitHub</a>
|
||||
|
||||
<hr>
|
||||
|
||||
{%- endif %}
|
||||
|
@ -1,160 +0,0 @@
|
||||
{% include "header.html" %}
|
||||
|
||||
|
||||
<h2 id="welcome-to-the-microbin-wiki-">Welcome to MicroBin!</h2>
|
||||
<p>This page contains help regarding the installation, configuration and use of MicroBin. If you have questions that are not answered here, head to our <a href="https://github.com/szabodanika/microbin">GitHub repository</a>.</p>
|
||||
<h2 id="1-usage">1 Usage</h2>
|
||||
<h3 id="what-is-a-pasta-anyway-">What is a "pasta" anyway?</h3>
|
||||
<p>In microbin, a pasta can be:</p>
|
||||
<ul>
|
||||
<li>A text that you want to paste from one machine to another, eg. some code</li>
|
||||
<li>A file that you want to share, eg. a video that is too large for Discord, a zip with a code project in it or an image</li>
|
||||
<li>A URL redirect</li>
|
||||
</ul>
|
||||
<h3 id="when-is-microbin-useful-">When is MicroBin useful?</h3>
|
||||
<p>You can use MicroBin</p>
|
||||
<ul>
|
||||
<li>As a URL shortener/redirect service,</li>
|
||||
<li>To send long texts to other people,</li>
|
||||
<li>To send large files to other people,</li>
|
||||
<li>To serve content on the web, eg. configuration files for testing, images, or any other file content using the Raw functionality,</li>
|
||||
<li>To move files between your desktop and a server you access from the console,</li>
|
||||
<li>As a "postbox" service where people can upload their files or texts, but they cannot see or remove what others sent you - just disable the pastalist page</li>
|
||||
<li>To take notes! Simply create an editable pasta.</li>
|
||||
</ul>
|
||||
<p>...and many other things, why not get creative?</p>
|
||||
<h3 id="creating-a-pasta">Creating a Pasta</h3>
|
||||
<p>Navigate to the root of your server, for example <a href="https://microbin.myserver.com/">https://microbin.myserver.com/</a>. This should show you a form where you will at the very least see an expiration selector, a file attachment input, a content text field and a green save button. Depending on your configuration there miight also be a syntax highlight selector, an editable checkbox and a private ceckbox.</p>
|
||||
<p>Use the expiration dropdown to choose how long you want your pasta to exist. When the selected time has expired, it will be removed from the server. The content can be any text, including plain text, code, html, even a URL. A URL is a special case, because when you open the pasta again, it will redirect you to that URL instead of showing it as a text. Entering content is optional, and so is the file attachment. If you want, you can even submit a pasta completely empty.</p>
|
||||
<p>You will be redirected to the URL of the pasta, which will end with a few animal names. If you remember those animals, you can simply type them in on another machine and open your pasta elsewhere.</p>
|
||||
<p>If you have editable pastas enabled and you check the editable checkbox, then later on there will be an option to change the text content of your pasta. Selecting the private checkbox will simply prevent your pasta to show up on the pasta list page, if that is enabled.</p>
|
||||
<p>If you have syntax higlighting enabled, then select your language from the dropdown, or leave it as none if you just want to upload plain with no highlighting.</p>
|
||||
<h3 id="listing-pastas">Listing Pastas</h3>
|
||||
<p>If you have pasta listing enabled, then there is a pasta list option in the navigation bar, which will list all the pastas on the server in two groups: regular pastas and URL redirects (pastas containing nothing but a URL). If you have private pastas enabled, they will not show up here at all.</p>
|
||||
<p>From the pasta list page, you will be able to view individual pastas by clicking on their animal identifiers on the lest, view their raw contrent by clicking on the Raw button, remove them, and if you have editable pastas enabled, then open them in edit view.</p>
|
||||
<h3 id="use-microbin-from-the-console-with-curl">Use MicroBin from the console with cURL</h3>
|
||||
<p>Simple text Pasta: <code>curl -d "expiration=10min&content=This is a test pasta" -X POST https://microbin.myserver.com/create</code></p>
|
||||
<p>File contents: <code>curl -d "expiration=10min&content=$( < mypastafile.txt )" -X POST https://microbin.myserver.com/create</code></p>
|
||||
<p>Available expiration options:
|
||||
<code>1min</code>, <code>10min</code>, <code>1hour</code>, <code>24hour</code>, <code>1week</code>, <code>never</code></p>
|
||||
<p>Use cURL to read the pasta: <code>curl https://microbin.myserver.com/rawpasta/fish-pony-crow</code>,</p>
|
||||
<p>or to download the pasta: <code>curl https://microbin.myserver.com/rawpasta/fish-pony-crow > output.txt</code> (use /file instead of /rawpasta to download attached file).</p>
|
||||
<h2 id="2-installation">2 Installation</h2>
|
||||
<h3 id="from-cargo">From Cargo</h3>
|
||||
<p>Install from Cargo:</p>
|
||||
<p><code>cargo install microbin</code></p>
|
||||
<p>Remember, MicroBin will create your database and file storage wherever you execute it. I recommend that you create a folder for it first and execute it there:</p>
|
||||
<p><code>mkdir ~/microbin/</code></p>
|
||||
<p><code>cd ~/microbin/</code></p>
|
||||
<p><code>microbin --port 8080 --highlightsyntax --editable</code></p>
|
||||
<h3 id="building-microbin">Building MicroBin</h3>
|
||||
<p>Simply clone the repository, build it with <code>cargo build --release</code> and run the <code>microbin</code> executable in the created <code>target/release/</code> directory. It will start on port 8080. You can change the port with <code>-p</code> or <code>--port</code> CL arguments. For other arguments see <a href="https://github.com/szabodanika/microbin/wiki">the Wiki</a>.</p>
|
||||
<pre><code>git clone https:<span class="hljs-comment">//github.com/szabodanika/microbin.git</span>
|
||||
cd microbin
|
||||
cargo build --<span class="hljs-built_in">release</span>
|
||||
./target/<span class="hljs-built_in">release</span>/microbin -p <span class="hljs-number">80</span>
|
||||
</code></pre><h3 id="microbin-as-a-service">MicroBin as a service</h3>
|
||||
<p>To install it as a service on your Linux machine, create a file called <code>/etc/systemd/system/microbin.service</code>, paste this into it with the <code>[username]</code> and <code>[path to installation directory]</code> replaced with the actual values. If you installed MicroBin from Cargo, your executable will be in your system's Cargo directory, e.g. <code>/Users/daniel/.cargo/bin/microbin</code>.</p>
|
||||
<pre><code><span class="hljs-section">[Unit]</span>
|
||||
<span class="hljs-attr">Description</span>=MicroBin
|
||||
<span class="hljs-attr">After</span>=network.target
|
||||
<span class="hljs-section">
|
||||
[Service]</span>
|
||||
<span class="hljs-attr">Type</span>=simple
|
||||
<span class="hljs-attr">Restart</span>=always
|
||||
<span class="hljs-attr">User</span>=[username]
|
||||
<span class="hljs-attr">RootDirectory</span>=/
|
||||
<span class="hljs-attr">WorkingDirectory</span>=[path to installation directory]
|
||||
<span class="hljs-attr">ExecStart</span>=[path to installation directory]/target/release/microbin
|
||||
<span class="hljs-section">
|
||||
[Install]</span>
|
||||
<span class="hljs-attr">WantedBy</span>=multi-user.target
|
||||
</code></pre><p>Here is my <code>microbin.service</code> for example, with some optional arguments:</p>
|
||||
<pre><code><span class="hljs-section">[Unit]</span>
|
||||
<span class="hljs-attr">Description</span>=MicroBin
|
||||
<span class="hljs-attr">After</span>=network.target
|
||||
<span class="hljs-section">
|
||||
[Service]</span>
|
||||
<span class="hljs-attr">Type</span>=simple
|
||||
<span class="hljs-attr">Restart</span>=always
|
||||
<span class="hljs-attr">User</span>=ubuntu
|
||||
<span class="hljs-attr">RootDirectory</span>=/
|
||||
|
||||
<span class="hljs-comment"># This is the directory where I want to run microbin. It will store all the pastas here.</span>
|
||||
<span class="hljs-attr">WorkingDirectory</span>=/home/ubuntu/server/microbin
|
||||
|
||||
<span class="hljs-comment"># This is the location of my executable - I also have 2 optional features enabled</span>
|
||||
<span class="hljs-attr">ExecStart</span>=/home/ubuntu/server/microbin/target/release/microbin --editable --linenumbers --highlightsyntax
|
||||
|
||||
<span class="hljs-comment"># I keep my installation in the home directory, so I need to add this</span>
|
||||
<span class="hljs-attr">ProtectHome</span>=<span class="hljs-literal">off</span>
|
||||
<span class="hljs-section">
|
||||
[Install]</span>
|
||||
<span class="hljs-attr">WantedBy</span>=multi-user.target
|
||||
</code></pre><p>Then start the service with <code>systemctl start microbin</code> and enable it on boot with <code>systemctl enable microbin</code>. To update your MicroBin, simply update or clone the repository again, build it again, and then restart the service with <code>systemctl restart microbin</code>. An update will never affect your existing pastas, unless there is a breaking change in the data model (in which case MicroBin just won't be able to import your DB), which will always be mentioned explicitly.</p>
|
||||
<h3 id="nginx-configuration">NGINX configuration</h3>
|
||||
<pre><code><span class="hljs-section">server</span> {
|
||||
<span class="hljs-comment"># I have HTTPS enabled using certbot - you can use HTTP of course if you want!</span>
|
||||
<span class="hljs-attribute">listen</span> <span class="hljs-number">443</span> ssl; <span class="hljs-comment"># managed by Certbot</span>
|
||||
|
||||
<span class="hljs-attribute">server_name</span> microbin.myserver.com;
|
||||
|
||||
<span class="hljs-attribute">location</span> / {
|
||||
<span class="hljs-comment"># Make sure to change the port if you are not running MicroBin at 8080!</span>
|
||||
<span class="hljs-attribute">proxy_pass</span> http://127.0.0.1:8080<span class="hljs-variable">$request_uri</span>;
|
||||
<span class="hljs-attribute">proxy_set_header</span> Host <span class="hljs-variable">$host</span>;
|
||||
<span class="hljs-attribute">proxy_set_header</span> X-Forwarded-Proto <span class="hljs-variable">$scheme</span>;
|
||||
<span class="hljs-attribute">proxy_set_header</span> X-Real-IP <span class="hljs-variable">$remote_addr</span>;
|
||||
<span class="hljs-attribute">proxy_set_header</span> X-Forwarded-For <span class="hljs-variable">$proxy_add_x_forwarded_for</span>;
|
||||
}
|
||||
|
||||
<span class="hljs-comment"># Limit content size - I have 1GB because my MicroBin server is private, no one else will use it.</span>
|
||||
<span class="hljs-attribute">client_max_body_size</span> <span class="hljs-number">1024M</span>;
|
||||
|
||||
<span class="hljs-attribute">ssl_certificate</span> /etc/letsencrypt/live/microbin.myserver.com/fullchain.pem; <span class="hljs-comment"># managed by Certbot</span>
|
||||
<span class="hljs-attribute">ssl_certificate_key</span> /etc/letsencrypt/live/microbin.myserver.com/privkey.pem; <span class="hljs-comment"># managed by Certbot</span>
|
||||
<span class="hljs-attribute">include</span> /etc/letsencrypt/options-ssl-nginx.conf; <span class="hljs-comment"># managed by Certbot</span>
|
||||
<span class="hljs-attribute">ssl_dhparam</span> /etc/letsencrypt/ssl-dhparams.pem; <span class="hljs-comment"># managed by Certbot</span>
|
||||
}
|
||||
</code></pre><h2 id="3-command-line-arguments">3 Command Line Arguments</h2>
|
||||
<p>There is an ever expanding list of customisations built into MicroBin so you can use it the way you want. Instead of a configuration file, we simply use arguments that you pass to the executable, making the workflow even simpler. Read the following options and if you cannot find what you need, you can always open an issue at our GitHub repository and request a new feature!</p>
|
||||
<h3 id="-auth-username-auth_username-">--auth-username [AUTH_USERNAME]</h3>
|
||||
<p>Require username for HTTP Basic Authentication when visiting the service. If <code>--auth-username</code> is set but <code>--auth-password</code> is not, just leave the password field empty when logging in. You can also just go to <a href="https://username:password@yourserver.net">https://username:password@yourserver.net</a> or <a href="https://username@yourserver.net">https://username@yourserver.net</a> if password is not set instead of typing into the password</p>
|
||||
<h3 id="-auth-password-auth_password-">--auth-password [AUTH_PASSWORD]</h3>
|
||||
<p>Require password for HTTP Basic Authentication when visiting the service. Will not have any affect unless <code>--auth-username</code> is also set. If <code>--auth-username</code> is set but <code>--auth-password</code> is not, just leave the password field empty when logging in. You can also just go to <a href="https://username:password@yourserver.net">https://username:password@yourserver.net</a> or <a href="https://username@yourserver.net">https://username@yourserver.net</a> if password is not set instead of typing into the password prompt.</p>
|
||||
<h3 id="-editable">--editable</h3>
|
||||
<p>Enables editable pastas. You will still be able to make finalised pastas but there will be an extra checkbox to make your new pasta editable from the pasta list or the pasta view page.</p>
|
||||
<h3 id="-footer_text-text-">--footer_text [TEXT]</h3>
|
||||
<p>Replaces the default footer text with your own. If you want to hide the footer, use --hide-footer instead.</p>
|
||||
<h3 id="-h-help">-h, --help</h3>
|
||||
<p>Show all commands in the terminal.</p>
|
||||
<h3 id="-hide-footer">--hide-footer</h3>
|
||||
<p>Hides the footer on every page.</p>
|
||||
<h3 id="-hide-header">--hide-header</h3>
|
||||
<p>Hides the navigation bar on every page.</p>
|
||||
<h3 id="-hide-logo">--hide-logo</h3>
|
||||
<p>Hides the MicroBin logo from the navigation bar on every page.</p>
|
||||
<h3 id="-no-listing">--no-listing</h3>
|
||||
<p>Disables the /pastalist endpoint, essentially making all pastas private.</p>
|
||||
<h3 id="-highlightsyntax">--highlightsyntax</h3>
|
||||
<p>Enables syntax highlighting support. When creating a new pasta, a new dropdown selector will be added where you can select your pasta's syntax, or just leave it empty for no highlighting.</p>
|
||||
<h3 id="-p-port-port-">-p, --port [PORT]</h3>
|
||||
<p>Default value: 8080</p>
|
||||
<p>Sets the port for the server will be listening on.</p>
|
||||
<h3 id="-private">--private</h3>
|
||||
<p>Enables private pastas. Adds a new checkbox to make your pasta private, which then won't show up on the pastalist page. With the URL to your pasta, it will still be accessible.</p>
|
||||
<h3 id="-pure-html">--pure-html</h3>
|
||||
<p>Disables main CSS styling, just uses a few in-line stylings for the layout. With this option you will lose dark-mode support.</p>
|
||||
<h3 id="-readonly">--readonly</h3>
|
||||
<p>Disables adding/editing/removing pastas entirely.</p>
|
||||
<h3 id="-title-title-">--title [TITLE]</h3>
|
||||
<p>Replaces "MicroBin" with your title of choice in the navigation bar.</p>
|
||||
<h3 id="-t-threads-threads-">-t, --threads [THREADS]</h3>
|
||||
<p>Default value: 1</p>
|
||||
<p>Number of workers MicroBin is allowed to have. Increase this to the number of CPU cores you have if you want to go beast mode, but for personal use one worker is enough.</p>
|
||||
<h3 id="-v-version">-V, --version</h3>
|
||||
<p>Displays your MicroBin's version information.</p>
|
||||
<h3 id="-wide">--wide</h3>
|
||||
<p>Changes the maximum width of the UI from 720 pixels to 1080 pixels.</p>
|
||||
|
||||
{% include "footer.html" %}
|
@ -1,99 +1,27 @@
|
||||
{% include "header.html" %}
|
||||
<form action="upload" method="POST" enctype="multipart/form-data">
|
||||
<br>
|
||||
<div style="display: grid;
|
||||
grid-gap: 4px;
|
||||
grid-template-columns: repeat(auto-fit, 234px);
|
||||
grid-template-rows: repeat(1, 100px); ">
|
||||
<div>
|
||||
<label for="expiration">Expiration</label><br>
|
||||
<select style="width: 100%;" name="expiration" id="expiration">
|
||||
<optgroup label="Expire">
|
||||
<option value="1min">1 minute</option>
|
||||
<option value="10min">10 minutes</option>
|
||||
<option value="1hour">1 hour</option>
|
||||
<option selected value="24hour">24 hours</option>
|
||||
<option value="1week">1 week</option>
|
||||
</optgroup>
|
||||
<option value="never">Never Expire</option>
|
||||
</select>
|
||||
</div>
|
||||
{% if args.highlightsyntax %}
|
||||
<div>
|
||||
<label for="syntax-highlight">Syntax Highlighting</label><br>
|
||||
<select style="width: 100%;" name="syntax-highlight" id="syntax-highlight">
|
||||
<option value="none">None</option>
|
||||
<optgroup label="Source Code">
|
||||
<option value="sh">Bash Shell</option>
|
||||
<option value="c">C</option>
|
||||
<option value="cpp">C++</option>
|
||||
<option value="cs">C#</option>
|
||||
<option value="pas">Delphi</option>
|
||||
<option value="erl">Erlang</option>
|
||||
<option value="go">Go</option>
|
||||
<option value="hs">Haskell</option>
|
||||
<option value="html">HTML</option>
|
||||
<option value="lua">Lua</option>
|
||||
<option value="lisp">Lisp</option>
|
||||
<option value="java">Java</option>
|
||||
<option value="js">JavaScript</option>
|
||||
<option value="kt">Kotlin</option>
|
||||
<option value="py">Python</option>
|
||||
<option value="php">PHP</option>
|
||||
<option value="r">R</option>
|
||||
<option value="rs">Rust</option>
|
||||
<option value="rb">Ruby</option>
|
||||
<option value="sc">Scala</option>
|
||||
<option value="swift">Swift</option>
|
||||
</optgroup>
|
||||
<optgroup label="Descriptors">
|
||||
<!-- no toml support ;-( -->
|
||||
<option value="json">TOML</option>
|
||||
<option value="yaml">YAML</option>
|
||||
<option value="json">JSON</option>
|
||||
<option value="xml">XML</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
{%- else %}
|
||||
<input type="hidden" name="syntax-highlight" value="none">
|
||||
{%- endif %}
|
||||
|
||||
<div>
|
||||
<label>File attachment</label>
|
||||
<br>
|
||||
<input style="width: 100%;" type="file" id="file" name="file">
|
||||
</div>
|
||||
</div>
|
||||
<label for="expiration">Expiration</label><br>
|
||||
<select style="width: 100%;" name="expiration" id="expiration">
|
||||
<optgroup label="Expire">
|
||||
<option value="1min">1 minute</option>
|
||||
<option value="10min">10 minutes</option>
|
||||
<option value="1hour">1 hour</option>
|
||||
<option selected value="24hour">24 hours</option>
|
||||
<option value="1week">1 week</option>
|
||||
</optgroup>
|
||||
<option value="never">Never Expire</option>
|
||||
</select>
|
||||
<br>
|
||||
<label>Content</label>
|
||||
<br>
|
||||
<textarea style="width: 100%; min-height: 100px" name="content" autofocus></textarea>
|
||||
<br>
|
||||
<div style="display: grid;
|
||||
grid-gap: 4px;
|
||||
grid-template-columns: repeat(auto-fit, 120px);
|
||||
grid-template-rows: repeat(1, 50px); ">
|
||||
{% if args.editable %}
|
||||
<div>
|
||||
<input type="checkbox" id="editable" name="editable" value="editable">
|
||||
<label for="editable">Editable</label>
|
||||
</div>
|
||||
{%- endif %}
|
||||
{% if args.private %}
|
||||
<div>
|
||||
<input type="checkbox" id="private" name="private" value="private">
|
||||
<label for="private">Private</label>
|
||||
</div>
|
||||
{%- endif %}
|
||||
</div>
|
||||
|
||||
{% if args.readonly %}
|
||||
<input style="width: 140px; background-color: limegreen" disabled type="submit" value="Read Only"/>
|
||||
{%- else %}
|
||||
<input style="width: 140px; background-color: limegreen" type="submit" value="Save"/>
|
||||
{%- endif %}
|
||||
</td>
|
||||
|
||||
<label>File attachment</label>
|
||||
<br>
|
||||
<input style="width: 100%;" type="file" id="file" name="file">
|
||||
<br>
|
||||
<input style="width: 120px; background-color: limegreen" ; type="submit" value="Save"/>
|
||||
<br>
|
||||
</form>
|
||||
{% include "footer.html" %}
|
||||
|
@ -1,53 +1,9 @@
|
||||
{% include "header.html" %}
|
||||
<div style="float: left">
|
||||
<a style="margin-right: 0.5rem" href="/raw/{{pasta.id_as_animals()}}">Raw Text Content</a>
|
||||
{% if pasta.file.is_some() %}
|
||||
<a style="margin-right: 0.5rem; margin-left: 0.5rem"
|
||||
href="/file/{{pasta.id_as_animals()}}/{{pasta.file.as_ref().unwrap().name()}}">
|
||||
Attached file'{{pasta.file.as_ref().unwrap().name()}}' [{{pasta.file.as_ref().unwrap().size}}]
|
||||
</a>
|
||||
{%- endif %}
|
||||
{% if pasta.editable %}
|
||||
<a style="margin-right: 0.5rem; margin-left: 0.5rem" href="/edit/{{pasta.id_as_animals()}}">Edit</a>
|
||||
{%- endif %}
|
||||
<a style="margin-right: 0.5rem; margin-left: 0.5rem" href="/remove/{{pasta.id_as_animals()}}">Remove</a>
|
||||
</div>
|
||||
<div style="float: right">
|
||||
<a href="/pasta/{{pasta.id_as_animals()}}"><i>{{pasta.id_as_animals()}}</i></a>
|
||||
</div>
|
||||
<br>
|
||||
<div style="clear: both;">
|
||||
{% if args.highlightsyntax %}
|
||||
<pre><code>{{pasta.content_syntax_highlighted()}}</code></pre>
|
||||
{%- else %}
|
||||
<pre><code>{{pasta.content_not_highlighted()}}</code></pre>
|
||||
{%- endif %}
|
||||
</div>
|
||||
<style>
|
||||
code-line {
|
||||
counter-increment: listing;
|
||||
text-align: right;
|
||||
float: left;
|
||||
clear: left;
|
||||
}
|
||||
|
||||
code-line::before {
|
||||
content: counter(listing);
|
||||
display: inline-block;
|
||||
float: left;
|
||||
padding-left: auto;
|
||||
margin-left: auto;
|
||||
text-align: left;
|
||||
width: 1.6rem;
|
||||
border-right: 1px solid lightgrey;
|
||||
color: grey;
|
||||
margin-right: 0.4rem;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
<a style="margin-right: 0.5rem" href="/raw/{{pasta.id_as_animals()}}">Raw Text Content</a>
|
||||
{% if pasta.file != "no-file" %}
|
||||
<a style="margin-right: 0.5rem; margin-left: 0.5rem" href="/file/{{pasta.id_as_animals()}}/{{pasta.file}}">Attached file '{{pasta.file}}'</a>
|
||||
{%- endif %}
|
||||
<a style="margin-right: 0.5rem; margin-left: 0.5rem" href="/remove/{{pasta.id_as_animals()}}">Remove</a>
|
||||
<pre><code>{{pasta}}</code></pre>
|
||||
|
||||
{% include "footer.html" %}
|
||||
|
@ -4,111 +4,97 @@
|
||||
{% if pastas.is_empty() %}
|
||||
<br>
|
||||
<p>
|
||||
No pastas yet. 😔 Create one <a href="/">here</a>.
|
||||
No pastas yet. 😔 Create one <a href="/">here</a>.
|
||||
</p>
|
||||
<br>
|
||||
{%- else %}
|
||||
<br>
|
||||
{% if args.pure_html %}
|
||||
<table border="1" style="width: 100%">
|
||||
{% else %}
|
||||
<table style="width: 100%">
|
||||
{% endif %}
|
||||
<thead>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="4">Pastas</th>
|
||||
<th colspan="4">Pastas</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Key
|
||||
</th>
|
||||
<th>
|
||||
Created
|
||||
</th>
|
||||
<th>
|
||||
Expiration
|
||||
</th>
|
||||
<th>
|
||||
<th>
|
||||
Key
|
||||
</th>
|
||||
<th>
|
||||
Created
|
||||
</th>
|
||||
<th>
|
||||
Expiration
|
||||
</th>
|
||||
<th>
|
||||
|
||||
</th>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for pasta in pastas %}
|
||||
{% if pasta.pasta_type == "text" && !pasta.private %}
|
||||
{% if pasta.pasta_type == "text" %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/pasta/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.created_as_string()}}
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.expiration_as_string()}}
|
||||
</td>
|
||||
<td>
|
||||
<a style="margin-right:1rem" href="/raw/{{pasta.id_as_animals()}}">Raw</a>
|
||||
{% if pasta.file.is_some() %}
|
||||
<a style="margin-right:1rem" href="/file/{{pasta.id_as_animals()}}/{{pasta.file.as_ref().unwrap().name()}}">File</a>
|
||||
{%- endif %}
|
||||
{% if pasta.editable %}
|
||||
<a style="margin-right:1rem" href="/edit/{{pasta.id_as_animals()}}">Edit</a>
|
||||
{%- endif %}
|
||||
<a href="/remove/{{pasta.id_as_animals()}}">Remove</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/pasta/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.created_as_string()}}
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.expiration_as_string()}}
|
||||
</td>
|
||||
<td>
|
||||
<a style="margin-right:1rem" href="/raw/{{pasta.id_as_animals()}}">Raw</a>
|
||||
{% if pasta.file != "no-file" %}
|
||||
<a style="margin-right:1rem" href="/file/{{pasta.id_as_animals()}}/{{pasta.file}}">File</a>
|
||||
{%- endif %}
|
||||
<a href="/remove/{{pasta.id_as_animals()}}">Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
{% if args.pure_html %}
|
||||
<table border="1" style="width: 100%">
|
||||
{% else %}
|
||||
<table style="width: 100%">
|
||||
{% endif %}
|
||||
<thead>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="4">URL Redirects</th>
|
||||
<th colspan="4">URL Redirects</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Key
|
||||
</th>
|
||||
<th>
|
||||
Created
|
||||
</th>
|
||||
<th>
|
||||
Expiration
|
||||
</th>
|
||||
<th>
|
||||
<tr >
|
||||
<th>
|
||||
Key
|
||||
</th>
|
||||
<th>
|
||||
Created
|
||||
</th>
|
||||
<th>
|
||||
Expiration
|
||||
</th>
|
||||
<th>
|
||||
|
||||
</th>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for pasta in pastas %}
|
||||
{% if pasta.pasta_type == "url" && !pasta.private %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/url/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.created_as_string()}}
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.expiration_as_string()}}
|
||||
</td>
|
||||
<td>
|
||||
<a style="margin-right:1rem" href="/raw/{{pasta.id_as_animals()}}">Raw</a>
|
||||
{% if pasta.editable %}
|
||||
<a style="margin-right:1rem" href="/edit/{{pasta.id_as_animals()}}">Edit</a>
|
||||
{%- endif %}
|
||||
<a href="/remove/{{pasta.id_as_animals()}}">Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</thead>
|
||||
{% for pasta in pastas %}
|
||||
{% if pasta.pasta_type == "url" %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/url/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.created_as_string()}}
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.expiration_as_string()}}
|
||||
</td>
|
||||
<td>
|
||||
<a style="margin-right:1rem" href="/raw/{{pasta.id_as_animals()}}">Raw</a>
|
||||
<a href="/remove/{{pasta.id_as_animals()}}">Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
{%- endif %}
|
||||
|
Loading…
x
Reference in New Issue
Block a user