Merge pull request #85 from szabodanika/szabodanika

Merge personal branch to master
This commit is contained in:
Dániel Szabó 2022-11-07 20:34:14 +02:00 committed by GitHub
commit 4362d934e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1381 additions and 865 deletions

4
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,4 @@
# These are supported funding model platforms
github: szabodanika
ko_fi: dani_sz

BIN
.github/index.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
.github/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

397
Cargo.lock generated
View file

@ -401,6 +401,12 @@ dependencies = [
"serde",
]
[[package]]
name = "bit_field"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -455,6 +461,18 @@ version = "3.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
[[package]]
name = "bytemuck"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.2.1"
@ -481,9 +499,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.0.74"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
dependencies = [
"jobserver",
]
@ -558,6 +576,12 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "convert_case"
version = "0.4.0"
@ -599,6 +623,55 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc"
dependencies = [
"cfg-if",
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348"
dependencies = [
"autocfg",
"cfg-if",
"crossbeam-utils",
"memoffset",
"scopeguard",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac"
dependencies = [
"cfg-if",
]
[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-common"
version = "0.1.6"
@ -676,6 +749,12 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "either"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[package]]
name = "encoding_rs"
version = "0.8.31"
@ -698,6 +777,21 @@ dependencies = [
"termcolor",
]
[[package]]
name = "exr"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eb5f255b5980bb0c8cf676b675d1a99be40f316881444f44e0462eaf5df5ded"
dependencies = [
"bit_field",
"flume",
"half",
"lebe",
"miniz_oxide 0.6.2",
"smallvec",
"threadpool",
]
[[package]]
name = "flate2"
version = "1.0.24"
@ -705,7 +799,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
dependencies = [
"crc32fast",
"miniz_oxide",
"miniz_oxide 0.5.4",
]
[[package]]
name = "flume"
version = "0.10.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
dependencies = [
"futures-core",
"futures-sink",
"nanorand",
"pin-project",
"spin",
]
[[package]]
@ -835,8 +942,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
name = "gif"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06"
dependencies = [
"color_quant",
"weezl",
]
[[package]]
@ -858,6 +977,15 @@ dependencies = [
"tracing",
]
[[package]]
name = "half"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad6a9459c9c30b177b925162351f97e7d967c7ea8bab3b8352805327daf45554"
dependencies = [
"crunchy",
]
[[package]]
name = "harsh"
version = "0.2.2"
@ -885,6 +1013,15 @@ dependencies = [
"libc",
]
[[package]]
name = "html-escape"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e7479fa1ef38eb49fb6a42c426be515df2d063f06cb8efd3e50af073dbc26c"
dependencies = [
"utf8-width",
]
[[package]]
name = "http"
version = "0.2.8"
@ -928,9 +1065,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "iana-time-zone"
version = "0.1.53"
version = "0.1.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
checksum = "f5a6ef98976b22b3b7f2f3a806f858cb862044cfa66805aa3ad84cb3d3b785ed"
dependencies = [
"android_system_properties",
"core-foundation-sys",
@ -960,6 +1097,25 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "image"
version = "0.24.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd8e4fb07cf672b1642304e731ef8a6a4c7891d67bb4fd4f5ce58cd6ed86803c"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"exr",
"gif",
"jpeg-decoder",
"num-rational",
"num-traits",
"png",
"scoped_threadpool",
"tiff",
]
[[package]]
name = "indexmap"
version = "1.9.1"
@ -985,6 +1141,15 @@ dependencies = [
"libc",
]
[[package]]
name = "jpeg-decoder"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9478aa10f73e7528198d75109c8be5cd7d15fb530238040148d5f9a22d4c5b3b"
dependencies = [
"rayon",
]
[[package]]
name = "js-sys"
version = "0.3.60"
@ -1006,6 +1171,12 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lebe"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "lexical-core"
version = "0.7.6"
@ -1101,9 +1272,18 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "memoffset"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
dependencies = [
"autocfg",
]
[[package]]
name = "microbin"
version = "1.1.0"
version = "1.2.0"
dependencies = [
"actix-files",
"actix-multipart",
@ -1120,7 +1300,10 @@ dependencies = [
"lazy_static",
"linkify",
"log",
"mime_guess",
"qrcode-generator",
"rand",
"rust-embed",
"sanitize-filename",
"serde",
"serde_json",
@ -1152,6 +1335,15 @@ dependencies = [
"adler",
]
[[package]]
name = "miniz_oxide"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
dependencies = [
"adler",
]
[[package]]
name = "mio"
version = "0.8.5"
@ -1164,6 +1356,15 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "nanorand"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
dependencies = [
"getrandom",
]
[[package]]
name = "nom"
version = "6.1.2"
@ -1187,6 +1388,17 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.15"
@ -1217,9 +1429,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.16.0"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]]
name = "onig"
@ -1245,9 +1457,9 @@ dependencies = [
[[package]]
name = "os_str_bytes"
version = "6.3.1"
version = "6.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9"
checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff"
[[package]]
name = "parking_lot"
@ -1284,6 +1496,26 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "pin-project"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pin-project-lite"
version = "0.2.9"
@ -1316,6 +1548,18 @@ dependencies = [
"xml-rs",
]
[[package]]
name = "png"
version = "0.17.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f0e7f4c94ec26ff209cee506314212639d6c91b80afb82984819fafce9df01c"
dependencies = [
"bitflags",
"crc32fast",
"flate2",
"miniz_oxide 0.5.4",
]
[[package]]
name = "ppv-lite86"
version = "0.2.16"
@ -1355,6 +1599,23 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "qrcode-generator"
version = "4.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501c33c9127afb867693646b38817bf63a9a756d4b66aefbc5c0d7e5e8c3125a"
dependencies = [
"html-escape",
"image",
"qrcodegen",
]
[[package]]
name = "qrcodegen"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142"
[[package]]
name = "quote"
version = "1.0.21"
@ -1400,6 +1661,30 @@ dependencies = [
"getrandom",
]
[[package]]
name = "rayon"
version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d"
dependencies = [
"autocfg",
"crossbeam-deque",
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-utils",
"num_cpus",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
@ -1426,6 +1711,40 @@ version = "0.6.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
[[package]]
name = "rust-embed"
version = "6.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "283ffe2f866869428c92e0d61c2f35dfb4355293cdfdc48f49e895c15f1333d1"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "6.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31ab23d42d71fb9be1b643fe6765d292c5e14d46912d13f3ae2815ca048ea04d"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "7.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1669d81dfabd1b5f8e2856b8bbe146c6192b0ba22162edc738ac0a5de18f054"
dependencies = [
"sha2",
"walkdir",
]
[[package]]
name = "rustc_version"
version = "0.4.0"
@ -1466,6 +1785,12 @@ dependencies = [
"regex",
]
[[package]]
name = "scoped_threadpool"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
[[package]]
name = "scopeguard"
version = "1.1.0"
@ -1538,6 +1863,17 @@ dependencies = [
"digest",
]
[[package]]
name = "sha2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.0"
@ -1572,6 +1908,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "spin"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09"
dependencies = [
"lock_api",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
@ -1659,6 +2004,26 @@ dependencies = [
"syn",
]
[[package]]
name = "threadpool"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
dependencies = [
"num_cpus",
]
[[package]]
name = "tiff"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7259662e32d1e219321eb309d5f9d898b779769d81b76e762c07c8e5d38fcb65"
dependencies = [
"flate2",
"jpeg-decoder",
"weezl",
]
[[package]]
name = "time"
version = "0.1.44"
@ -1845,6 +2210,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "utf8-width"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1"
[[package]]
name = "version_check"
version = "0.9.4"
@ -1928,6 +2299,12 @@ version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
[[package]]
name = "weezl"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
[[package]]
name = "winapi"
version = "0.3.9"

View file

@ -1,6 +1,6 @@
[package]
name = "microbin"
version = "1.1.0"
version = "1.2.0"
edition = "2021"
authors = ["Daniel Szabo <daniel.szabo99@outlook.com>"]
license = "BSD-3-Clause"
@ -31,6 +31,9 @@ env_logger = "0.9.0"
actix-web-httpauth = "0.6.0"
lazy_static = "1.4.0"
syntect = "5.0"
qrcode-generator = "4.1.6"
rust-embed = "6.4.2"
mime_guess = "2.0.4"
harsh = "0.2"
[profile.release]

365
README.MD
View file

@ -1,45 +1,47 @@
![Screenshot](git/index.png)
![Screenshot](.github/index.png)
# MicroBin
![Build](https://github.com/szabodanika/microbin/actions/workflows/rust.yml/badge.svg)
![crates.io](https://img.shields.io/crates/v/microbin.svg)
[![Docker Image](https://github.com/szabodanika/microbin/actions/workflows/docker.yml/badge.svg)](https://hub.docker.com/r/danielszabo99/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?
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/szabodanika/microbin)
Or install from Cargo:
Install from Cargo:
`cargo install microbin`
And run with your custom configuration:
`microbin --port 8080 --highlightsyntax --editable`
`microbin --port 8080 --public-path https://myserver.net --highlightsyntax --editable`
Or get the Docker image from [Dockerhub: danielszabo99/microbin](https://hub.docker.com/r/danielszabo99/microbin)
On our website [microbin.eu](microbin.eu) you will find the following:
- [Screenshots](https://microbin.eu/screenshots/)
- [Quickstart Guide](https://microbin.eu/quickstart/)
- [Documentation](https://microbin.eu/documentation/)
- [Donations and Sponsorhip](https://microbin.eu/donate/)
- [Community](https://microbin.eu/community/)
### Features
- Is very small
- Entirely self-contained executable, MicroBin is a single file!
- 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)
- Raw text serving (eg. server.com/raw/pig-dog-cat)
- URL shortening and redirection
- QR code support
- 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
- Private and public, editable and final, automatically and never expiring pastas
- Syntax highlighting
- Entirely self-contained executable, MicroBin is a single file!
- Automatic dark mode (follows system preferences)
- Very little CSS and absolutely no JS (see [water.css](https://github.com/kognise/water.css))
- Automatic dark mode and custom styling support with very little CSS and only vanilla 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?
In microbin, a pasta can be:
@ -60,334 +62,9 @@ You can use MicroBin
...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.
### License
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.
MicroBin and MicroBin.eu are available under the BSD 3-Clause License.
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
```
### Docker
The official automated docker images are available on [Docker Hub at danielszabo99/microbin](https://hub.docker.com/r/danielszabo99/microbin). Alternatively you can also build an image yourself, following the steps below:
```
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:/app/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`.
```
[Unit]
Description=MicroBin
After=network.target
[Service]
Type=simple
Restart=always
User=[username]
RootDirectory=/
WorkingDirectory=[path to installation directory]
ExecStart=[path to installation directory]/target/release/microbin
[Install]
WantedBy=multi-user.target
```
Here is my `microbin.service` for example, with some optional arguments:
```
[Unit]
Description=MicroBin
After=network.target
[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.
### --public-path [PUBLIC_PATH]
Add the given public path prefix to all urls.
This allows you to host MicroBin behind a reverse proxy on a subpath.
Note that MicroBin itself still expects all routes to be as without this option, and thus is unsuited
if you are running MicroBin directly.
#### Example Usage (caddy)
An example of running MicroBin behind the reverse proxy `caddy` on the path `/paste` (using systemd)
**microbin.service**
```unit file (systemd)
[Unit]
Description=Micobin Paste
[Service]
Type=simple
User=microbin
# Path to your binary
ExecStart=/home/microbin/microbin/target/release/microbin
# Set your desired working directory, eg where microbin places its data
WorkingDirectory=/home/pi/bin/micro
# bind to localhost, as its exposed via Caddy
Environment=MICROBIN_BIND=127.0.0.1
# bind to a unused port
Environment=MICROBIN_PORT=31333
# All your other config (change and add as needed)
Environment=MICROBIN_EDITABLE=true
Environment=MICROBIN_HIGHLIGHTSYNTAX=true
Environment=MICROBIN_THREADS=2
Environment=MICROBIN_FOOTER_TEXT="Bin the bytes"
# Set your **public** url. Eg the one you will later use to access microbin
# Ensure it either starts with http(s):// or with a /
Environment=MICROBIN_PUBLIC_PATH="http://100.127.233.32/paste"
[Install]
WantedBy=multi-user.target
```
**Caddyfile**
```caddy
example.com {
# Your normal http root
root * /var/www/html
file_server
# Route all requests to past
handle_path /paste/* {
reverse_proxy http://127.0.0.1:31333
}
}
```
### --no-file-upload
Disables and hides the file upload option in the UI.
© Dániel Szabó 2022

View file

@ -1,19 +1,12 @@
# Security Policy
## Supported Versions
## Version Support
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: |
Currently we only have capacity to support the latest version of MicroBin. We recommend that you always update to the newest one and check our pages regularly for announcements.
## Reporting a Vulnerability
Security vulnerabilities can be reported directly to
the developer/maintainer at d@szab.eu.
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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 791 KiB

View file

@ -63,12 +63,33 @@ pub struct Args {
#[clap(short, long, env = "MICROBIN_THREADS", default_value_t = 1)]
pub threads: u8,
#[clap(short, long, env = "MICROBIN_GC_DAYS", default_value_t = 90)]
pub gc_days: u16,
#[clap(long, env = "MICROBIN_ENABLE_BURN_AFTER")]
pub enable_burn_after: bool,
#[clap(short, long, env = "MICROBIN_DEFAULT_BURN_AFTER", default_value_t = 0)]
pub default_burn_after: u16,
#[clap(long, env = "MICROBIN_WIDE")]
pub wide: bool,
#[clap(long, env = "MICROBIN_QR")]
pub qr: bool,
#[clap(long, env = "MICROBIN_NO_ETERNAL_PASTA")]
pub no_eternal_pasta: bool,
#[clap(long, env = "MICROBIN_DEFAULT_EXPIRY", default_value = "24hour")]
pub default_expiry: String,
#[clap(short, long, env = "MICROBIN_NO_FILE_UPLOAD")]
pub no_file_upload: bool,
#[clap(long, env = "MICROBIN_CUSTOM_CSS")]
pub custom_css: Option<String>,
#[clap(long, env = "MICROBIN_HASH_IDS")]
pub hash_ids: bool,
}

View file

@ -55,6 +55,9 @@ pub async fn create(
private: false,
editable: false,
created: timenow,
read_count: 0,
burn_after_reads: 0,
last_read: timenow,
pasta_type: String::from(""),
expiration: 0,
};
@ -79,9 +82,34 @@ pub async fn create(
"1hour" => timenow + 60 * 60,
"24hour" => timenow + 60 * 60 * 24,
"1week" => timenow + 60 * 60 * 24 * 7,
"never" => 0,
"never" => {
if ARGS.no_eternal_pasta {
timenow + 60 * 60 * 24 * 7
} else {
0
}
}
_ => {
log::error!("{}", "Unexpected expiration time!");
timenow + 60 * 60 * 24 * 7
}
};
}
continue;
}
"burn_after" => {
while let Some(chunk) = field.try_next().await? {
new_pasta.burn_after_reads = match std::str::from_utf8(&chunk).unwrap() {
// give an extra read because the user will be redirected to the pasta page automatically
"1" => 2,
"10" => 10,
"100" => 100,
"1000" => 1000,
"10000" => 10000,
"0" => 0,
_ => {
log::error!("{}", "Unexpected burn after value!");
0
}
};
@ -90,8 +118,13 @@ pub async fn create(
continue;
}
"content" => {
let mut content = String::from("");
while let Some(chunk) = field.try_next().await? {
new_pasta.content = std::str::from_utf8(&chunk).unwrap().to_string();
content.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
}
if content.len() > 0 {
new_pasta.content = content;
new_pasta.pasta_type = if is_valid_url(new_pasta.content.as_str()) {
String::from("url")
} else {
@ -151,7 +184,9 @@ pub async fn create(
new_pasta.file = Some(file);
new_pasta.pasta_type = String::from("text");
}
_ => {}
field => {
log::error!("Unexpected multipart field: {}", field);
}
}
}

View file

@ -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(),
)
}

42
src/endpoints/info.rs Normal file
View file

@ -0,0 +1,42 @@
use crate::args::{Args, ARGS};
use crate::pasta::Pasta;
use crate::AppState;
use actix_web::{get, web, HttpResponse};
use askama::Template;
#[derive(Template)]
#[template(path = "info.html")]
struct Info<'a> {
args: &'a Args,
pastas: &'a Vec<Pasta>,
status: &'a String,
version_string: &'a String,
message: &'a String,
}
#[get("/info")]
pub async fn info(data: web::Data<AppState>) -> HttpResponse {
// get access to the pasta collection
let mut pastas = data.pastas.lock().unwrap();
// todo status report more sophisticated
let mut status = "OK";
let mut message = "";
if ARGS.public_path.to_string() == "" {
status = "WARNING";
message = "Warning: No public URL set with --public-path parameter. QR code and URL Copying functions have been disabled"
}
HttpResponse::Ok().content_type("text/html").body(
Info {
args: &ARGS,
pastas: &pastas,
status: &String::from(status),
version_string: &String::from("1.2.0-20221107"),
message: &String::from(message),
}
.render()
.unwrap(),
)
}

View file

@ -1,13 +1,15 @@
use actix_web::{get, web, HttpResponse};
use askama::Template;
use crate::args::{Args, ARGS};
use crate::dbio::save_to_file;
use crate::endpoints::errors::ErrorTemplate;
use crate::pasta::Pasta;
use crate::util::animalnumbers::to_u64;
use crate::util::hashids::to_u64 as hashid_to_u64;
use crate::util::misc::remove_expired;
use crate::AppState;
use actix_web::rt::time;
use actix_web::{get, web, HttpResponse};
use askama::Template;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Template)]
#[template(path = "pasta.html", escape = "none")]
@ -18,33 +20,63 @@ struct PastaTemplate<'a> {
#[get("/pasta/{id}")]
pub async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
// get access to the pasta collection
let mut pastas = data.pastas.lock().unwrap();
println!("{}", id);
let id = if ARGS.hash_ids {
hashid_to_u64(&*id).unwrap_or(0)
} else {
to_u64(&*id.into_inner()).unwrap_or(0)
};
println!("{}", id);
// remove expired pastas (including this one if needed)
remove_expired(&mut pastas);
for pasta in pastas.iter() {
// find the index of the pasta in the collection based on u64 id
let mut index: usize = 0;
let mut found: bool = false;
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
return HttpResponse::Ok().content_type("text/html").body(
index = i;
found = true;
break;
}
}
if found {
// increment read count
pastas[index].read_count = pastas[index].read_count + 1;
// save the updated read count
save_to_file(&pastas);
// serve pasta in template
let response = HttpResponse::Ok().content_type("text/html").body(
PastaTemplate {
pasta: &pasta,
pasta: &pastas[index],
args: &ARGS,
}
.render()
.unwrap(),
);
// get current unix time in seconds
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;
// update last read time
pastas[index].last_read = timenow;
return response;
}
// otherwise
// send pasta not found error
HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
@ -52,6 +84,7 @@ pub async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpR
#[get("/url/{id}")]
pub async fn redirecturl(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
// get access to the pasta collection
let mut pastas = data.pastas.lock().unwrap();
let id = if ARGS.hash_ids {
@ -60,22 +93,57 @@ pub async fn redirecturl(data: web::Data<AppState>, id: web::Path<String>) -> Ht
to_u64(&*id.into_inner()).unwrap_or(0)
};
// remove expired pastas (including this one if needed)
remove_expired(&mut pastas);
for pasta in pastas.iter() {
// find the index of the pasta in the collection based on u64 id
let mut index: usize = 0;
let mut found: bool = false;
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
if pasta.pasta_type == "url" {
return HttpResponse::Found()
.append_header(("Location", String::from(&pasta.content)))
index = i;
found = true;
break;
}
}
if found {
// increment read count
pastas[index].read_count = pastas[index].read_count + 1;
// save the updated read count
save_to_file(&pastas);
// send redirect if it's a url pasta
if pastas[index].pasta_type == "url" {
let response = HttpResponse::Found()
.append_header(("Location", String::from(&pastas[index].content)))
.finish();
// get current unix time in seconds
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;
// update last read time
pastas[index].last_read = timenow;
return response;
// send error if we're trying to open a non-url pasta as a redirect
} else {
return HttpResponse::Ok()
HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap());
}
}
}
// otherwise
// send pasta not found error
HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
@ -83,6 +151,7 @@ pub async fn redirecturl(data: web::Data<AppState>, id: web::Path<String>) -> Ht
#[get("/raw/{id}")]
pub async fn getrawpasta(data: web::Data<AppState>, id: web::Path<String>) -> String {
// get access to the pasta collection
let mut pastas = data.pastas.lock().unwrap();
let id = if ARGS.hash_ids {
@ -91,13 +160,44 @@ pub async fn getrawpasta(data: web::Data<AppState>, id: web::Path<String>) -> St
to_u64(&*id.into_inner()).unwrap_or(0)
};
// remove expired pastas (including this one if needed)
remove_expired(&mut pastas);
for pasta in pastas.iter() {
// find the index of the pasta in the collection based on u64 id
let mut index: usize = 0;
let mut found: bool = false;
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
return pasta.content.to_owned();
index = i;
found = true;
break;
}
}
if found {
// increment read count
pastas[index].read_count = pastas[index].read_count + 1;
// get current unix time in seconds
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;
// update last read time
pastas[index].last_read = timenow;
// save the updated read count
save_to_file(&pastas);
// send raw content of pasta
return pastas[index].content.to_owned();
}
// otherwise
// send pasta not found error as raw text
String::from("Pasta not found! :-(")
}

70
src/endpoints/qr.rs Normal file
View file

@ -0,0 +1,70 @@
use crate::args::{Args, ARGS};
use crate::endpoints::errors::ErrorTemplate;
use crate::pasta::Pasta;
use crate::util::animalnumbers::to_u64;
use crate::util::hashids::to_u64 as hashid_to_u64;
use crate::util::misc::{self, remove_expired};
use crate::AppState;
use actix_web::{get, web, HttpResponse};
use askama::Template;
use qrcode_generator::QrCodeEcc;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Template)]
#[template(path = "qr.html", escape = "none")]
struct QRTemplate<'a> {
qr: &'a String,
pasta: &'a Pasta,
args: &'a Args,
}
#[get("/qr/{id}")]
pub async fn getqr(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
// get access to the pasta collection
let mut pastas = data.pastas.lock().unwrap();
let u64_id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
to_u64(&id).unwrap_or(0)
};
// remove expired pastas (including this one if needed)
remove_expired(&mut pastas);
// find the index of the pasta in the collection based on u64 id
let mut index: usize = 0;
let mut found: bool = false;
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == u64_id {
index = i;
found = true;
break;
}
}
if found {
// generate the QR code as an SVG - if its a file or text pastas, this will point to the /pasta endpoint, otherwise to the /url endpoint, essentially directly taking the user to the url stored in the pasta
let svg: String = match pastas[index].pasta_type.as_str() {
"url" => misc::string_to_qr_svg(format!("{}/url/{}", &ARGS.public_path, &id).as_str()),
_ => misc::string_to_qr_svg(format!("{}/pasta/{}", &ARGS.public_path, &id).as_str()),
};
// serve qr code in template
return HttpResponse::Ok().content_type("text/html").body(
QRTemplate {
qr: &svg,
pasta: &pastas[index],
args: &ARGS,
}
.render()
.unwrap(),
);
}
// otherwise
// send pasta not found error
HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}

View file

@ -1,36 +1,63 @@
use actix_web::{get, web, HttpResponse};
use askama::Template;
use std::marker::PhantomData;
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use mime_guess::from_path;
use rust_embed::RustEmbed;
#[derive(Template)]
#[template(path = "water.css", escape = "none")]
struct WaterCSS<'a> {
_marker: PhantomData<&'a ()>,
#[derive(RustEmbed)]
#[folder = "templates/assets/"]
struct Asset;
fn handle_embedded_file(path: &str) -> HttpResponse {
match Asset::get(path) {
Some(content) => HttpResponse::Ok()
.content_type(from_path(path).first_or_octet_stream().as_ref())
.body(content.data.into_owned()),
None => HttpResponse::NotFound().body("404 Not Found"),
}
}
#[derive(Template)]
#[template(path = "favicon.svg", escape = "none")]
struct Favicon<'a> {
_marker: PhantomData<&'a ()>,
#[actix_web::get("/static/{_:.*}")]
async fn static_resources(path: web::Path<String>) -> impl Responder {
handle_embedded_file(path.as_str())
}
#[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(),
),
"favicon.svg" => HttpResponse::Ok().content_type("image/svg+xml").body(
Favicon {
_marker: Default::default(),
}
.render()
.unwrap(),
),
_ => HttpResponse::NotFound().content_type("text/html").finish(),
}
}
// #[derive(Template)]
// #[template(path = "water.css", escape = "none")]
// struct WaterCSS<'a> {
// _marker: PhantomData<&'a ()>,
// }
// // #[derive(Template)]
// // #[template(path = "logo.png", escape = "none")]
// struct LogoPNG<'a> {
// _marker: PhantomData<&'a ()>,
// }
// #[derive(Template)]
// #[template(path = "favicon.svg", escape = "none")]
// struct Favicon<'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(),
// ),
// "logo.png" => HttpResponse::Ok()
// .content_type("image/png")
// .body(Ok(EmbedFile::open("templates/logo.png")?)),
// "favicon.svg" => HttpResponse::Ok().content_type("image/svg+xml").body(
// Favicon {
// _marker: Default::default(),
// }
// .render()
// .unwrap(),
// ),
// _ => HttpResponse::NotFound().content_type("text/html").finish(),
// }
// }

View file

@ -2,7 +2,7 @@ extern crate core;
use crate::args::ARGS;
use crate::endpoints::{
create, edit, errors, help, pasta as pasta_endpoint, pastalist, remove, static_resources,
create, edit, errors, info, pasta as pasta_endpoint, pastalist, qr, remove, static_resources,
};
use crate::pasta::Pasta;
use crate::util::dbio;
@ -32,9 +32,10 @@ pub mod endpoints {
pub mod create;
pub mod edit;
pub mod errors;
pub mod help;
pub mod info;
pub mod pasta;
pub mod pastalist;
pub mod qr;
pub mod remove;
pub mod static_resources;
}
@ -87,13 +88,14 @@ async fn main() -> std::io::Result<()> {
.app_data(data.clone())
.wrap(middleware::NormalizePath::trim())
.service(create::index)
.service(help::help)
.service(info::info)
.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(qr::getqr)
.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))

View file

@ -3,6 +3,7 @@ use chrono::{Datelike, Local, TimeZone, Timelike};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::args::ARGS;
use crate::util::animalnumbers::to_animal_names;
@ -41,6 +42,9 @@ pub struct Pasta {
pub editable: bool,
pub created: i64,
pub expiration: i64,
pub last_read: i64,
pub read_count: u64,
pub burn_after_reads: u64,
pub pasta_type: String,
}
@ -79,6 +83,58 @@ impl Pasta {
}
}
pub fn last_read_time_ago_as_string(&self) -> String {
// get current unix time in seconds
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;
// get seconds since last read and convert it to days
let days = ((timenow - self.last_read) / 86400) as u16;
if days > 1 {
return format!("{} days ago", days);
};
// it's less than 1 day, let's do hours then
let hours = ((timenow - self.last_read) / 3600) as u16;
if hours > 1 {
return format!("{} hours ago", hours);
};
// it's less than 1 hour, let's do minutes then
let minutes = ((timenow - self.last_read) / 60) as u16;
if minutes > 1 {
return format!("{} minutes ago", minutes);
};
// it's less than 1 minute, let's do seconds then
let seconds = (timenow - self.last_read) as u16;
if seconds > 1 {
return format!("{} seconds ago", seconds);
};
// it's less than 1 second?????
return String::from("just now");
}
pub fn last_read_days_ago(&self) -> u16 {
// get current unix time in seconds
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;
// get seconds since last read and convert it to days
return ((timenow - self.last_read) / 86400) as u16;
}
pub fn content_syntax_highlighted(&self) -> String {
html_highlight(&self.content, &self.extension)
}

View file

@ -1,6 +1,8 @@
use std::time::{SystemTime, UNIX_EPOCH};
use crate::args::ARGS;
use linkify::{LinkFinder, LinkKind};
use qrcode_generator::QrCodeEcc;
use std::fs;
use crate::{dbio, Pasta};
@ -16,8 +18,16 @@ pub fn remove_expired(pastas: &mut Vec<Pasta>) {
} as i64;
pastas.retain(|p| {
// keep if:
// expiration is `never` or not reached
if p.expiration == 0 || p.expiration > timenow {
// AND
// read count is less than burn limit, or no limit set
// AND
// has been read in the last N days where N is the arg --gc-days OR N is 0 (no GC)
if (p.expiration == 0 || p.expiration > timenow)
&& (p.read_count < p.burn_after_reads || p.burn_after_reads == 0)
&& (p.last_read_days_ago() < ARGS.gc_days || ARGS.gc_days == 0)
{
// keep
true
} else {
@ -45,6 +55,10 @@ pub fn remove_expired(pastas: &mut Vec<Pasta>) {
dbio::save_to_file(pastas);
}
pub fn string_to_qr_svg(str: &str) -> String {
qrcode_generator::to_svg_to_string(str, QrCodeEcc::Low, 256, None::<&str>).unwrap()
}
pub fn is_valid_url(url: &str) -> bool {
let finder = LinkFinder::new();
let spans: Vec<_> = finder.spans(url).collect();

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

BIN
templates/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -1,10 +1,9 @@
{% if !args.hide_footer %}
<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>!
<a href="https://microbin.eu">MicroBin</a> by Dániel Szabó and the FOSS Community.
Let's keep the Web <b>compact</b>, <b>accessible</b> and <b>humane</b>!
{%- else %}
{{ args.footer_text.as_ref().unwrap() }}
@ -14,4 +13,5 @@
{%- endif %}
</body>
</html>

View file

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html>
<head>
{% if args.title.as_ref().is_none() %}
<title>MicroBin</title>
@ -9,14 +10,20 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="icon" type="image/svg+xml" href="{{ args.public_path }}/static/favicon.svg">
{% if !args.pure_html %}
{% if args.custom_css.as_ref().is_none() %}
<link rel="stylesheet" href="{{ args.public_path }}/static/water.css">
{%- else %}
<link rel="stylesheet" href="{{ args.custom_css.as_ref().unwrap() }}">
{%- endif %}
{%- endif %}
</head>
{% if args.wide %}
<body style="
max-width: 1080;
max-width: 1080px;
margin: auto;
padding-left:0.5rem;
padding-right:0.5rem;
@ -24,8 +31,9 @@
font-size: 1.1em;
">
{%- else %}
<body style="
max-width: 720;
max-width: 800px;
margin: auto;
padding-left:0.5rem;
padding-right:0.5rem;
@ -40,21 +48,19 @@
<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> -->
<img width=26 style="margin-bottom: -6px; margin-right: 0.5rem;"
src="{{ args.public_path }}/static/logo.png">
{%- endif %} {% if args.title.as_ref().is_none() %}
MicroBin {%- else %} {{ args.title.as_ref().unwrap() }} {%- endif %}
</b>
<a href="{{ args.public_path }}/" style="margin-right: 0.5rem; margin-left: 0.5rem">New Pasta</a>
<a href="{{ args.public_path }}/" style="margin-right: 0.5rem; margin-left: 0.5rem">New
</a>
<a href="{{ args.public_path }}/pastalist" style="margin-right: 0.5rem; margin-left: 0.5rem">Pasta List</a>
<a href="{{ args.public_path }}/pastalist" style="margin-right: 0.5rem; margin-left: 0.5rem">List</a>
<a href="{{ args.public_path }}/help" style="margin-right: 0.5rem; margin-left: 0.5rem">Help</a>
<a href="{{ args.public_path }}/info" style="margin-right: 0.5rem; margin-left: 0.5rem">Info</a>
<hr>

View file

@ -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 &quot;pasta&quot; 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 &quot;postbox&quot; 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 &quot;expiration=10min&amp;content=This is a test pasta&quot; -X POST https://microbin.myserver.com/create</code></p>
<p>File contents: <code>curl -d &quot;expiration=10min&amp;content=$( &lt; mypastafile.txt )&quot; -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 &gt; 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&#39;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&#39;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&#39;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 &quot;MicroBin&quot; 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&#39;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" %}

View file

@ -1,26 +1,108 @@
{% include "header.html" %}
<form action="upload" method="POST" enctype="multipart/form-data">
<form id="pasta-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 id="settings">
<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 label="Expire after">
{% if args.default_expiry == "1min" %}
<option selected value="1min">
{%- else %}
<option value="1min">
{%- endif %}
1 minute
</option>
{% if args.default_expiry == "10min" %}
<option selected value="10min">
{%- else %}
<option value="10min">
{%- endif %}
10 minutes
</option>
{% if args.default_expiry == "1hour" %}
<option selected value="1hour">
{%- else %}
<option value="1hour">
{%- endif %}
1 hour
</option>
{% if args.default_expiry == "24hour" %}
<option selected value="24hour">
{%- else %}
<option value="24hour">
{%- endif %}
24 hours
</option>
{% if args.default_expiry == "1week" %}
<option selected value="1week">
{%- else %}
<option value="1week">
{%- endif %}
1 week
</option>
</optgroup>
{% if !args.no_eternal_pasta %}
<option value="never">Never Expire</option>
{%- endif %}
</select>
</div>
{% if args.enable_burn_after %}
<div>
<label for="expiration">Burn After</label><br>
<select style="width: 100%;" name="burn_after" id="burn_after">
<optgroup label="Burn after">
{% if args.default_burn_after == 1 %}
<option selected value="1">
{%- else %}
<option value="1">
{%- endif %}
First Read
</option>
{% if args.default_burn_after == 10 %}
<option selected value="10">
{%- else %}
<option value="10">
{%- endif %}
10th Read
</option>
{% if args.default_burn_after == 100 %}
<option selected value="100">
{%- else %}
<option value="100">
{%- endif %}
100th Read
</option>
{% if args.default_burn_after == 1000 %}
<option selected value="1000">
{%- else %}
<option value="1000">
{%- endif %}
1000th Read
</option>
{% if args.default_burn_after == 10000 %}
<option selected value="10000">
{%- else %}
<option value="10000">
{%- endif %}
10000th Read
</option>
</optgroup>
{% if args.default_burn_after == 0 %}
<option selected value="0">
{%- else %}
<option value="0">
{%- endif %}
No Limit
</option>
</select>
</div>
{%- endif %}
{% if args.highlightsyntax %}
<div>
<label for="syntax-highlight">Syntax Highlighting</label><br>
<label for="syntax-highlight">Syntax</label><br>
<select style="width: 100%;" name="syntax-highlight" id="syntax-highlight">
<option value="none">None</option>
<optgroup label="Source Code">
@ -59,22 +141,10 @@
<input type="hidden" name="syntax-highlight" value="none">
{%- endif %}
{% if !args.no_file_upload %}
<div>
<label>File attachment</label>
<br>
<input style="width: 100%;" type="file" id="file" name="file">
</div>
{% endif %}
</div>
<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 || args.private %}
<label>Other</label>
{%- endif %}
{% if args.editable %}
<div>
<input type="checkbox" id="editable" name="editable" value="editable">
@ -88,14 +158,85 @@
</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>
</div>
<label>Content</label>
<textarea style="width: 100%; min-height: 100px; margin-bottom: 2em" name="content" autofocus></textarea>
<div style="overflow:auto;">
{% if !args.no_file_upload %}
<div style="float: left">
<label for="file" id="attach-file-button-label"><a role="button" id="attach-file-button">Select or drop
file attachment</a></label>
<br>
<input type="file" id="file" name="file" />
</div>
{% endif %}
{% if args.readonly %}
<b>
<input style="width: 140px; float: right; background-color: #0076d18f;" disabled type="submit"
value="Read Only" /></b>
{%- else %}
<b>
<input style="width: 140px; float: right; background-color: #0076d18f;" type="submit" value="Save" />
</b>
{%- endif %}
</div>
</form>
<script>
const hiddenFileButton = document.getElementById('file');
const attachFileButton = document.getElementById('attach-file-button');
const dropContainer = document.getElementById('pasta-form');
hiddenFileButton.addEventListener('change', function () {
attachFileButton.textContent = "Attached: " + this.files[0].name;
});
dropContainer.ondragover = dropContainer.ondragenter = function (evt) {
evt.preventDefault();
if (hiddenFileButton.files.length == 0) {
attachFileButton.textContent = "Drop your file here";
} else {
attachFileButton.textContent = "Drop your file here to replace " + hiddenFileButton.files[0].name;
}
};
dropContainer.ondrop = function (evt) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(evt.dataTransfer.files[0]);
hiddenFileButton.files = dataTransfer.files;
attachFileButton.textContent = "Attached: " + hiddenFileButton.files[0].name;
evt.preventDefault();
};
</script>
<style>
input::file-selector-button {
display: none;
}
#settings {
display: grid;
grid-gap: 6px;
grid-template-columns: repeat(auto-fit, 150px);
grid-template-rows: repeat(1, 90px);
margin-bottom: 0.5rem;
}
select {
height: 3rem;
}
#attach-file-button-label {
padding-top: 1rem;
padding-bottom: 1rem;
cursor: pointer;
}
#file {
display: none;
}
</style>
{% include "footer.html" %}

42
templates/info.html Normal file
View file

@ -0,0 +1,42 @@
{% include "header.html" %}
<h2>Welcome to MicroBin</h2>
<div style="height: 200px;">
<div style="float: left">
<h4>Links</h4>
<a href="https://microbin.eu/documentation" style="margin-right: 1rem">Documentation and Help</a>
<br>
<a href="https://github.com/szabodanika/microbin" style="margin-right: 1rem">Source Code</a>
<br>
<a href="https://github.com/szabodanika/microbin/issues" style="margin-right: 1rem">Feedback</a>
<br>
<a href="https://microbin.eu/donate">Donate and Sponsor</a>
</div>
<div style="float: right">
<h4>Info</h4>
<table style="width: 400px">
<tr>
<td><b>Version</b></td>
<td>{{version_string}} </td>
</tr>
<tr>
<td><b>Status</b></td>
<td>{{status}} </td>
</tr>
<tr>
<td><b>Pastas</b></td>
<td>{{pastas.len()}} </td>
</tr>
</table>
</div>
</div>
{% if message != "" %}
<h4>Messages</h4>
<p>{{message}}</p>
{%- endif %}
<br>
{% include "footer.html" %}

View file

@ -1,25 +1,45 @@
{% include "header.html" %}
<div style="float: left">
<a style="margin-right: 0.5rem" href="{{ args.public_path }}/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="{{ args.public_path }}/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>
{% if pasta.content != "No Text Content" %}
<button id="copy-text-button" class="copy-button" style="margin-right: 0.5rem">
Copy Text
</button>
{% if args.public_path.to_string() != "" && pasta.pasta_type == "url" %}
<button id="copy-redirect-button" class="copy-button" style="margin-right: 0.5rem">
Copy Redirect
</button>
{%- endif %}
<a style="margin-right: 1rem" href="{{ args.public_path }}/raw/{{pasta.id_as_animals()}}">Raw Text
Content</a>
{%- endif %}
{% if args.qr && args.public_path.to_string() != "" %}
<a style="margin-right: 1rem" href="{{ args.public_path }}/qr/{{pasta.id_as_animals()}}">QR</a>
{%- endif %}
{% if pasta.editable %}
<a style="margin-right: 0.5rem; margin-left: 0.5rem" href="{{ args.public_path }}/edit/{{pasta.id_as_animals()}}">Edit</a>
<a style="margin-right: 1rem" href="{{ args.public_path }}/edit/{{pasta.id_as_animals()}}">Edit</a>
{%- endif %}
<a style="margin-right: 0.5rem; margin-left: 0.5rem" href="{{ args.public_path }}/remove/{{pasta.id_as_animals()}}">Remove</a>
<a style="margin-right: 1rem" href="{{ args.public_path }}/remove/{{pasta.id_as_animals()}}">Remove</a>
</div>
<div style="float: right">
<a href="{{ args.public_path }}/pasta/{{pasta.id_as_animals()}}"><i>{{pasta.id_as_animals()}}</i></a>
<a style="margin-right: 0.5rem"
href="{{ args.public_path }}/pasta/{{pasta.id_as_animals()}}"><i>{{pasta.id_as_animals()}}</i></a>
{% if args.public_path.to_string() != "" %}
<button id="copy-url-button" class="copy-button" style="margin-right: 0">
Copy URL
</button>
{%- endif %}
</div>
{% if pasta.file.is_some() %}
<br>
<div class="code-container">
<a role="button" id="copy-button" class="copy-button">
Copy
<br>
<a href="{{ args.public_path }}/file/{{pasta.id_as_animals()}}/{{pasta.file.as_ref().unwrap().name()}}" download>
Download attached file: '{{pasta.file.as_ref().unwrap().name()}}' [{{pasta.file.as_ref().unwrap().size}}]
</a>
{%- endif %}
<br>
<br>
{% if pasta.content != "No Text Content" %}
<div class="code-container">
<div style="clear: both;">
{% if args.highlightsyntax %}
<pre><code id="code">{{pasta.content_syntax_highlighted()}}</code></pre>
@ -28,16 +48,51 @@
{%- endif %}
</div>
</div>
{%- endif %}
<div>
{% if pasta.read_count == 1 %}
<p style="font-size: small">Read {{pasta.read_count}} time, last {{pasta.last_read_time_ago_as_string()}}</p>
{%- else %}
<p style="font-size: small">Read {{pasta.read_count}} times, last {{pasta.last_read_time_ago_as_string()}}</p>
{%- endif %}
</div>
<br>
<script>
const btn = document.getElementById("copy-button")
const copyURLBtn = document.getElementById("copy-url-button")
const copyTextBtn = document.getElementById("copy-text-button")
const copyRedirectBtn = document.getElementById("copy-redirect-button")
const content = `{{ pasta.content_escaped() }}`
const url = `{{ args.public_path }}/pasta/{{pasta.id_as_animals()}}`
const redirect_url = `{{ args.public_path }}/url/{{pasta.id_as_animals()}}`
btn.addEventListener("click", () => {
navigator.clipboard.writeText(content)
btn.innerHTML = "Copied"
copyURLBtn.addEventListener("click", () => {
navigator.clipboard.writeText(url)
copyURLBtn.innerHTML = "Copied"
setTimeout(() => {
btn.innerHTML = "Copy"
copyURLBtn.innerHTML = "Copy URL"
}, 1000)
})
// it will be undefined when the element does not exist on non-url pastas
if (copyRedirectBtn) {
copyRedirectBtn.addEventListener("click", () => {
navigator.clipboard.writeText(redirect_url)
copyRedirectBtn.innerHTML = "Copied"
setTimeout(() => {
copyRedirectBtn.innerHTML = "Copy Redirect"
}, 1000)
})
}
copyTextBtn.addEventListener("click", () => {
navigator.clipboard.writeText(content)
copyTextBtn.innerHTML = "Copied"
setTimeout(() => {
copyTextBtn.innerHTML = "Copy Text"
}, 1000)
})
@ -54,7 +109,6 @@ code-line {
code-line::before {
content: counter(listing);
display: inline-block;
float: left;
padding-left: auto;
margin-left: auto;
text-align: left;
@ -68,6 +122,10 @@ code-line::before {
user-select: none;
}
#code {
min-height: 2rem;
}
.code-container {
position: relative;
}
@ -77,15 +135,11 @@ code-line::before {
}
.copy-button {
position: absolute;
background: transparent;
top: 0;
right: 0;
padding: 3px;
margin: 3px;
cursor: pointer;
font-size: small;
padding: 4px;
padding-left: 0.8rem;
padding-right: 0.8rem;
}
</style>
{% include "footer.html" %}

View file

@ -8,30 +8,24 @@
</p>
<br>
{%- else %}
<br>
<h3>Pastas</h3>
{% if args.pure_html %}
<table border="1" style="width: 100%">
<table border="1" style="width: 100%; white-space: nowrap;">
{% else %}
<table style="width: 100%">
{% endif %}
<thead>
<tr>
<th colspan="4">Pastas</th>
</tr>
<tr>
<th>
<th style="width: 30%">
Key
</th>
<th>
<th style="width: 20%">
Created
</th>
<th>
<th style="width: 20%">
Expiration
</th>
<th>
<th style="width: 30%">
</th>
</tr>
</thead>
<tbody>
{% for pasta in pastas %}
@ -49,7 +43,8 @@
<td>
<a style="margin-right:1rem" href="{{ args.public_path }}/raw/{{pasta.id_as_animals()}}">Raw</a>
{% if pasta.file.is_some() %}
<a style="margin-right:1rem" href="{{ args.public_path }}/file/{{pasta.id_as_animals()}}/{{pasta.file.as_ref().unwrap().name()}}">File</a>
<a style="margin-right:1rem"
href="{{ args.public_path }}/file/{{pasta.id_as_animals()}}/{{pasta.file.as_ref().unwrap().name()}}">File</a>
{%- endif %}
{% if pasta.editable %}
<a style="margin-right:1rem" href="{{ args.public_path }}/edit/{{pasta.id_as_animals()}}">Edit</a>
@ -62,35 +57,30 @@
</tbody>
</table>
<br>
<h3>URL Redirects</h3>
{% if args.pure_html %}
<table border="1" style="width: 100%">
{% else %}
<table style="width: 100%">
{% endif %}
<thead>
<tr>
<th colspan="4">URL Redirects</th>
</tr>
<tr>
<th>
<th style="width: 30%">
Key
</th>
<th>
<th style="width: 20%">
Created
</th>
<th>
<th style="width: 20%">
Expiration
</th>
<th>
<th style="width: 30%">
</th>
</tr>
</thead>
{% for pasta in pastas %}
{% if pasta.pasta_type == "url" && !pasta.private %}
<tr>
<td>
<a href="{{ args.public_path }}/url/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
<a href="{{ args.public_path }}/pasta/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
</td>
<td>
{{pasta.created_as_string()}}
@ -99,7 +89,9 @@
{{pasta.expiration_as_string()}}
</td>
<td>
<a style="margin-right:1rem" href="{{ args.public_path }}raw/{{pasta.id_as_animals()}}">Raw</a>
<a style="margin-right:1rem" href="{{ args.public_path }}/url/{{pasta.id_as_animals()}}">Open</a>
<a style="margin-right:1rem; cursor: pointer;" id="copy-button"
data-url="{{ args.public_path }}/url/{{pasta.id_as_animals()}}">Copy</a>
{% if pasta.editable %}
<a style="margin-right:1rem" href="{{ args.public_path }}/edit/{{pasta.id_as_animals()}}">Edit</a>
{%- endif %}
@ -112,4 +104,18 @@
</table>
<br>
{%- endif %}
<script>
// const btn = document.getElementById("copy-button")
const btn = document.querySelector("#copy-button");
btn.addEventListener("click", () => {
navigator.clipboard.writeText(btn.dataset.url)
btn.innerHTML = "Copied"
setTimeout(() => {
btn.innerHTML = "Copy"
}, 1000)
})
</script>
{% include "footer.html" %}

29
templates/qr.html Normal file
View file

@ -0,0 +1,29 @@
{% include "header.html" %}
<div style="float: left">
<a href="{{ args.public_path }}/pasta/{{pasta.id_as_animals()}}">Back to Pasta</a>
</div>
<div style="text-align: center; padding: 3rem;">
{% if pasta.pasta_type == "url" %}
<a href="{{ args.public_path }}/url/{{pasta.id_as_animals()}}">
{{qr}}
</a>
{% else %}
<a href="{{ args.public_path }}/pasta/{{pasta.id_as_animals()}}">
{{qr}}
</a>
{% endif %}
</div>
<style>
.copy-text-button,
.copy-url-button {
font-size: small;
padding: 4px;
width: 6rem;
}
</style>
{% include "footer.html" %}