Compare commits
161 commits
plugin-sup
...
master
Author | SHA1 | Date | |
---|---|---|---|
68996acd54 | |||
768c8c60b8 | |||
5002f11bf3 | |||
808ffd4964 | |||
1f69a77a03 | |||
f3ca7e3328 | |||
42aceb2a01 | |||
1652a850b8 | |||
181ebb3a63 | |||
c83a775ac2 | |||
e3a5527a8c | |||
5d0f73a5f3 | |||
c5b0a8ef79 | |||
75755052c3 | |||
01eb19e732 | |||
91568a1590 | |||
8039fe75a2 | |||
86e41865bb | |||
b313b5ce73 | |||
f1de42e5c0 | |||
e4575d7d6e | |||
d1c583d4b0 | |||
8623bcb9ae | |||
353cb5dfde | |||
1f2589976d | |||
3f2bdfdaed | |||
27fe305640 | |||
1ebbe5d922 | |||
51751e3ee2 | |||
f352129c78 | |||
8db35b80d9 | |||
56332c61f3 | |||
fa31a0a51a | |||
1069d4c676 | |||
7bfebb27d9 | |||
7abb3c5d11 | |||
7d5c70ddd6 | |||
528a7b6899 | |||
fa67edc8c5 | |||
39881a036a | |||
57fd472eda | |||
|
84136f1106 | ||
|
ba784da74e | ||
|
0a80ac1359 | ||
|
6564499c98 | ||
|
4fa2cc2a53 | ||
|
4279653ca9 | ||
|
53326b0435 | ||
|
68f4081745 | ||
|
089bb95c4f | ||
|
5f05206891 | ||
|
4fcd4e9e19 | ||
|
89f902f99f | ||
|
958466818b | ||
|
f41c2eb66b | ||
|
76cfc906ef | ||
|
66f6e0e46f | ||
|
4362d934e3 | ||
|
d7f0f9637d | ||
|
edd46eae58 | ||
|
c6e2b026e6 | ||
|
5854572e87 | ||
|
5e1fcff979 | ||
|
ca1cd91635 | ||
|
dac9ae8385 | ||
|
719691dfac | ||
|
b9a6717ba0 | ||
|
5e2513eb1f | ||
|
7522d41919 | ||
|
c6e5c6f018 | ||
|
6a0c6b736d | ||
|
2198cbdff9 | ||
|
b5da40fbdc | ||
|
44b55ae08e | ||
|
769901c895 | ||
|
d2e7234d96 | ||
|
e258bcc2bd | ||
|
dc2c7094a8 | ||
|
43061699f5 | ||
|
4980d72df2 | ||
|
2e981b3128 | ||
|
3e58ba325a | ||
|
b1ccb43855 | ||
|
fd8a66bcbc | ||
|
e031ea0e95 | ||
|
ec4d764f5a | ||
|
e8e21d561e | ||
|
487de0fcf7 | ||
|
7f4f784f2b | ||
|
52d87652ea | ||
|
bc26fe87a5 | ||
|
4926c578ec | ||
|
cfd494f80d | ||
|
a404f9a997 | ||
|
e786fa5a22 | ||
|
c39b778234 | ||
|
0b5dea5dd1 | ||
|
82c30ce6cd | ||
|
fc3998243b | ||
|
e17b26994f | ||
|
ef5d07392b | ||
|
cc504f781e | ||
|
1e8b17bb89 | ||
|
8223dd4973 | ||
|
6cea6262b8 | ||
|
3ca89291dc | ||
|
2322c6713e | ||
|
05ad1d46c1 | ||
|
d36472bcac | ||
|
ff28faa222 | ||
|
60aac1aea7 | ||
|
afa3c516ee | ||
|
7cd136cd7b | ||
|
a593ea0160 | ||
|
5ae641fda0 | ||
|
08871e15b6 | ||
|
f55a5eba96 | ||
|
011cc25490 | ||
|
d44a3081bc | ||
|
51f7f54be7 | ||
|
a3fc97a460 | ||
|
05941f0d6f | ||
|
7b4cd7c26e | ||
|
f54d5bd780 | ||
|
435c07d75e | ||
|
cc09d1b529 | ||
|
60c3a1f9ac | ||
|
d4d94b61da | ||
|
9053211904 | ||
|
35a512680c | ||
|
bcd620ed43 | ||
|
556f4e87df | ||
|
fa88bce917 | ||
|
a5d326b679 | ||
|
465873e095 | ||
|
39233e9447 | ||
|
738e036cb5 | ||
|
de2cc48f88 | ||
|
0687f44137 | ||
|
fec933c5ec | ||
|
cc2dd1e1fe | ||
|
85ed1b2b92 | ||
|
73ec59ccda | ||
|
f5b9036a2a | ||
|
0d43f2f60a | ||
|
aa9246da4e | ||
|
1c21943c75 | ||
|
7f4b9f4aee | ||
|
acd547dbf3 | ||
|
ce8bd4dd02 | ||
|
81bf17e004 | ||
|
92a29a02e5 | ||
|
8b1702365c | ||
|
291438e771 | ||
|
497a8ef0e3 | ||
|
ff921dc103 | ||
|
fe013d9d85 | ||
|
be3ac27920 | ||
|
2f13a0e8e7 | ||
|
35d4df2cb8 | ||
|
4cc737731a |
56 changed files with 5981 additions and 1026 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
|||
use flake
|
4
.github/FUNDING.yml
vendored
Normal file
4
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: szabodanika
|
||||
ko_fi: dani_sz
|
BIN
.github/index.png
vendored
Normal file
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
BIN
.github/logo.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
13
.github/workflows/build_nix.yml
vendored
Normal file
13
.github/workflows/build_nix.yml
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
name: "Build legacy Nix package on Ubuntu"
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: cachix/install-nix-action@v12
|
||||
- name: Building package
|
||||
run: nix-build . -A defaultPackage.x86_64-linux
|
192
.github/workflows/docker.yml
vendored
Normal file
192
.github/workflows/docker.yml
vendored
Normal file
|
@ -0,0 +1,192 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v[0-9]+.[0-9]+.[0-9]+*
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Publish to Github Relases
|
||||
outputs:
|
||||
rc: ${{ steps.check-tag.outputs.rc }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: aarch64-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
use-cross: true
|
||||
cargo-flags: ""
|
||||
- target: aarch64-apple-darwin
|
||||
os: macos-latest
|
||||
use-cross: true
|
||||
cargo-flags: ""
|
||||
- target: aarch64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
use-cross: true
|
||||
cargo-flags: "--no-default-features"
|
||||
- target: x86_64-apple-darwin
|
||||
os: macos-latest
|
||||
cargo-flags: ""
|
||||
- target: x86_64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
cargo-flags: ""
|
||||
- target: x86_64-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
use-cross: true
|
||||
cargo-flags: ""
|
||||
- target: i686-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
use-cross: true
|
||||
cargo-flags: ""
|
||||
- target: i686-pc-windows-msvc
|
||||
os: windows-latest
|
||||
use-cross: true
|
||||
cargo-flags: ""
|
||||
- target: armv7-unknown-linux-musleabihf
|
||||
os: ubuntu-latest
|
||||
use-cross: true
|
||||
cargo-flags: ""
|
||||
- target: arm-unknown-linux-musleabihf
|
||||
os: ubuntu-latest
|
||||
use-cross: true
|
||||
cargo-flags: ""
|
||||
- target: mips-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
use-cross: true
|
||||
cargo-flags: "--no-default-features"
|
||||
- target: mipsel-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
use-cross: true
|
||||
cargo-flags: "--no-default-features"
|
||||
- target: mips64-unknown-linux-gnuabi64
|
||||
os: ubuntu-latest
|
||||
use-cross: true
|
||||
cargo-flags: "--no-default-features"
|
||||
- target: mips64el-unknown-linux-gnuabi64
|
||||
os: ubuntu-latest
|
||||
use-cross: true
|
||||
cargo-flags: "--no-default-features"
|
||||
runs-on: ${{matrix.os}}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Check Tag
|
||||
id: check-tag
|
||||
shell: bash
|
||||
run: |
|
||||
tag=${GITHUB_REF##*/}
|
||||
echo "::set-output name=version::$tag"
|
||||
if [[ "$tag" =~ [0-9]+.[0-9]+.[0-9]+$ ]]; then
|
||||
echo "::set-output name=rc::false"
|
||||
else
|
||||
echo "::set-output name=rc::true"
|
||||
fi
|
||||
|
||||
|
||||
- name: Install Rust Toolchain Components
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
override: true
|
||||
target: ${{ matrix.target }}
|
||||
toolchain: stable
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
|
||||
- name: Show Version Information (Rust, cargo, GCC)
|
||||
shell: bash
|
||||
run: |
|
||||
gcc --version || true
|
||||
rustup -V
|
||||
rustup toolchain list
|
||||
rustup default
|
||||
cargo -V
|
||||
rustc -V
|
||||
|
||||
- name: Build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: ${{ matrix.use-cross }}
|
||||
command: build
|
||||
args: --locked --release --target=${{ matrix.target }} ${{ matrix.cargo-flags }}
|
||||
|
||||
- name: Build Archive
|
||||
shell: bash
|
||||
id: package
|
||||
env:
|
||||
target: ${{ matrix.target }}
|
||||
version: ${{ steps.check-tag.outputs.version }}
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
|
||||
bin=${GITHUB_REPOSITORY##*/}
|
||||
src=`pwd`
|
||||
dist=$src/dist
|
||||
name=$bin-$version-$target
|
||||
executable=target/$target/release/$bin
|
||||
|
||||
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
||||
executable=$executable.exe
|
||||
fi
|
||||
|
||||
mkdir $dist
|
||||
cp $executable $dist
|
||||
cd $dist
|
||||
|
||||
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
||||
archive=$dist/$name.zip
|
||||
7z a $archive *
|
||||
echo "::set-output name=archive::`pwd -W`/$name.zip"
|
||||
else
|
||||
archive=$dist/$name.tar.gz
|
||||
tar czf $archive *
|
||||
echo "::set-output name=archive::$archive"
|
||||
fi
|
||||
|
||||
- name: Publish Archive
|
||||
uses: softprops/action-gh-release@v0.1.5
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
with:
|
||||
draft: false
|
||||
files: ${{ steps.package.outputs.archive }}
|
||||
prerelease: ${{ steps.check-tag.outputs.rc == 'true' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
docker:
|
||||
name: Publish to Docker Hub
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
steps:
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_REPO }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
build-args: |
|
||||
REPO=${{ github.repository }}
|
||||
VER=${{ github.ref_name }}
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
push: ${{ github.ref_type == 'tag' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
154
.github/workflows/gh-release.yml
vendored
Normal file
154
.github/workflows/gh-release.yml
vendored
Normal file
|
@ -0,0 +1,154 @@
|
|||
name: GitHub Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v[0-9]+.[0-9]+.[0-9]+*
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Publish to Github Releases
|
||||
outputs:
|
||||
rc: ${{ steps.check-tag.outputs.rc }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: aarch64-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
use-cross: true
|
||||
cargo-flags: ""
|
||||
- target: aarch64-apple-darwin
|
||||
os: macos-latest
|
||||
use-cross: true
|
||||
cargo-flags: ""
|
||||
- target: aarch64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
use-cross: true
|
||||
cargo-flags: "--no-default-features"
|
||||
- target: x86_64-apple-darwin
|
||||
os: macos-latest
|
||||
cargo-flags: ""
|
||||
- target: x86_64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
cargo-flags: ""
|
||||
- target: x86_64-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
use-cross: true
|
||||
cargo-flags: ""
|
||||
- target: i686-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
use-cross: true
|
||||
cargo-flags: ""
|
||||
- target: i686-pc-windows-msvc
|
||||
os: windows-latest
|
||||
use-cross: true
|
||||
cargo-flags: ""
|
||||
- target: armv7-unknown-linux-musleabihf
|
||||
os: ubuntu-latest
|
||||
use-cross: true
|
||||
cargo-flags: ""
|
||||
- target: arm-unknown-linux-musleabihf
|
||||
os: ubuntu-latest
|
||||
use-cross: true
|
||||
cargo-flags: ""
|
||||
- target: mips-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
use-cross: true
|
||||
cargo-flags: "--no-default-features"
|
||||
- target: mipsel-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
use-cross: true
|
||||
cargo-flags: "--no-default-features"
|
||||
- target: mips64-unknown-linux-gnuabi64
|
||||
os: ubuntu-latest
|
||||
use-cross: true
|
||||
cargo-flags: "--no-default-features"
|
||||
- target: mips64el-unknown-linux-gnuabi64
|
||||
os: ubuntu-latest
|
||||
use-cross: true
|
||||
cargo-flags: "--no-default-features"
|
||||
runs-on: ${{matrix.os}}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Check Tag
|
||||
id: check-tag
|
||||
shell: bash
|
||||
run: |
|
||||
tag=${GITHUB_REF##*/}
|
||||
echo "::set-output name=version::$tag"
|
||||
if [[ "$tag" =~ [0-9]+.[0-9]+.[0-9]+$ ]]; then
|
||||
echo "::set-output name=rc::false"
|
||||
else
|
||||
echo "::set-output name=rc::true"
|
||||
fi
|
||||
|
||||
|
||||
- name: Install Rust Toolchain Components
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
override: true
|
||||
target: ${{ matrix.target }}
|
||||
toolchain: stable
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
|
||||
- name: Show Version Information (Rust, cargo, GCC)
|
||||
shell: bash
|
||||
run: |
|
||||
gcc --version || true
|
||||
rustup -V
|
||||
rustup toolchain list
|
||||
rustup default
|
||||
cargo -V
|
||||
rustc -V
|
||||
|
||||
- name: Build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: ${{ matrix.use-cross }}
|
||||
command: build
|
||||
args: --locked --release --target=${{ matrix.target }} ${{ matrix.cargo-flags }}
|
||||
|
||||
- name: Build Archive
|
||||
shell: bash
|
||||
id: package
|
||||
env:
|
||||
target: ${{ matrix.target }}
|
||||
version: ${{ steps.check-tag.outputs.version }}
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
|
||||
bin=${GITHUB_REPOSITORY##*/}
|
||||
src=`pwd`
|
||||
dist=$src/dist
|
||||
name=$bin-$version-$target
|
||||
executable=target/$target/release/$bin
|
||||
|
||||
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
||||
executable=$executable.exe
|
||||
fi
|
||||
|
||||
mkdir $dist
|
||||
cp $executable $dist
|
||||
cd $dist
|
||||
|
||||
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
||||
archive=$dist/$name.zip
|
||||
7z a $archive *
|
||||
echo "::set-output name=archive::`pwd -W`/$name.zip"
|
||||
else
|
||||
archive=$dist/$name.tar.gz
|
||||
tar czf $archive *
|
||||
echo "::set-output name=archive::$archive"
|
||||
fi
|
||||
|
||||
- name: Publish Archive
|
||||
uses: softprops/action-gh-release@v0.1.5
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
with:
|
||||
draft: false
|
||||
files: ${{ steps.package.outputs.archive }}
|
||||
prerelease: ${{ steps.check-tag.outputs.rc == 'true' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
result
|
||||
.direnv/
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
pasta_data/*
|
2444
Cargo.lock
generated
Normal file
2444
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
25
Cargo.toml
25
Cargo.toml
|
@ -1,19 +1,28 @@
|
|||
[package]
|
||||
name="microbin"
|
||||
version="0.2.0"
|
||||
name = "karton"
|
||||
version = "2.0.1"
|
||||
edition = "2021"
|
||||
authors = ["Jade <jade@schrottkatze.de>", "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://gitlab.com/obsidianical/microbin"
|
||||
repository = "https://gitlab.com/obsidianical/microbin"
|
||||
keywords = ["pastebin", "karton", "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"] }
|
||||
clap = { version = "3.1.12", features = ["derive", "env"] }
|
||||
actix-multipart = "0.4.0"
|
||||
futures = "0.3"
|
||||
sanitize-filename = "0.3.0"
|
||||
|
@ -21,4 +30,12 @@ log = "0.4"
|
|||
env_logger = "0.9.0"
|
||||
actix-web-httpauth = "0.6.0"
|
||||
lazy_static = "1.4.0"
|
||||
rutie = "0.8.4"
|
||||
syntect = "5.0"
|
||||
qrcode-generator = "4.1.6"
|
||||
rust-embed = "6.4.2"
|
||||
mime_guess = "2.0.4"
|
||||
harsh = "0.2"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
strip = true
|
||||
|
|
54
Dockerfile
54
Dockerfile
|
@ -1,31 +1,37 @@
|
|||
# latest rust will be used to build the binary
|
||||
FROM rust:latest as builder
|
||||
FROM docker.io/rust:latest as build
|
||||
|
||||
# the temporary directory where we build
|
||||
WORKDIR /usr/src/microbin
|
||||
WORKDIR /app
|
||||
|
||||
# copy sources to /usr/src/microbin on the temporary container
|
||||
COPY . .
|
||||
|
||||
# run release build
|
||||
RUN cargo build --release
|
||||
|
||||
# create final container using slim version of debian
|
||||
FROM debian:buster-slim
|
||||
|
||||
# microbin will be in /usr/local/bin/microbin/
|
||||
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 \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get update &&\
|
||||
apt-get install -y ruby
|
||||
apt-get -y install ca-certificates tzdata &&\
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI=true \
|
||||
cargo build --release
|
||||
|
||||
# run the binary
|
||||
CMD ["microbin"]
|
||||
# https://hub.docker.com/r/bitnami/minideb
|
||||
FROM docker.io/bitnami/minideb:latest
|
||||
|
||||
# microbin will be in /app
|
||||
WORKDIR /app
|
||||
|
||||
# copy time zone info
|
||||
COPY --from=build \
|
||||
/usr/share/zoneinfo \
|
||||
/usr/share/zoneinfo
|
||||
|
||||
COPY --from=build \
|
||||
/etc/ssl/certs/ca-certificates.crt \
|
||||
/etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
# copy built executable
|
||||
COPY --from=build \
|
||||
/app/target/release/karton \
|
||||
/usr/bin/karton
|
||||
|
||||
# Expose webport used for the webserver to the docker runtime
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["karton"]
|
||||
|
|
66
README.MD
66
README.MD
|
@ -1,66 +0,0 @@
|
|||
# MicroBin
|
||||
|
||||
![Screenshot](git/index.png)
|
||||
|
||||
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
|
||||
- File uploads
|
||||
- Raw pasta content (/raw/[animals])
|
||||
- URL shortening and redirection
|
||||
- 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))
|
||||
|
||||
### 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).
|
||||
|
||||
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]
|
||||
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
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
|
||||
### 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
|
||||
|
76
README.md
Normal file
76
README.md
Normal file
|
@ -0,0 +1,76 @@
|
|||
# Karton
|
||||
|
||||
A small, rusty pastebin with URL shortener functionality.
|
||||
|
||||
The github repository is a mirror of [this gitlab repository](https://gitlab.com/obsidianical/microbin).
|
||||
|
||||
This is a fork of [MicroBin](https://github.com/szabodanika/microbin).
|
||||
|
||||
## Features
|
||||
|
||||
- Animal names (by default) or custom namefiles instead of just hashes (though hashes are an option too!)
|
||||
- File and image uploads
|
||||
- raw text serving
|
||||
- URL shortening
|
||||
- QR codes
|
||||
- Listing and removing pastas (though currently everyone can do that)
|
||||
- Expiration times
|
||||
- Editable pastas
|
||||
- Syntax highlighting
|
||||
- Styling via [water.css](https://github.com/kognise/water.css)
|
||||
- Customizable endpoints
|
||||
|
||||
## Installation guide
|
||||
|
||||
Karton is available on [Docker hub](https://hub.docker.com/r/schrottkatze/karton), [crates.io](https://crates.io/crates/karton) and using the nix flake.
|
||||
|
||||
The only "officially supported" (I will actively debug and search for the problem) method is the last one using nix flakes.
|
||||
|
||||
### Installation via the nix flake
|
||||
|
||||
Add the repository to your inputs.
|
||||
|
||||
```nix
|
||||
karton.url = "git+https://gitlab.com/obsidianical/microbin.git";
|
||||
```
|
||||
|
||||
```nix
|
||||
# microbin.nix
|
||||
{ inputs, config, pkgs, ... }:
|
||||
{
|
||||
environment.systemPackages = [ inputs.karton.defaultPackage."x86_64-linux" ];
|
||||
systemd.services.karton = {
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
environment = {
|
||||
# set environment variables to configure karton
|
||||
KARTON_HASH_IDS = "";
|
||||
KARTON_EDITABLE = "";
|
||||
KARTON_PRIVATE = "";
|
||||
KARTON_HIGHLIGHTSYNTAX = "";
|
||||
# adjust this to your domain
|
||||
KARTON_PUBLIC_PATH = "https://example.org";
|
||||
KARTON_QR = "";
|
||||
# configure endpoints to be shorter
|
||||
KARTON_URL_EP = "u";
|
||||
KARTON_RAW_EP = "r";
|
||||
KARTON_PASTA_EP = "p";
|
||||
};
|
||||
script = "${inputs.karton.defaultPackage."x86_64-linux"}/bin/karton";
|
||||
# register a simple systemd service
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
RootDirectory="/";
|
||||
WorkingDirectory = "/karton";
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Contact
|
||||
|
||||
This fork of MicroBin was created by [Schrottkatze](https://schrottkatze.de).
|
||||
|
||||
Join [the matrix room](https://matrix.to/#/#s10e-microbin:matrix.org) to chat!
|
||||
|
||||
Contact me via e-mail at [contact@schrottkatze.de](mailto:contact@schrottkatze.de).
|
54
TODO.md
Normal file
54
TODO.md
Normal file
|
@ -0,0 +1,54 @@
|
|||
# TODO lists
|
||||
|
||||
these are just rough guides tho
|
||||
|
||||
## v2.1
|
||||
|
||||
- [ ] customizable endpoints
|
||||
- [ ] create
|
||||
- [ ] edit
|
||||
- [ ] info
|
||||
- [ ] get pastas
|
||||
- [ ] remove
|
||||
- [ ] improve remove endpoint
|
||||
- [ ] disable it
|
||||
- [ ] client library
|
||||
- [ ] request .well-known data
|
||||
- [ ] support most endpoints
|
||||
- [ ] karton cli
|
||||
|
||||
## v3.0
|
||||
|
||||
- [ ] internal rewrite & docs
|
||||
- [ ] design new frontend
|
||||
- [ ] switch to yew
|
||||
- [ ] using client lib
|
||||
- [ ] theme and general config files
|
||||
- [ ] unified theme format
|
||||
- [ ] no env configs anymore if possible
|
||||
- [ ] proper dbs
|
||||
- [ ] sqlite
|
||||
- [ ] postgres
|
||||
- [ ] apis/endpoints
|
||||
- [ ] IDs, name IDs AND user/pastaname
|
||||
- [ ] root (and admin) user for root level pastas
|
||||
- [ ] status/instance health admin dashboard and ap
|
||||
- [ ] storage
|
||||
- [ ] db status
|
||||
- [ ] how up to date
|
||||
- [ ] stats (users etc)
|
||||
- [ ] errors
|
||||
- [ ] loading speeds, performance monitor?
|
||||
- [ ] memory use
|
||||
- [ ] auth
|
||||
- [ ] general auth
|
||||
- [ ] oidc
|
||||
- [ ] permssion system & api keys
|
||||
- [ ] only allow some other users to open pasta
|
||||
- [ ] access control and editing
|
||||
- [ ] password protected pastas too
|
||||
- [ ] features for pastas
|
||||
- [ ] pw protection
|
||||
- [ ] better editor
|
||||
- [ ] markdown pastas
|
||||
- [ ] optional, opt-in commenting
|
7
default.nix
Normal file
7
default.nix
Normal file
|
@ -0,0 +1,7 @@
|
|||
(import (
|
||||
fetchTarball {
|
||||
url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz";
|
||||
sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; }
|
||||
) {
|
||||
src = ./.;
|
||||
}).defaultNix
|
77
flake.lock
Normal file
77
flake.lock
Normal file
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"nodes": {
|
||||
"naersk": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1671096816,
|
||||
"narHash": "sha256-ezQCsNgmpUHdZANDCILm3RvtO1xH8uujk/+EqNvzIOg=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "d998160d6a076cfe8f9741e56aeec7e267e3e114",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"ref": "master",
|
||||
"repo": "naersk",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1677852945,
|
||||
"narHash": "sha256-liiVJjkBTuBTAkRW3hrI8MbPD2ImYzwUpa7kvteiKhM=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f5ffd5787786dde3a8bf648c7a1b5f78c4e01abb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1677852945,
|
||||
"narHash": "sha256-liiVJjkBTuBTAkRW3hrI8MbPD2ImYzwUpa7kvteiKhM=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f5ffd5787786dde3a8bf648c7a1b5f78c4e01abb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"naersk": "naersk",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"utils": "utils"
|
||||
}
|
||||
},
|
||||
"utils": {
|
||||
"locked": {
|
||||
"lastModified": 1676283394,
|
||||
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
21
flake.nix
Normal file
21
flake.nix
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
inputs = {
|
||||
naersk.url = "github:nix-community/naersk/master";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, utils, naersk }:
|
||||
utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
naersk-lib = pkgs.callPackage naersk { };
|
||||
in
|
||||
{
|
||||
defaultPackage = naersk-lib.buildPackage ./.;
|
||||
devShell = with pkgs; mkShell {
|
||||
buildInputs = [ cargo rustc rustfmt pre-commit rustPackages.clippy cargo-watch podman ];
|
||||
RUST_SRC_PATH = rustPlatform.rustLibSrc;
|
||||
};
|
||||
});
|
||||
}
|
BIN
git/index.png
BIN
git/index.png
Binary file not shown.
Before Width: | Height: | Size: 480 KiB |
|
@ -1,78 +0,0 @@
|
|||
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
|
|
@ -1,78 +0,0 @@
|
|||
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
|
|
@ -1,42 +0,0 @@
|
|||
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
|
7
shell.nix
Normal file
7
shell.nix
Normal file
|
@ -0,0 +1,7 @@
|
|||
(import (
|
||||
fetchTarball {
|
||||
url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz";
|
||||
sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; }
|
||||
) {
|
||||
src = ./.;
|
||||
}).shellNix
|
|
@ -1,48 +0,0 @@
|
|||
const ANIMAL_NAMES: &[&str] = &[
|
||||
"ant", "eel", "mole", "sloth", "ape", "emu", "monkey", "snail", "bat", "falcon", "mouse",
|
||||
"snake", "bear", "fish", "otter", "spider", "bee", "fly", "parrot", "squid", "bird", "fox",
|
||||
"panda", "swan", "bison", "frog", "pig", "tiger", "camel", "gecko", "pigeon", "toad", "cat",
|
||||
"goat", "pony", "turkey", "cobra", "goose", "pug", "turtle", "crow", "hawk", "rabbit", "viper",
|
||||
"deer", "horse", "rat", "wasp", "dog", "jaguar", "raven", "whale", "dove", "koala", "seal",
|
||||
"wolf", "duck", "lion", "shark", "worm", "eagle", "lizard", "sheep", "zebra",
|
||||
];
|
||||
|
||||
pub fn to_animal_names(mut number: u64) -> String {
|
||||
let mut result: Vec<&str> = Vec::new();
|
||||
|
||||
if number == 0 {
|
||||
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) {
|
||||
result.push(ANIMAL_NAMES[digit as usize]);
|
||||
}
|
||||
number -= digit * ANIMAL_NAMES.len().pow(power) as u64;
|
||||
if power > 0 {
|
||||
power -= 1;
|
||||
} else if power == 0 || number == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result.join("-")
|
||||
}
|
||||
|
||||
pub fn to_u64(animal_names: &str) -> u64 {
|
||||
let mut result: u64 = 0;
|
||||
|
||||
let animals: Vec<&str> = animal_names.split("-").collect();
|
||||
|
||||
let mut pow = animals.len();
|
||||
for i in 0..animals.len() {
|
||||
pow -= 1;
|
||||
result += (ANIMAL_NAMES.iter().position(|&r| r == animals[i]).unwrap()
|
||||
* ANIMAL_NAMES.len().pow(pow as u32)) as u64;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
166
src/args.rs
Normal file
166
src/args.rs
Normal file
|
@ -0,0 +1,166 @@
|
|||
use clap::Parser;
|
||||
use lazy_static::lazy_static;
|
||||
use std::convert::Infallible;
|
||||
use std::fmt;
|
||||
use std::net::IpAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ARGS: Args = Args::parse();
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
pub struct Args {
|
||||
/// The username for basic HTTP auth.
|
||||
/// If unset, HTTP authentication stays disabled.
|
||||
///
|
||||
/// WARNING: people opening pastas will have to authenticate too.
|
||||
#[clap(long, env = "KARTON_AUTH_USERNAME")]
|
||||
pub auth_username: Option<String>,
|
||||
|
||||
/// Set a password for HTTP authentication.
|
||||
/// If unset, HTTP authentication will not require a password.
|
||||
/// If `auth_username` is unset, this option will not have any effect.
|
||||
#[clap(long, env = "KARTON_AUTH_PASSWORD")]
|
||||
pub auth_password: Option<String>,
|
||||
|
||||
/// Enable the option to make pastas editable.
|
||||
#[clap(long, env = "KARTON_EDITABLE")]
|
||||
pub editable: bool,
|
||||
|
||||
/// The text displayed in the browser navigation bar.
|
||||
#[clap(long, env = "KARTON_TITLE", default_value = " Karton")]
|
||||
pub title: String,
|
||||
|
||||
/// The web interfaces' footer text.
|
||||
#[clap(long, env = "KARTON_FOOTER_TEXT")]
|
||||
pub footer_text: Option<String>,
|
||||
|
||||
/// Hide the footer of the web interface.
|
||||
#[clap(long, env = "KARTON_HIDE_FOOTER")]
|
||||
pub hide_footer: bool,
|
||||
|
||||
/// Hide the header of the web interface.
|
||||
#[clap(long, env = "KARTON_HIDE_HEADER")]
|
||||
pub hide_header: bool,
|
||||
|
||||
/// Hide the logo in the header.
|
||||
#[clap(long, env = "KARTON_HIDE_LOGO")]
|
||||
pub hide_logo: bool,
|
||||
|
||||
/// Disable the listing page.
|
||||
#[clap(long, env = "KARTON_NO_LISTING")]
|
||||
pub no_listing: bool,
|
||||
|
||||
/// Enable syntax highlighting in pastas.
|
||||
#[clap(long, env = "KARTON_HIGHLIGHTSYNTAX")]
|
||||
pub highlightsyntax: bool,
|
||||
|
||||
/// The port to which to bind the server.
|
||||
#[clap(short, long, env = "KARTON_PORT", default_value_t = 8080)]
|
||||
pub port: u16,
|
||||
|
||||
/// The IP adress to bind the server to.
|
||||
#[clap(short, long, env="KARTON_BIND", default_value_t = IpAddr::from([0, 0, 0, 0]))]
|
||||
pub bind: IpAddr,
|
||||
|
||||
/// Enable the option to create private pastas.
|
||||
#[clap(long, env = "KARTON_PRIVATE")]
|
||||
pub private: bool,
|
||||
|
||||
/// Disables most css, apart form some inline styles.
|
||||
#[clap(long, env = "KARTON_PURE_HTML")]
|
||||
pub pure_html: bool,
|
||||
|
||||
/// The servers public path, making it possible to run Karton behind a reverse proxy subpath.
|
||||
#[clap(long, env="KARTON_PUBLIC_PATH", default_value_t = PublicUrl(String::from("")))]
|
||||
pub public_path: PublicUrl,
|
||||
|
||||
/// Enable creation of QR codes of pastas. Requires `public_path` to be set.
|
||||
#[clap(long, env = "KARTON_QR")]
|
||||
pub qr: bool,
|
||||
|
||||
|
||||
/// Disable adding/removing/editing pastas.
|
||||
#[clap(long, env = "KARTON_READONLY")]
|
||||
pub readonly: bool,
|
||||
|
||||
/// The amount of worker threads that the server is allowed to have.
|
||||
#[clap(short, long, env = "KARTON_THREADS", default_value_t = 1)]
|
||||
pub threads: u8,
|
||||
|
||||
/// Sets a time value for the garbage collector. Pastas that aren't accessed for the given
|
||||
/// amount of days will be deleted. Set to 0 to disable garbage collection.
|
||||
#[clap(short, long, env = "KARTON_GC_DAYS", default_value_t = 90)]
|
||||
pub gc_days: u16,
|
||||
|
||||
/// Enable the option to delete after a given amount of reads.
|
||||
#[clap(long, env = "KARTON_ENABLE_BURN_AFTER")]
|
||||
pub enable_burn_after: bool,
|
||||
|
||||
/// The default amount of reads for the self-delete mechanism.
|
||||
#[clap(short, long, env = "KARTON_DEFAULT_BURN_AFTER", default_value_t = 0)]
|
||||
pub default_burn_after: u16,
|
||||
|
||||
/// Changes the UIs maximum width from 720 pixels to 1080.
|
||||
#[clap(long, env = "KARTON_WIDE")]
|
||||
pub wide: bool,
|
||||
|
||||
/// Disable "Never" expiry setting.
|
||||
#[clap(long, env = "KARTON_NO_ETERNAL_PASTA")]
|
||||
pub no_eternal_pasta: bool,
|
||||
|
||||
/// Set the default expiry time value.
|
||||
#[clap(long, env = "KARTON_DEFAULT_EXPIRY", default_value = "24hour")]
|
||||
pub default_expiry: String,
|
||||
|
||||
/// Disable file uploading.
|
||||
#[clap(short, long, env = "KARTON_NO_FILE_UPLOAD")]
|
||||
pub no_file_upload: bool,
|
||||
|
||||
// TODO: replace with simple path.
|
||||
/// Replace built-in CSS file with a CSS file provided by the linked URL.
|
||||
#[clap(long, env = "KARTON_CUSTOM_CSS")]
|
||||
pub custom_css: Option<String>,
|
||||
|
||||
/// Replace built-in animal names file with custom names file for pasta links.
|
||||
/// The file must be newline seperated.
|
||||
#[clap(long, env = "KARTON_CUSTOM_NAMES")]
|
||||
pub custom_names: Option<PathBuf>,
|
||||
|
||||
/// Enable the use of Hash IDs for shorter URLs instead of animal names.
|
||||
#[clap(long, env = "KARTON_HASH_IDS")]
|
||||
pub hash_ids: bool,
|
||||
|
||||
/// Endpoint for /url/
|
||||
#[clap(long, env = "KARTON_URL_EP", default_value = "url" )]
|
||||
pub url_endpoint: String,
|
||||
|
||||
/// Endpoint for /pasta/
|
||||
#[clap(long, env = "KARTON_PASTA_EP", default_value = "pasta" )]
|
||||
pub pasta_endpoint: String,
|
||||
|
||||
/// Endpoint for /raw/
|
||||
#[clap(long, env = "KARTON_RAW_EP", default_value = "raw" )]
|
||||
pub raw_endpoint: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PublicUrl(pub String);
|
||||
|
||||
impl fmt::Display for PublicUrl {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PublicUrl {
|
||||
type Err = Infallible;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let uri = s.strip_suffix('/').unwrap_or(s).to_owned();
|
||||
Ok(PublicUrl(uri))
|
||||
}
|
||||
}
|
214
src/endpoints/create.rs
Normal file
214
src/endpoints/create.rs
Normal file
|
@ -0,0 +1,214 @@
|
|||
use crate::dbio::save_to_file;
|
||||
use crate::pasta::PastaFile;
|
||||
use crate::util::hashids::to_hashids;
|
||||
use crate::util::misc::is_valid_url;
|
||||
use crate::util::pasta_id_converter::CONVERTER;
|
||||
use crate::{AppState, Pasta, ARGS};
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::{BytesMut, BufMut};
|
||||
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};
|
||||
|
||||
use super::errors::ErrorTemplate;
|
||||
|
||||
#[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())
|
||||
}
|
||||
|
||||
/// Pasta creation endpoint.
|
||||
pub async fn create(
|
||||
data: web::Data<AppState>,
|
||||
mut payload: Multipart,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
if ARGS.readonly {
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header(("Location", format!("{}/", ARGS.public_path)))
|
||||
.finish());
|
||||
}
|
||||
|
||||
let mut pastas = data.pastas.lock().await;
|
||||
|
||||
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,
|
||||
read_count: 0,
|
||||
burn_after_reads: 0,
|
||||
last_read: timenow,
|
||||
pasta_type: String::from(""),
|
||||
expiration: 0,
|
||||
};
|
||||
|
||||
while let Some(mut field) = payload.try_next().await? {
|
||||
match field.name() {
|
||||
"editable" => {
|
||||
new_pasta.editable = true;
|
||||
}
|
||||
"private" => {
|
||||
new_pasta.private = true;
|
||||
}
|
||||
"expiration" => {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
new_pasta.expiration = match std::str::from_utf8(&chunk).unwrap() {
|
||||
// TODO: customizable times
|
||||
"1min" => timenow + 60,
|
||||
"10min" => timenow + 60 * 10,
|
||||
"1hour" => timenow + 60 * 60,
|
||||
"24hour" => timenow + 60 * 60 * 24,
|
||||
"3days" => timenow + 60 * 60 * 24 * 3,
|
||||
"1week" => timenow + 60 * 60 * 24 * 7,
|
||||
"never" => {
|
||||
if ARGS.no_eternal_pasta {
|
||||
timenow + 60 * 60 * 24 * 7
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::error!("{}", "Unexpected expiration time!");
|
||||
timenow + 60 * 60 * 24 * 7
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
"burn_after" => {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
new_pasta.burn_after_reads = match std::str::from_utf8(&chunk).unwrap() {
|
||||
// TODO: also make customizable
|
||||
// maybe options in config files, with defaults
|
||||
// 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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
"content" => {
|
||||
let mut content = BytesMut::new();
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
content.put(chunk);
|
||||
}
|
||||
if !content.is_empty() {
|
||||
new_pasta.content = match String::from_utf8(content.to_vec()) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Ok(HttpResponse::BadRequest()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate {
|
||||
status_code: StatusCode::BAD_REQUEST,
|
||||
args: &ARGS
|
||||
}.render().unwrap())),
|
||||
};
|
||||
|
||||
new_pasta.pasta_type = if is_valid_url(new_pasta.content.as_str()) {
|
||||
String::from("url")
|
||||
} else {
|
||||
String::from("text")
|
||||
};
|
||||
}
|
||||
}
|
||||
"syntax-highlight" => {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
new_pasta.extension = std::str::from_utf8(&chunk).unwrap().to_string();
|
||||
}
|
||||
}
|
||||
"file" => {
|
||||
if ARGS.no_file_upload {
|
||||
continue;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
field => {
|
||||
log::error!("Unexpected multipart field: {}", field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let id = new_pasta.id;
|
||||
|
||||
pastas.push(new_pasta);
|
||||
|
||||
save_to_file(&pastas);
|
||||
|
||||
let slug = if ARGS.hash_ids {
|
||||
to_hashids(id)
|
||||
} else {
|
||||
CONVERTER.to_names(id)
|
||||
};
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("Location", format!("{}/{}/{}", ARGS.public_path, ARGS.pasta_endpoint, slug)))
|
||||
.finish())
|
||||
}
|
110
src/endpoints/edit.rs
Normal file
110
src/endpoints/edit.rs
Normal file
|
@ -0,0 +1,110 @@
|
|||
use crate::args::Args;
|
||||
use crate::dbio::save_to_file;
|
||||
use crate::endpoints::errors::ErrorTemplate;
|
||||
use crate::util::hashids::to_u64 as hashid_to_u64;
|
||||
use crate::util::misc::remove_expired;
|
||||
use crate::util::pasta_id_converter::CONVERTER;
|
||||
use crate::{AppState, Pasta, ARGS};
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::http::StatusCode;
|
||||
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().await;
|
||||
|
||||
let id = if ARGS.hash_ids {
|
||||
hashid_to_u64(&id).unwrap_or(0)
|
||||
} else {
|
||||
CONVERTER.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", format!("{}/", ARGS.public_path)))
|
||||
.finish();
|
||||
}
|
||||
return HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(EditTemplate { pasta, args: &ARGS }.render().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse::NotFound()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate {
|
||||
status_code: StatusCode::NOT_FOUND,
|
||||
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", format!("{}/", ARGS.public_path)))
|
||||
.finish());
|
||||
}
|
||||
|
||||
let id = if ARGS.hash_ids {
|
||||
hashid_to_u64(&id).unwrap_or(0)
|
||||
} else {
|
||||
CONVERTER.to_u64(&id.into_inner()).unwrap_or(0)
|
||||
};
|
||||
|
||||
let mut pastas = data.pastas.lock().await;
|
||||
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
let mut new_content = String::from("");
|
||||
|
||||
while let Some(mut field) = payload.try_next().await? {
|
||||
if 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/{}", ARGS.public_path, pastas[i].id_as_animals()),
|
||||
))
|
||||
.finish());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HttpResponse::NotFound()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate {
|
||||
status_code: StatusCode::NOT_FOUND,
|
||||
args: &ARGS
|
||||
}.render().unwrap()))
|
||||
}
|
20
src/endpoints/errors.rs
Normal file
20
src/endpoints/errors.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
use actix_web::{Error, HttpResponse, http::StatusCode};
|
||||
use askama::Template;
|
||||
|
||||
use crate::args::{Args, ARGS};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "error.html")]
|
||||
pub struct ErrorTemplate<'a> {
|
||||
pub status_code: StatusCode,
|
||||
pub args: &'a Args,
|
||||
}
|
||||
|
||||
pub async fn not_found() -> Result<HttpResponse, Error> {
|
||||
Ok(HttpResponse::NotFound()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate {
|
||||
status_code: StatusCode::NOT_FOUND,
|
||||
args: &ARGS
|
||||
}.render().unwrap()))
|
||||
}
|
47
src/endpoints/info.rs
Normal file
47
src/endpoints/info.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
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 str,
|
||||
version_string: &'a str,
|
||||
message: &'a str,
|
||||
}
|
||||
|
||||
/// Endpoint to get information about the instance.
|
||||
#[get("/info")]
|
||||
pub async fn info(data: web::Data<AppState>) -> HttpResponse {
|
||||
// get access to the pasta collection
|
||||
let pastas = data.pastas.lock().await;
|
||||
|
||||
// TODO: status report more sophisticated
|
||||
// maybe:
|
||||
// - detect weird/invalid configurations?
|
||||
// - detect server storage issues
|
||||
// - detect performance problems?
|
||||
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,
|
||||
version_string: env!("CARGO_PKG_VERSION"),
|
||||
message
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
213
src/endpoints/pasta.rs
Normal file
213
src/endpoints/pasta.rs
Normal file
|
@ -0,0 +1,213 @@
|
|||
use crate::args::{Args, ARGS};
|
||||
use crate::dbio::save_to_file;
|
||||
use crate::endpoints::errors::ErrorTemplate;
|
||||
use crate::pasta::Pasta;
|
||||
use crate::util::hashids::to_u64 as hashid_to_u64;
|
||||
use crate::util::misc::remove_expired;
|
||||
use crate::AppState;
|
||||
use crate::util::pasta_id_converter::CONVERTER;
|
||||
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{web, HttpResponse};
|
||||
use askama::Template;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "pasta.html", escape = "none")]
|
||||
struct PastaTemplate<'a> {
|
||||
pasta: &'a Pasta,
|
||||
args: &'a Args,
|
||||
}
|
||||
|
||||
/// Endpoint to view a pasta.
|
||||
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().await;
|
||||
|
||||
let id = if ARGS.hash_ids {
|
||||
hashid_to_u64(&id).unwrap_or(0)
|
||||
} else {
|
||||
CONVERTER.to_u64(&id.into_inner()).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 == id {
|
||||
index = i;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
// increment 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: &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::NotFound()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate {
|
||||
status_code: StatusCode::NOT_FOUND,
|
||||
args: &ARGS
|
||||
}.render().unwrap())
|
||||
}
|
||||
|
||||
/// Endpoint for redirection.
|
||||
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().await;
|
||||
|
||||
let id = if ARGS.hash_ids {
|
||||
hashid_to_u64(&id).unwrap_or(0)
|
||||
} else {
|
||||
CONVERTER.to_u64(&id.into_inner()).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 == id {
|
||||
index = i;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
// increment 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 {
|
||||
HttpResponse::NotFound()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate {
|
||||
status_code: StatusCode::NOT_FOUND,
|
||||
args: &ARGS
|
||||
}.render().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise
|
||||
// send pasta not found error
|
||||
HttpResponse::NotFound()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate {
|
||||
status_code: StatusCode::NOT_FOUND,
|
||||
args: &ARGS
|
||||
}.render().unwrap())
|
||||
}
|
||||
|
||||
/// Endpoint to request pasta as raw file.
|
||||
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().await;
|
||||
|
||||
let id = if ARGS.hash_ids {
|
||||
hashid_to_u64(&id).unwrap_or(0)
|
||||
} else {
|
||||
CONVERTER.to_u64(&id.into_inner()).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 == id {
|
||||
index = i;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
// increment 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! :-(")
|
||||
}
|
37
src/endpoints/pastalist.rs
Normal file
37
src/endpoints/pastalist.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
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,
|
||||
}
|
||||
|
||||
/// The endpoint to view all currently registered pastas.
|
||||
#[get("/pastalist")]
|
||||
pub async fn list(data: web::Data<AppState>) -> HttpResponse {
|
||||
if ARGS.no_listing {
|
||||
return HttpResponse::Found()
|
||||
.append_header(("Location", format!("{}/", ARGS.public_path)))
|
||||
.finish();
|
||||
}
|
||||
|
||||
let mut pastas = data.pastas.lock().await;
|
||||
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
HttpResponse::Ok().content_type("text/html").body(
|
||||
PastaListTemplate {
|
||||
pastas: &pastas,
|
||||
args: &ARGS,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
73
src/endpoints/qr.rs
Normal file
73
src/endpoints/qr.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
use crate::args::{Args, ARGS};
|
||||
use crate::endpoints::errors::ErrorTemplate;
|
||||
use crate::pasta::Pasta;
|
||||
use crate::util::hashids::to_u64 as hashid_to_u64;
|
||||
use crate::util::misc::{self, remove_expired};
|
||||
use crate::AppState;
|
||||
use crate::util::pasta_id_converter::CONVERTER;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{get, web, HttpResponse};
|
||||
use askama::Template;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "qr.html", escape = "none")]
|
||||
struct QRTemplate<'a> {
|
||||
qr: &'a String,
|
||||
pasta: &'a Pasta,
|
||||
args: &'a Args,
|
||||
}
|
||||
|
||||
/// Endpoint to open a QR code to a pasta.
|
||||
#[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().await;
|
||||
|
||||
let u64_id = if ARGS.hash_ids {
|
||||
hashid_to_u64(&id).unwrap_or(0)
|
||||
} else {
|
||||
CONVERTER.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::NotFound()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate {
|
||||
status_code: StatusCode::NOT_FOUND,
|
||||
args: &ARGS
|
||||
}.render().unwrap())
|
||||
}
|
68
src/endpoints/remove.rs
Normal file
68
src/endpoints/remove.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use actix_web::http::StatusCode;
|
||||
use actix_web::{get, web, HttpResponse};
|
||||
|
||||
use crate::args::ARGS;
|
||||
use crate::endpoints::errors::ErrorTemplate;
|
||||
use crate::pasta::PastaFile;
|
||||
use crate::util::hashids::to_u64 as hashid_to_u64;
|
||||
use crate::util::misc::remove_expired;
|
||||
use crate::AppState;
|
||||
use crate::util::pasta_id_converter::CONVERTER;
|
||||
use askama::Template;
|
||||
use std::fs;
|
||||
|
||||
/// Endpoint to remove a pasta.
|
||||
#[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", format!("{}/", ARGS.public_path)))
|
||||
.finish();
|
||||
}
|
||||
|
||||
let mut pastas = data.pastas.lock().await;
|
||||
|
||||
let id = if ARGS.hash_ids {
|
||||
hashid_to_u64(&id).unwrap_or(0)
|
||||
} else {
|
||||
CONVERTER.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", format!("{}/pastalist", ARGS.public_path)))
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
HttpResponse::NotFound()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate {
|
||||
status_code: StatusCode::NOT_FOUND,
|
||||
args: &ARGS
|
||||
}.render().unwrap())
|
||||
}
|
21
src/endpoints/static_resources.rs
Normal file
21
src/endpoints/static_resources.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
use actix_web::{web, HttpResponse, Responder};
|
||||
use mime_guess::from_path;
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
#[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"),
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web::get("/static/{_:.*}")]
|
||||
async fn static_resources(path: web::Path<String>) -> impl Responder {
|
||||
handle_embedded_file(path.as_str())
|
||||
}
|
456
src/main.rs
456
src/main.rs
|
@ -1,353 +1,51 @@
|
|||
extern crate core;
|
||||
|
||||
use env_logger::Builder;
|
||||
use std::io::Write;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
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;
|
||||
|
||||
use crate::animalnumbers::{to_animal_names, to_u64};
|
||||
use crate::dbio::save_to_file;
|
||||
use crate::args::ARGS;
|
||||
use crate::endpoints::{
|
||||
create, edit, errors, info, pasta as pasta_endpoint, pastalist, qr, 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 futures::lock::Mutex;
|
||||
use log::LevelFilter;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
|
||||
mod animalnumbers;
|
||||
mod dbio;
|
||||
mod pasta;
|
||||
mod plugins;
|
||||
pub mod args;
|
||||
pub mod pasta;
|
||||
|
||||
lazy_static! {
|
||||
static ref ARGS: Args = Args::parse();
|
||||
pub mod util {
|
||||
pub mod pasta_id_converter;
|
||||
pub mod auth;
|
||||
pub mod dbio;
|
||||
pub mod hashids;
|
||||
pub mod misc;
|
||||
pub mod syntaxhighlighter;
|
||||
}
|
||||
|
||||
struct AppState {
|
||||
pastas: Mutex<Vec<Pasta>>,
|
||||
pub mod endpoints {
|
||||
pub mod create;
|
||||
pub mod edit;
|
||||
pub mod errors;
|
||||
pub mod info;
|
||||
pub mod pasta;
|
||||
pub mod pastalist;
|
||||
pub mod qr;
|
||||
pub mod remove;
|
||||
pub mod static_resources;
|
||||
}
|
||||
|
||||
#[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(),
|
||||
)
|
||||
pub struct AppState {
|
||||
pub pastas: Mutex<Vec<Pasta>>,
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let args: Args = Args::parse();
|
||||
|
||||
Builder::new()
|
||||
.format(|buf, record| {
|
||||
writeln!(
|
||||
|
@ -362,15 +60,16 @@ async fn main() -> std::io::Result<()> {
|
|||
.init();
|
||||
|
||||
log::info!(
|
||||
"MicroBin starting on http://127.0.0.1:{}",
|
||||
args.port.to_string()
|
||||
"MicroBin starting on http://{}:{}",
|
||||
ARGS.bind.to_string(),
|
||||
ARGS.port.to_string()
|
||||
);
|
||||
|
||||
match std::fs::create_dir_all("./pasta_data") {
|
||||
match fs::create_dir_all("./pasta_data/public") {
|
||||
Ok(dir) => dir,
|
||||
Err(error) => {
|
||||
log::error!("Couldn't create data directory ./pasta_data: {:?}", error);
|
||||
panic!("Couldn't create data directory ./pasta_data: {:?}", error);
|
||||
log::error!("Couldn't create data directory ./pasta_data/public/: {error:?}");
|
||||
panic!("Couldn't create data directory ./pasta_data/public/: {error:?}");
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -382,53 +81,40 @@ async fn main() -> std::io::Result<()> {
|
|||
App::new()
|
||||
.app_data(data.clone())
|
||||
.wrap(middleware::NormalizePath::trim())
|
||||
.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))
|
||||
.service(create::index)
|
||||
.service(info::info)
|
||||
.route(
|
||||
&format!("/{}/{{id}}", ARGS.pasta_endpoint),
|
||||
web::get().to(pasta_endpoint::getpasta)
|
||||
)
|
||||
.route(
|
||||
&format!("/{}/{{id}}", ARGS.raw_endpoint),
|
||||
web::get().to(pasta_endpoint::getrawpasta)
|
||||
)
|
||||
.route(
|
||||
&format!("/{}/{{id}}", ARGS.url_endpoint),
|
||||
web::get().to(pasta_endpoint::redirecturl)
|
||||
)
|
||||
//.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))
|
||||
.wrap(middleware::Logger::default())
|
||||
.service(remove)
|
||||
.service(list)
|
||||
.service(remove::remove)
|
||||
.service(pastalist::list)
|
||||
.wrap(Condition::new(
|
||||
args.auth_username.is_some(),
|
||||
HttpAuthentication::basic(auth_validator),
|
||||
ARGS.auth_username.is_some(),
|
||||
HttpAuthentication::basic(util::auth::auth_validator),
|
||||
))
|
||||
})
|
||||
.bind(format!("0.0.0.0:{}", args.port.to_string()))?
|
||||
.workers(args.threads as usize)
|
||||
.bind((ARGS.bind, ARGS.port))?
|
||||
.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()
|
||||
}
|
||||
|
|
133
src/pasta.rs
133
src/pasta.rs
|
@ -1,29 +1,74 @@
|
|||
use std::fmt;
|
||||
|
||||
use chrono::{DateTime, Datelike, NaiveDateTime, Timelike, Utc};
|
||||
use bytesize::ByteSize;
|
||||
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::to_animal_names;
|
||||
use crate::args::ARGS;
|
||||
use crate::util::hashids::to_hashids;
|
||||
use crate::util::pasta_id_converter::CONVERTER;
|
||||
use crate::util::syntaxhighlighter::html_highlight;
|
||||
|
||||
#[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
|
||||
}
|
||||
|
||||
/// Check if file is an image for embedding
|
||||
pub fn is_image(&self) -> bool {
|
||||
let guess = mime_guess::from_path(&self.name).first_or_text_plain();
|
||||
guess.type_() == "image"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Pasta {
|
||||
pub id: u64,
|
||||
pub content: String,
|
||||
pub file: String,
|
||||
pub file: Option<PastaFile>,
|
||||
pub extension: String,
|
||||
pub private: bool,
|
||||
pub editable: bool,
|
||||
pub created: i64,
|
||||
pub expiration: i64,
|
||||
pub last_read: i64,
|
||||
pub read_count: u64,
|
||||
pub burn_after_reads: u64,
|
||||
// what types can there be?
|
||||
// `url`, `text`,
|
||||
pub pasta_type: String,
|
||||
}
|
||||
|
||||
impl Pasta {
|
||||
pub fn id_as_animals(&self) -> String {
|
||||
to_animal_names(self.id)
|
||||
if ARGS.hash_ids {
|
||||
to_hashids(self.id)
|
||||
} else {
|
||||
CONVERTER.to_names(self.id)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn created_as_string(&self) -> String {
|
||||
let date = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(self.created, 0), Utc);
|
||||
let date = Local.timestamp(self.created, 0);
|
||||
format!(
|
||||
"{:02}-{:02} {}:{}",
|
||||
"{:02}-{:02} {:02}:{:02}",
|
||||
date.month(),
|
||||
date.day(),
|
||||
date.hour(),
|
||||
|
@ -35,10 +80,9 @@ impl Pasta {
|
|||
if self.expiration == 0 {
|
||||
String::from("Never")
|
||||
} else {
|
||||
let date =
|
||||
DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(self.expiration, 0), Utc);
|
||||
let date = Local.timestamp(self.expiration, 0);
|
||||
format!(
|
||||
"{:02}-{:02} {}:{}",
|
||||
"{:02}-{:02} {:02}:{:02}",
|
||||
date.month(),
|
||||
date.day(),
|
||||
date.hour(),
|
||||
|
@ -46,6 +90,73 @@ 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} days ago");
|
||||
};
|
||||
|
||||
// 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} hours ago");
|
||||
};
|
||||
|
||||
// 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} minutes ago");
|
||||
};
|
||||
|
||||
// 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} seconds ago");
|
||||
};
|
||||
|
||||
// it's less than 1 second?????
|
||||
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
|
||||
((timenow - self.last_read) / 86400) as u16
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
pub fn content_escaped(&self) -> String {
|
||||
self.content
|
||||
.replace('`', "\\`")
|
||||
.replace('$', "\\$")
|
||||
.replace('/', "\\/")
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Pasta {
|
||||
|
|
134
src/plugins.rs
134
src/plugins.rs
|
@ -1,134 +0,0 @@
|
|||
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
|
||||
}
|
29
src/util/auth.rs
Normal file
29
src/util/auth.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
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."))
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ use std::io::{BufReader, BufWriter};
|
|||
|
||||
use crate::Pasta;
|
||||
|
||||
static DATABASE_PATH: &'static str = "pasta_data/database.json";
|
||||
static DATABASE_PATH: &str = "pasta_data/database.json";
|
||||
|
||||
pub fn save_to_file(pasta_data: &Vec<Pasta>) {
|
||||
let mut file = File::create(DATABASE_PATH);
|
||||
|
@ -14,11 +14,11 @@ pub fn save_to_file(pasta_data: &Vec<Pasta>) {
|
|||
serde_json::to_writer(writer, &pasta_data).expect("Failed to create JSON writer");
|
||||
}
|
||||
Err(_) => {
|
||||
log::info!("Database file {} not found!", DATABASE_PATH);
|
||||
log::info!("Database file {DATABASE_PATH} not found!");
|
||||
file = File::create(DATABASE_PATH);
|
||||
match file {
|
||||
Ok(_) => {
|
||||
log::info!("Database file {} created.", DATABASE_PATH);
|
||||
log::info!("Database file {DATABASE_PATH} created.");
|
||||
save_to_file(pasta_data);
|
||||
}
|
||||
Err(err) => {
|
||||
|
@ -27,7 +27,7 @@ pub fn save_to_file(pasta_data: &Vec<Pasta>) {
|
|||
&DATABASE_PATH,
|
||||
&err
|
||||
);
|
||||
panic!("Failed to create database file {}: {}!", DATABASE_PATH, err)
|
||||
panic!("Failed to create database file {DATABASE_PATH}: {err}!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,14 +39,17 @@ pub fn load_from_file() -> io::Result<Vec<Pasta>> {
|
|||
match file {
|
||||
Ok(_) => {
|
||||
let reader = BufReader::new(file.unwrap());
|
||||
let data: Vec<Pasta> = serde_json::from_reader(reader).unwrap();
|
||||
let data: Vec<Pasta> = match serde_json::from_reader(reader) {
|
||||
Ok(t) => t,
|
||||
_ => Vec::new(),
|
||||
};
|
||||
Ok(data)
|
||||
}
|
||||
Err(_) => {
|
||||
log::info!("Database file {} not found!", DATABASE_PATH);
|
||||
log::info!("Database file {DATABASE_PATH} not found!");
|
||||
save_to_file(&Vec::<Pasta>::new());
|
||||
|
||||
log::info!("Database file {} created.", DATABASE_PATH);
|
||||
log::info!("Database file {DATABASE_PATH} created.");
|
||||
load_from_file()
|
||||
}
|
||||
}
|
18
src/util/hashids.rs
Normal file
18
src/util/hashids.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use harsh::Harsh;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref HARSH: Harsh = Harsh::builder().length(6).build().unwrap();
|
||||
}
|
||||
|
||||
pub fn to_hashids(number: u64) -> String {
|
||||
HARSH.encode(&[number])
|
||||
}
|
||||
|
||||
pub fn to_u64(hash_id: &str) -> Result<u64, &str> {
|
||||
let ids = HARSH
|
||||
.decode(hash_id)
|
||||
.map_err(|_e| "Failed to decode hash ID")?;
|
||||
let id = ids.first().ok_or("No ID found in hash ID")?;
|
||||
Ok(*id)
|
||||
}
|
66
src/util/misc.rs
Normal file
66
src/util/misc.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
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};
|
||||
|
||||
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| {
|
||||
// keep if:
|
||||
// expiration is `never` or not reached
|
||||
// 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 {
|
||||
// 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 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();
|
||||
spans[0].as_str() == url && Some(&LinkKind::Url) == spans[0].kind()
|
||||
}
|
98
src/util/pasta_id_converter.rs
Normal file
98
src/util/pasta_id_converter.rs
Normal file
|
@ -0,0 +1,98 @@
|
|||
use std::fs;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use crate::args::ARGS;
|
||||
|
||||
const ANIMAL_NAMES: &[&str] = &[
|
||||
"ant", "eel", "mole", "sloth", "ape", "emu", "monkey", "snail", "bat", "falcon", "mouse",
|
||||
"snake", "bear", "fish", "otter", "spider", "bee", "fly", "parrot", "squid", "bird", "fox",
|
||||
"panda", "swan", "bison", "frog", "pig", "tiger", "camel", "gecko", "pigeon", "toad", "cat",
|
||||
"goat", "pony", "turkey", "cobra", "goose", "pug", "turtle", "crow", "hawk", "rabbit", "viper",
|
||||
"deer", "horse", "rat", "wasp", "dog", "jaguar", "raven", "whale", "dove", "koala", "seal",
|
||||
"wolf", "duck", "lion", "shark", "worm", "eagle", "lizard", "sheep", "zebra",
|
||||
];
|
||||
|
||||
lazy_static!{
|
||||
pub static ref CONVERTER: PastaIdConverter = PastaIdConverter::new();
|
||||
}
|
||||
|
||||
/// Convert pasta IDs to names and vice versa
|
||||
pub struct PastaIdConverter {
|
||||
names: Vec<String>
|
||||
}
|
||||
|
||||
impl PastaIdConverter {
|
||||
pub fn new() -> Self {
|
||||
let names;
|
||||
if let Some(names_path) = &ARGS.custom_names {
|
||||
let names_data = fs::read_to_string(names_path)
|
||||
.expect("path for the names file should contain a names file");
|
||||
names = names_data
|
||||
.split('\n')
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<String>>();
|
||||
} else {
|
||||
names = ANIMAL_NAMES
|
||||
.iter()
|
||||
.copied()
|
||||
.map(ToOwned::to_owned)
|
||||
.collect();
|
||||
}
|
||||
|
||||
Self { names }
|
||||
}
|
||||
|
||||
pub fn to_names(&self, mut number: u64) -> String {
|
||||
let mut result: Vec<&str> = Vec::new();
|
||||
|
||||
if number == 0 {
|
||||
return self.names[0].parse().unwrap();
|
||||
}
|
||||
|
||||
let mut power = 6;
|
||||
|
||||
loop {
|
||||
let digit = number / self.names.len().pow(power) as u64;
|
||||
if !(result.is_empty() && digit == 0) {
|
||||
result.push(&self.names[digit as usize]);
|
||||
}
|
||||
number -= digit * self.names.len().pow(power) as u64;
|
||||
if power > 0 {
|
||||
power -= 1;
|
||||
} else if power == 0 || number == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result.join("-")
|
||||
}
|
||||
|
||||
pub fn to_u64(&self, pasta_id: &str) -> Result<u64, &str> {
|
||||
let mut result: u64 = 0;
|
||||
|
||||
let names: Vec<&str> = pasta_id.split('-').collect();
|
||||
|
||||
let mut pow = names.len();
|
||||
for name in names {
|
||||
pow -= 1;
|
||||
let name_index = self.names.iter().position(|r| r == name);
|
||||
match name_index {
|
||||
None => return Err("Failed to convert animal name to u64!"),
|
||||
Some(_) => {
|
||||
result += (name_index.unwrap() * self.names.len().pow(pow as u32)) as u64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PastaIdConverter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
|
37
src/util/syntaxhighlighter.rs
Normal file
37
src/util/syntaxhighlighter.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
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_else(|| 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>{line}</code-line>\n");
|
||||
}
|
||||
|
||||
// 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;\"");
|
||||
|
||||
highlighted_content2
|
||||
}
|
File diff suppressed because one or more lines are too long
81
templates/assets/favicon.svg
Normal file
81
templates/assets/favicon.svg
Normal file
|
@ -0,0 +1,81 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="31.999998"
|
||||
height="31.999998"
|
||||
viewBox="0 0 8.4666661 8.4666661"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:export-filename="logo.png"
|
||||
inkscape:export-xdpi="384"
|
||||
inkscape:export-ydpi="384"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#4a4a55"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="true"
|
||||
inkscape:zoom="19.556004"
|
||||
inkscape:cx="6.0339524"
|
||||
inkscape:cy="16.721207"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1036"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="44"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid182"
|
||||
visible="true" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<g
|
||||
id="g14438"
|
||||
inkscape:label="Box">
|
||||
<path
|
||||
style="color:#000000;fill:#f7f7ff;-inkscape-stroke:none"
|
||||
d="M 1.984375,3.8066406 V 6.2207031 L 4.2324219,7.34375 4.2929687,7.3144531 6.4824219,6.2207031 V 3.8066406 l -2.25,1.125 z m 0.2636719,0.4296875 1.984375,0.9921875 1.984375,-0.9921875 V 6.0566406 L 4.2324219,7.0488281 2.2480469,6.0566406 Z"
|
||||
id="path2056" />
|
||||
<path
|
||||
id="path3512"
|
||||
style="color:#000000;fill:#f7f7ff;fill-opacity:1;-inkscape-stroke:none"
|
||||
d="M 4.3651082,5.3087199 4.2322998,5.3748657 4.1015584,5.3097534 v 1.5260051 l 0.1328085,0.066663 0.1307413,-0.065629 z" />
|
||||
</g>
|
||||
<g
|
||||
id="g18197"
|
||||
inkscape:label="Cat"
|
||||
style="display:inline">
|
||||
<path
|
||||
id="path14440"
|
||||
style="color:#000000;display:inline;fill:#f7f7ff;fill-opacity:1;-inkscape-stroke:none"
|
||||
d="M 4.6663818,2.8716593 4.3862956,3.0959351 h 0.045475 L 5.4063883,3.291272 5.709729,4.0462646 5.9464071,3.9279256 5.5996582,3.058728 Z M 2.6199951,2.9434896 2.4618652,3.8989868 2.7088786,4.0230103 2.8571899,3.1331421 Z" />
|
||||
<path
|
||||
style="color:#000000;fill:#f7f7ff;-inkscape-stroke:none"
|
||||
d="m 2.1308594,1.0234375 0.2773437,1.109375 v 0.4707031 l 1.1894532,0.953125 0.083984,-0.066406 1.1074219,-0.8867188 V 2.1328125 L 5.0664062,1.0234375 3.9902344,1.5605469 H 3.2070313 Z M 2.5273438,1.5175781 3.1445313,1.8261719 H 4.0527344 L 4.6699219,1.5175781 4.5234375,2.0996094 V 2.4765625 L 3.5976563,3.2167969 2.671875,2.4765625 V 2.0996094 Z"
|
||||
id="path14903" />
|
||||
</g>
|
||||
<path
|
||||
id="path22977"
|
||||
style="color:#000000;fill:#f7f7ff;fill-opacity:1;-inkscape-stroke:none"
|
||||
d="M 5.94434,3.5646403 6.0657796,3.8684977 6.3086589,3.7470581 Z m -3.5723918,0.074414 -0.2149739,0.1074869 0.181901,0.090951 z"
|
||||
sodipodi:nodetypes="cccccccc" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.3 KiB |
BIN
templates/assets/logo.png
Normal file
BIN
templates/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
81
templates/assets/logo.svg
Normal file
81
templates/assets/logo.svg
Normal file
|
@ -0,0 +1,81 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 8.4666666 8.4666666"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
sodipodi:docname="microbin-logo.svg"
|
||||
inkscape:export-filename="microbin-logo-exp.svg"
|
||||
inkscape:export-xdpi="144"
|
||||
inkscape:export-ydpi="144"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#4a4a55"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="true"
|
||||
inkscape:zoom="19.556004"
|
||||
inkscape:cx="6.0339524"
|
||||
inkscape:cy="16.721207"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1036"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="44"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid182"
|
||||
visible="true" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<g
|
||||
id="g14438"
|
||||
inkscape:label="Box">
|
||||
<path
|
||||
style="color:#000000;fill:#f7f7ff;-inkscape-stroke:none"
|
||||
d="M 1.984375,3.8066406 V 6.2207031 L 4.2324219,7.34375 4.2929687,7.3144531 6.4824219,6.2207031 V 3.8066406 l -2.25,1.125 z m 0.2636719,0.4296875 1.984375,0.9921875 1.984375,-0.9921875 V 6.0566406 L 4.2324219,7.0488281 2.2480469,6.0566406 Z"
|
||||
id="path2056" />
|
||||
<path
|
||||
id="path3512"
|
||||
style="color:#000000;fill:#f7f7ff;-inkscape-stroke:none;fill-opacity:1"
|
||||
d="M 4.3651082 5.3087199 L 4.2322998 5.3748657 L 4.1015584 5.3097534 L 4.1015584 6.8357585 L 4.2343669 6.9024211 L 4.3651082 6.836792 L 4.3651082 5.3087199 z " />
|
||||
</g>
|
||||
<g
|
||||
id="g18197"
|
||||
inkscape:label="Cat"
|
||||
style="display:inline">
|
||||
<path
|
||||
id="path14440"
|
||||
style="color:#000000;display:inline;fill:#f7f7ff;-inkscape-stroke:none;fill-opacity:1"
|
||||
d="M 4.6663818,2.8716593 4.3862956,3.0959351 h 0.045475 L 5.4063883,3.291272 5.709729,4.0462646 5.9464071,3.9279256 5.5996582,3.058728 Z M 2.6199951,2.9434896 2.4618652,3.8989868 2.7088786,4.0230103 2.8571899,3.1331421 Z" />
|
||||
<path
|
||||
style="color:#000000;fill:#f7f7ff;-inkscape-stroke:none"
|
||||
d="m 2.1308594,1.0234375 0.2773437,1.109375 v 0.4707031 l 1.1894532,0.953125 0.083984,-0.066406 1.1074219,-0.8867188 V 2.1328125 L 5.0664062,1.0234375 3.9902344,1.5605469 H 3.2070313 Z M 2.5273438,1.5175781 3.1445313,1.8261719 H 4.0527344 L 4.6699219,1.5175781 4.5234375,2.0996094 V 2.4765625 L 3.5976563,3.2167969 2.671875,2.4765625 V 2.0996094 Z"
|
||||
id="path14903" />
|
||||
</g>
|
||||
<path
|
||||
id="path22977"
|
||||
style="color:#000000;fill:#f7f7ff;-inkscape-stroke:none;fill-opacity:1"
|
||||
d="M 5.94434,3.5646403 6.0657796,3.8684977 6.3086589,3.7470581 Z m -3.5723918,0.074414 -0.2149739,0.1074869 0.181901,0.090951 z"
|
||||
sodipodi:nodetypes="cccccccc" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.3 KiB |
626
templates/assets/water.css
Normal file
626
templates/assets/water.css
Normal file
|
@ -0,0 +1,626 @@
|
|||
/*
|
||||
* This is (basically) water.css.
|
||||
*
|
||||
* repo: https://github.com/kognise/water.css
|
||||
*
|
||||
* The license:
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright © 2019 Kognise
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the “Software”), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
:root {
|
||||
--background-body:#4a4a55;
|
||||
--background:#383844;
|
||||
--background-alt:#242438;
|
||||
--selection:#23bf7c;
|
||||
--text-main:#dfdfef;
|
||||
--text-bright:#f7f7ff;
|
||||
--text-muted:#878797;
|
||||
--links:#28db8f;
|
||||
--focus:#299465df;
|
||||
--border:#676773;
|
||||
--code:var(--text-main);
|
||||
--animation-duration:0.1s;
|
||||
--button-base:#299465;
|
||||
--button-hover:#23bf7c;
|
||||
--scrollbar-thumb:var(--button-hover);
|
||||
--scrollbar-thumb-hover:#000;
|
||||
--form-placeholder:#a9a9a9;
|
||||
--form-text:#fff;
|
||||
--variable:#d941e2;
|
||||
--highlight:#efdb43;
|
||||
--select-arrow:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='63' width='117' fill='%23efefef'%3E%3Cpath d='M115 2c-1-2-4-2-5 0L59 53 7 2a4 4 0 00-5 5l54 54 2 2 3-2 54-54c2-1 2-4 0-5z'/%3E%3C/svg%3E")
|
||||
}
|
||||
html {
|
||||
scrollbar-color:#040a0f #202b38;
|
||||
scrollbar-color:var(--scrollbar-thumb) var(--background-body);
|
||||
scrollbar-width:thin
|
||||
}
|
||||
body {
|
||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,Segoe UI Emoji,Apple Color Emoji,Noto Color Emoji,sans-serif;
|
||||
line-height:1.4;
|
||||
max-width:800px;
|
||||
margin:20px auto;
|
||||
padding:0 10px;
|
||||
word-wrap:break-word;
|
||||
color:#dbdbdb;
|
||||
color:var(--text-main);
|
||||
background:#202b38;
|
||||
background:var(--background-body);
|
||||
text-rendering:optimizeLegibility
|
||||
}
|
||||
button,
|
||||
input,
|
||||
textarea {
|
||||
transition:background-color .1s linear,border-color .1s linear,color .1s linear,box-shadow .1s linear,transform .1s ease;
|
||||
transition:background-color var(--animation-duration) linear,border-color var(--animation-duration) linear,color var(--animation-duration) linear,box-shadow var(--animation-duration) linear,transform var(--animation-duration) ease
|
||||
}
|
||||
h1 {
|
||||
font-size:2.2em;
|
||||
margin-top:0
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-bottom:12px;
|
||||
margin-top:24px
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
strong {
|
||||
color:#fff;
|
||||
color:var(--text-bright)
|
||||
}
|
||||
b,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
strong,
|
||||
th {
|
||||
font-weight:600
|
||||
}
|
||||
q:after,
|
||||
q:before {
|
||||
content:none
|
||||
}
|
||||
blockquote,
|
||||
q {
|
||||
border-left:4px solid rgba(0,150,191,.67);
|
||||
border-left:4px solid var(--focus);
|
||||
margin:1.5em 0;
|
||||
padding:.5em 1em;
|
||||
font-style:italic
|
||||
}
|
||||
blockquote>footer {
|
||||
font-style:normal;
|
||||
border:0
|
||||
}
|
||||
address,
|
||||
blockquote cite {
|
||||
font-style:normal
|
||||
}
|
||||
a[href^=mailto\:]:before {
|
||||
content:"📧 "
|
||||
}
|
||||
a[href^=tel\:]:before {
|
||||
content:"📞 "
|
||||
}
|
||||
a[href^=sms\:]:before {
|
||||
content:"💬 "
|
||||
}
|
||||
mark {
|
||||
background-color:#efdb43;
|
||||
background-color:var(--highlight);
|
||||
border-radius:2px;
|
||||
padding:0 2px;
|
||||
color:#000
|
||||
}
|
||||
a>code,
|
||||
a>strong {
|
||||
color:inherit
|
||||
}
|
||||
button,
|
||||
input[type=button],
|
||||
input[type=checkbox],
|
||||
input[type=radio],
|
||||
input[type=range],
|
||||
input[type=reset],
|
||||
input[type=submit],
|
||||
select {
|
||||
cursor:pointer
|
||||
}
|
||||
input,
|
||||
select {
|
||||
display:block
|
||||
}
|
||||
[type=checkbox],
|
||||
[type=radio] {
|
||||
display:initial
|
||||
}
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
color:#fff;
|
||||
color:var(--form-text);
|
||||
background-color:#161f27;
|
||||
background-color:var(--background);
|
||||
font-family:inherit;
|
||||
font-size:inherit;
|
||||
margin-right:6px;
|
||||
margin-bottom:6px;
|
||||
padding:10px;
|
||||
border:none;
|
||||
border-radius:6px;
|
||||
outline:none
|
||||
}
|
||||
button,
|
||||
input[type=button],
|
||||
input[type=reset],
|
||||
input[type=submit] {
|
||||
background-color:#0c151c;
|
||||
background-color:var(--button-base);
|
||||
padding-right:30px;
|
||||
padding-left:30px
|
||||
}
|
||||
button:hover,
|
||||
input[type=button]:hover,
|
||||
input[type=reset]:hover,
|
||||
input[type=submit]:hover {
|
||||
background:#040a0f;
|
||||
background:var(--button-hover)
|
||||
}
|
||||
input[type=color] {
|
||||
min-height:2rem;
|
||||
padding:8px;
|
||||
cursor:pointer
|
||||
}
|
||||
input[type=checkbox],
|
||||
input[type=radio] {
|
||||
height:1em;
|
||||
width:1em
|
||||
}
|
||||
input[type=radio] {
|
||||
border-radius:100%
|
||||
}
|
||||
input {
|
||||
vertical-align:top
|
||||
}
|
||||
label {
|
||||
vertical-align:middle;
|
||||
margin-bottom:4px;
|
||||
display:inline-block
|
||||
}
|
||||
button,
|
||||
input:not([type=checkbox]):not([type=radio]),
|
||||
input[type=range],
|
||||
select,
|
||||
textarea {
|
||||
-webkit-appearance:none
|
||||
}
|
||||
textarea {
|
||||
display:block;
|
||||
margin-right:0;
|
||||
box-sizing:border-box;
|
||||
resize:vertical
|
||||
}
|
||||
textarea:not([cols]) {
|
||||
width:100%
|
||||
}
|
||||
textarea:not([rows]) {
|
||||
min-height:40px;
|
||||
height:140px
|
||||
}
|
||||
select {
|
||||
background:#161f27 url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='63' width='117' fill='%23efefef'%3E%3Cpath d='M115 2c-1-2-4-2-5 0L59 53 7 2a4 4 0 00-5 5l54 54 2 2 3-2 54-54c2-1 2-4 0-5z'/%3E%3C/svg%3E") calc(100% - 12px) 50%/12px no-repeat;
|
||||
background:var(--background) var(--select-arrow) calc(100% - 12px) 50%/12px no-repeat;
|
||||
padding-right:35px
|
||||
}
|
||||
select::-ms-expand {
|
||||
display:none
|
||||
}
|
||||
select[multiple] {
|
||||
padding-right:10px;
|
||||
background-image:none;
|
||||
overflow-y:auto
|
||||
}
|
||||
button:focus,
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
box-shadow:0 0 0 2px rgba(0,150,191,.67);
|
||||
box-shadow:0 0 0 2px var(--focus)
|
||||
}
|
||||
button:active,
|
||||
input[type=button]:active,
|
||||
input[type=checkbox]:active,
|
||||
input[type=radio]:active,
|
||||
input[type=range]:active,
|
||||
input[type=reset]:active,
|
||||
input[type=submit]:active {
|
||||
transform:translateY(2px)
|
||||
}
|
||||
button:disabled,
|
||||
input:disabled,
|
||||
select:disabled,
|
||||
textarea:disabled {
|
||||
cursor:not-allowed;
|
||||
opacity:.5
|
||||
}
|
||||
::-moz-placeholder {
|
||||
color:#a9a9a9;
|
||||
color:var(--form-placeholder)
|
||||
}
|
||||
:-ms-input-placeholder {
|
||||
color:#a9a9a9;
|
||||
color:var(--form-placeholder)
|
||||
}
|
||||
::-ms-input-placeholder {
|
||||
color:#a9a9a9;
|
||||
color:var(--form-placeholder)
|
||||
}
|
||||
::placeholder {
|
||||
color:#a9a9a9;
|
||||
color:var(--form-placeholder)
|
||||
}
|
||||
fieldset {
|
||||
border:1px solid rgba(0,150,191,.67);
|
||||
border:1px solid var(--focus);
|
||||
border-radius:6px;
|
||||
margin:0 0 12px;
|
||||
padding:10px
|
||||
}
|
||||
legend {
|
||||
font-size:.9em;
|
||||
font-weight:600
|
||||
}
|
||||
input[type=range] {
|
||||
margin:10px 0;
|
||||
padding:10px 0;
|
||||
background:transparent
|
||||
}
|
||||
input[type=range]:focus {
|
||||
outline:none
|
||||
}
|
||||
input[type=range]::-webkit-slider-runnable-track {
|
||||
width:100%;
|
||||
height:9.5px;
|
||||
-webkit-transition:.2s;
|
||||
transition:.2s;
|
||||
background:#161f27;
|
||||
background:var(--background);
|
||||
border-radius:3px
|
||||
}
|
||||
input[type=range]::-webkit-slider-thumb {
|
||||
box-shadow:0 1px 1px #000,0 0 1px #0d0d0d;
|
||||
height:20px;
|
||||
width:20px;
|
||||
border-radius:50%;
|
||||
background:#526980;
|
||||
background:var(--border);
|
||||
-webkit-appearance:none;
|
||||
margin-top:-7px
|
||||
}
|
||||
input[type=range]:focus::-webkit-slider-runnable-track {
|
||||
background:#161f27;
|
||||
background:var(--background)
|
||||
}
|
||||
input[type=range]::-moz-range-track {
|
||||
width:100%;
|
||||
height:9.5px;
|
||||
-moz-transition:.2s;
|
||||
transition:.2s;
|
||||
background:#161f27;
|
||||
background:var(--background);
|
||||
border-radius:3px
|
||||
}
|
||||
input[type=range]::-moz-range-thumb {
|
||||
box-shadow:1px 1px 1px #000,0 0 1px #0d0d0d;
|
||||
height:20px;
|
||||
width:20px;
|
||||
border-radius:50%;
|
||||
background:#526980;
|
||||
background:var(--border)
|
||||
}
|
||||
input[type=range]::-ms-track {
|
||||
width:100%;
|
||||
height:9.5px;
|
||||
background:transparent;
|
||||
border-color:transparent;
|
||||
border-width:16px 0;
|
||||
color:transparent
|
||||
}
|
||||
input[type=range]::-ms-fill-lower,
|
||||
input[type=range]::-ms-fill-upper {
|
||||
background:#161f27;
|
||||
background:var(--background);
|
||||
border:.2px solid #010101;
|
||||
border-radius:3px;
|
||||
box-shadow:1px 1px 1px #000,0 0 1px #0d0d0d
|
||||
}
|
||||
input[type=range]::-ms-thumb {
|
||||
box-shadow:1px 1px 1px #000,0 0 1px #0d0d0d;
|
||||
border:1px solid #000;
|
||||
height:20px;
|
||||
width:20px;
|
||||
border-radius:50%;
|
||||
background:#526980;
|
||||
background:var(--border)
|
||||
}
|
||||
input[type=range]:focus::-ms-fill-lower,
|
||||
input[type=range]:focus::-ms-fill-upper {
|
||||
background:#161f27;
|
||||
background:var(--background)
|
||||
}
|
||||
a {
|
||||
text-decoration:none;
|
||||
color:#41adff;
|
||||
color:var(--links)
|
||||
}
|
||||
a:hover {
|
||||
text-decoration:underline
|
||||
}
|
||||
code,
|
||||
samp,
|
||||
time {
|
||||
background:#161f27;
|
||||
background:var(--background);
|
||||
color:#ffbe85;
|
||||
color:var(--code);
|
||||
padding:2.5px 5px;
|
||||
border-radius:6px;
|
||||
font-size:1em
|
||||
}
|
||||
pre>code {
|
||||
padding:10px;
|
||||
display:block;
|
||||
overflow-x:auto
|
||||
}
|
||||
var {
|
||||
color:#d941e2;
|
||||
color:var(--variable);
|
||||
font-style:normal;
|
||||
font-family:monospace
|
||||
}
|
||||
kbd {
|
||||
background:#161f27;
|
||||
background:var(--background);
|
||||
border:1px solid #526980;
|
||||
border:1px solid var(--border);
|
||||
border-radius:2px;
|
||||
color:#dbdbdb;
|
||||
color:var(--text-main);
|
||||
padding:2px 4px
|
||||
}
|
||||
img,
|
||||
video {
|
||||
max-width:100%;
|
||||
height:auto
|
||||
}
|
||||
hr {
|
||||
border:none;
|
||||
border-top:1px solid #526980;
|
||||
border-top:1px solid var(--border)
|
||||
}
|
||||
table {
|
||||
border-collapse:collapse;
|
||||
margin-bottom:10px;
|
||||
width:100%;
|
||||
table-layout:fixed
|
||||
}
|
||||
table caption,
|
||||
td,
|
||||
th {
|
||||
text-align:left
|
||||
}
|
||||
td,
|
||||
th {
|
||||
padding:6px;
|
||||
vertical-align:top;
|
||||
word-wrap:break-word
|
||||
}
|
||||
thead {
|
||||
border-bottom:1px solid #526980;
|
||||
border-bottom:1px solid var(--border)
|
||||
}
|
||||
tfoot {
|
||||
border-top:1px solid #526980;
|
||||
border-top:1px solid var(--border)
|
||||
}
|
||||
tbody tr:nth-child(2n) {
|
||||
background-color:#161f27;
|
||||
background-color:var(--background)
|
||||
}
|
||||
tbody tr:nth-child(2n) button {
|
||||
background-color:#1a242f;
|
||||
background-color:var(--background-alt)
|
||||
}
|
||||
tbody tr:nth-child(2n) button:hover {
|
||||
background-color:#202b38;
|
||||
background-color:var(--background-body)
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
height:10px;
|
||||
width:10px
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background:#161f27;
|
||||
background:var(--background);
|
||||
border-radius:6px
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background:#040a0f;
|
||||
background:var(--scrollbar-thumb);
|
||||
border-radius:6px
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background:#000;
|
||||
background:var(--scrollbar-thumb-hover)
|
||||
}
|
||||
::-moz-selection {
|
||||
background-color:#1c76c5;
|
||||
background-color:var(--selection);
|
||||
color:#fff;
|
||||
color:var(--text-bright)
|
||||
}
|
||||
::selection {
|
||||
background-color:#1c76c5;
|
||||
background-color:var(--selection);
|
||||
color:#fff;
|
||||
color:var(--text-bright)
|
||||
}
|
||||
details {
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
align-items:flex-start;
|
||||
background-color:#1a242f;
|
||||
background-color:var(--background-alt);
|
||||
padding:10px 10px 0;
|
||||
margin:1em 0;
|
||||
border-radius:6px;
|
||||
overflow:hidden
|
||||
}
|
||||
details[open] {
|
||||
padding:10px
|
||||
}
|
||||
details>:last-child {
|
||||
margin-bottom:0
|
||||
}
|
||||
details[open] summary {
|
||||
margin-bottom:10px
|
||||
}
|
||||
summary {
|
||||
display:list-item;
|
||||
background-color:#161f27;
|
||||
background-color:var(--background);
|
||||
padding:10px;
|
||||
margin:-10px -10px 0;
|
||||
cursor:pointer;
|
||||
outline:none
|
||||
}
|
||||
summary:focus,
|
||||
summary:hover {
|
||||
text-decoration:underline
|
||||
}
|
||||
details>:not(summary) {
|
||||
margin-top:0
|
||||
}
|
||||
summary::-webkit-details-marker {
|
||||
color:#dbdbdb;
|
||||
color:var(--text-main)
|
||||
}
|
||||
dialog {
|
||||
background-color:#1a242f;
|
||||
background-color:var(--background-alt);
|
||||
color:#dbdbdb;
|
||||
color:var(--text-main);
|
||||
border-radius:6px;
|
||||
border:#526980;
|
||||
border-color:var(--border);
|
||||
padding:10px 30px
|
||||
}
|
||||
dialog>header:first-child {
|
||||
background-color:#161f27;
|
||||
background-color:var(--background);
|
||||
border-radius:6px 6px 0 0;
|
||||
margin:-10px -30px 10px;
|
||||
padding:10px;
|
||||
text-align:center
|
||||
}
|
||||
dialog::-webkit-backdrop {
|
||||
background:rgba(0,0,0,.61);
|
||||
-webkit-backdrop-filter:blur(4px);
|
||||
backdrop-filter:blur(4px)
|
||||
}
|
||||
dialog::backdrop {
|
||||
background:rgba(0,0,0,.61);
|
||||
-webkit-backdrop-filter:blur(4px);
|
||||
backdrop-filter:blur(4px)
|
||||
}
|
||||
footer {
|
||||
border-top:1px solid #526980;
|
||||
border-top:1px solid var(--border);
|
||||
padding-top:10px;
|
||||
color:#a9b1ba;
|
||||
color:var(--text-muted)
|
||||
}
|
||||
body>footer {
|
||||
margin-top:40px
|
||||
}
|
||||
@media print {
|
||||
body,
|
||||
button,
|
||||
code,
|
||||
details,
|
||||
input,
|
||||
pre,
|
||||
summary,
|
||||
textarea {
|
||||
background-color:#fff
|
||||
}
|
||||
button,
|
||||
input,
|
||||
textarea {
|
||||
border:1px solid #000
|
||||
}
|
||||
body,
|
||||
button,
|
||||
code,
|
||||
footer,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
input,
|
||||
pre,
|
||||
strong,
|
||||
summary,
|
||||
textarea {
|
||||
color:#000
|
||||
}
|
||||
summary::marker {
|
||||
color:#000
|
||||
}
|
||||
summary::-webkit-details-marker {
|
||||
color:#000
|
||||
}
|
||||
tbody tr:nth-child(2n) {
|
||||
background-color:#f2f2f2
|
||||
}
|
||||
a {
|
||||
color:#00f;
|
||||
text-decoration:underline
|
||||
}
|
||||
}
|
||||
|
20
templates/edit.html
Normal file
20
templates/edit.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% 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" %}
|
|
@ -1,10 +1,10 @@
|
|||
{% include "header.html" %}
|
||||
<br>
|
||||
<h2>404</h2>
|
||||
<b>Not Found</b>
|
||||
<h2>{{ status_code.as_u16() }}</h2>
|
||||
<b>{{ status_code.canonical_reason().unwrap_or("Unknown error") }}</b>
|
||||
<br>
|
||||
<br>
|
||||
<a href="/" > Go Home</a>
|
||||
<a href="{{ args.public_path }}/"> Go Home</a>
|
||||
<br>
|
||||
<br>
|
||||
{% include "footer.html" %}
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
|
||||
{% if !args.hide_footer %}
|
||||
|
||||
<hr>
|
||||
<p style="font-size: smaller">
|
||||
MicroBin by Daniel Szabo. Fork me on <a href="https://github.com/szabodanika/microbin">GitHub</a>!
|
||||
{% if args.footer_text.as_ref().is_none() %}
|
||||
<b>Karton</b> by Schrottkatze, based on <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() }}
|
||||
{%- endif %}
|
||||
</p>
|
||||
|
||||
{%- endif %}
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
|
|
|
@ -1,34 +1,64 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>MicroBin</title>
|
||||
<title>{{ args.title }}</title>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" type="image/svg+xml" href="{{ args.public_path }}/static/favicon.svg">
|
||||
{% if !args.pure_html %}
|
||||
<link rel="stylesheet" href="/static/water.css">
|
||||
{% 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: 720px;
|
||||
max-width: 1080px;
|
||||
margin: auto;
|
||||
padding-left:0.5rem;
|
||||
padding-right:0.5rem;
|
||||
line-height: 1.5;
|
||||
font-size: 1.1em;
|
||||
">
|
||||
{%- else %}
|
||||
|
||||
<body style="
|
||||
max-width: 800px;
|
||||
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">
|
||||
<i><span style="font-size:2.2rem; margin-right:1rem">μ</span></i> MicroBin
|
||||
|
||||
{% if !args.hide_logo %}
|
||||
<a href="{{ args.public_path }}/"><img
|
||||
width=48
|
||||
style="margin-bottom: -12px;"
|
||||
src="{{ args.public_path }}/static/logo.png"
|
||||
></a>
|
||||
{%- endif %}
|
||||
{{ args.title }}
|
||||
</b>
|
||||
|
||||
<a href="/" 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="/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="https://github.com/szabodanika/microbin" style="margin-right: 0.5rem; margin-left: 0.5rem">GitHub</a>
|
||||
<a href="{{ args.public_path }}/info" style="margin-right: 0.5rem; margin-left: 0.5rem">Info</a>
|
||||
|
||||
<hr>
|
||||
|
||||
|
|
|
@ -1,27 +1,252 @@
|
|||
{% 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 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 == "3days" %}
|
||||
<option selected value="3days">
|
||||
{%- else %}
|
||||
<option value="3days">
|
||||
{%- endif %}
|
||||
3 days
|
||||
</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>
|
||||
<br>
|
||||
</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</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>
|
||||
{% if args.editable || args.private %}
|
||||
<label>Other</label>
|
||||
{%- endif %}
|
||||
{% 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>
|
||||
</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>
|
||||
<textarea style="width: 100%; min-height: 100px" name="content" autofocus></textarea>
|
||||
<br>
|
||||
<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>
|
||||
<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>-->
|
||||
<input style="width: 140px; float: right" disabled type="submit"
|
||||
value="Read Only" /></b>
|
||||
{%- else %}
|
||||
<b>
|
||||
<!--<input style="width: 140px; float: right; background-color: #0076d18f;" type="submit" value="Save" />-->
|
||||
<input style="width: 140px; float: right" 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
42
templates/info.html
Normal 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://gitlab.com/obsidianical/microbin" style="margin-right: 1rem">Source Code</a>
|
||||
<br>
|
||||
<a href="https://gitlab.com/obsidianical/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" %}
|
|
@ -1,9 +1,149 @@
|
|||
{% include "header.html" %}
|
||||
<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>
|
||||
<div style="float: left">
|
||||
{% 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: 0.5rem; margin-left: 0.5rem" href="/remove/{{pasta.id_as_animals()}}">Remove</a>
|
||||
<pre><code>{{pasta}}</code></pre>
|
||||
<a style="margin-right: 1rem" href="{{ args.public_path }}/{{ args.raw_endpoint }}/{{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: 1rem" href="{{ args.public_path }}/edit/{{pasta.id_as_animals()}}">Edit</a>
|
||||
{%- endif %}
|
||||
<a style="margin-right: 1rem" href="{{ args.public_path }}/remove/{{pasta.id_as_animals()}}">Remove</a>
|
||||
</div>
|
||||
<div style="float: right">
|
||||
<a style="margin-right: 0.5rem"
|
||||
href="{{ args.public_path }}/{{ args.pasta_endpoint }}/{{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>
|
||||
<br>
|
||||
{% if pasta.file.as_ref().unwrap().is_image() %}
|
||||
<img src="{{ args.public_path }}/file/{{pasta.id_as_animals()}}/{{pasta.file.as_ref().unwrap().name()}}" alt="">
|
||||
<br>
|
||||
{%- endif %}
|
||||
<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>
|
||||
{%- else %}
|
||||
<pre><code id="code">{{pasta.content_not_highlighted()}}</code></pre>
|
||||
{%- 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 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 }}/{{ args.pasta_endpoint }}/{{pasta.id_as_animals()}}`
|
||||
const redirect_url = `{{ args.public_path }}/{{ args.url_endpoint }}/{{pasta.id_as_animals()}}`
|
||||
|
||||
copyURLBtn.addEventListener("click", () => {
|
||||
navigator.clipboard.writeText(url)
|
||||
copyURLBtn.innerHTML = "Copied"
|
||||
setTimeout(() => {
|
||||
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)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
code-line {
|
||||
counter-increment: listing;
|
||||
text-align: right;
|
||||
float: left;
|
||||
clear: left;
|
||||
}
|
||||
|
||||
code-line::before {
|
||||
content: counter(listing);
|
||||
display: inline-block;
|
||||
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;
|
||||
}
|
||||
|
||||
#code {
|
||||
min-height: 2rem;
|
||||
}
|
||||
|
||||
.code-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
font-size: small;
|
||||
padding: 4px;
|
||||
padding-left: 0.8rem;
|
||||
padding-right: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% include "footer.html" %}
|
||||
|
|
|
@ -4,37 +4,35 @@
|
|||
{% if pastas.is_empty() %}
|
||||
<br>
|
||||
<p>
|
||||
No pastas yet. 😔 Create one <a href="/">here</a>.
|
||||
No pastas yet. 😔 Create one <a href="{{ args.public_path }}/">here</a>.
|
||||
</p>
|
||||
<br>
|
||||
{%- else %}
|
||||
<br>
|
||||
<h3>Pastas</h3>
|
||||
{% if args.pure_html %}
|
||||
<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 %}
|
||||
{% if pasta.pasta_type == "text" %}
|
||||
{% if pasta.pasta_type == "text" && !pasta.private %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/pasta/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
|
||||
<a href="{{ args.public_path }}/{{ args.pasta_endpoint }}/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.created_as_string()}}
|
||||
|
@ -43,11 +41,15 @@
|
|||
{{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>
|
||||
<a style="margin-right:1rem" href="{{ args.public_path }}/{{ args.raw_endpoint }}/{{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>
|
||||
{%- endif %}
|
||||
<a href="/remove/{{pasta.id_as_animals()}}">Remove</a>
|
||||
{% if pasta.editable %}
|
||||
<a style="margin-right:1rem" href="{{ args.public_path }}/edit/{{pasta.id_as_animals()}}">Edit</a>
|
||||
{%- endif %}
|
||||
<a href="{{ args.public_path }}/remove/{{pasta.id_as_animals()}}">Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
{%- endif %}
|
||||
|
@ -55,31 +57,30 @@
|
|||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
<table>
|
||||
<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" %}
|
||||
{% if pasta.pasta_type == "url" && !pasta.private %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/url/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
|
||||
<a href="{{ args.public_path }}/{{ args.pasta_endpoint }}/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.created_as_string()}}
|
||||
|
@ -88,8 +89,13 @@
|
|||
{{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>
|
||||
<a style="margin-right:1rem" href="{{ args.public_path }}/{{ args.url_endpoint }}/{{pasta.id_as_animals()}}">Open</a>
|
||||
<a style="margin-right:1rem; cursor: pointer;" id="copy-button"
|
||||
data-url="{{ args.public_path }}/{{ args.url_endpoint }}/{{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 %}
|
||||
<a href="{{ args.public_path }}/remove/{{pasta.id_as_animals()}}">Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
{%- endif %}
|
||||
|
@ -98,4 +104,17 @@
|
|||
</table>
|
||||
<br>
|
||||
{%- endif %}
|
||||
|
||||
<script>
|
||||
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
29
templates/qr.html
Normal 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" %}
|
Loading…
Reference in a new issue