Compare commits
No commits in common. "master" and "v0.1" have entirely different histories.
54 changed files with 451 additions and 6192 deletions
1
.envrc
1
.envrc
|
@ -1 +0,0 @@
|
||||||
use flake
|
|
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
|
@ -1,4 +0,0 @@
|
||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: szabodanika
|
|
||||||
ko_fi: dani_sz
|
|
BIN
.github/index.png
vendored
BIN
.github/index.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 1.5 MiB |
BIN
.github/logo.png
vendored
BIN
.github/logo.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 33 KiB |
13
.github/workflows/build_nix.yml
vendored
13
.github/workflows/build_nix.yml
vendored
|
@ -1,13 +0,0 @@
|
||||||
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
192
.github/workflows/docker.yml
vendored
|
@ -1,192 +0,0 @@
|
||||||
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
154
.github/workflows/gh-release.yml
vendored
|
@ -1,154 +0,0 @@
|
||||||
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 }}
|
|
54
.github/workflows/rust-clippy.yml
vendored
54
.github/workflows/rust-clippy.yml
vendored
|
@ -1,54 +0,0 @@
|
||||||
# This workflow uses actions that are not certified by GitHub.
|
|
||||||
# They are provided by a third-party and are governed by
|
|
||||||
# separate terms of service, privacy policy, and support
|
|
||||||
# documentation.
|
|
||||||
# rust-clippy is a tool that runs a bunch of lints to catch common
|
|
||||||
# mistakes in your Rust code and help improve your Rust code.
|
|
||||||
# More details at https://github.com/rust-lang/rust-clippy
|
|
||||||
# and https://rust-lang.github.io/rust-clippy/
|
|
||||||
|
|
||||||
name: rust-clippy analyze
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
pull_request:
|
|
||||||
# The branches below must be a subset of the branches above
|
|
||||||
branches: [ master ]
|
|
||||||
schedule:
|
|
||||||
- cron: '35 12 * * 5'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
rust-clippy-analyze:
|
|
||||||
name: Run rust-clippy analyzing
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
|
||||||
uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af #@v1
|
|
||||||
with:
|
|
||||||
profile: minimal
|
|
||||||
toolchain: stable
|
|
||||||
components: clippy
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Install required cargo
|
|
||||||
run: cargo install clippy-sarif sarif-fmt
|
|
||||||
|
|
||||||
- name: Run rust-clippy
|
|
||||||
run:
|
|
||||||
cargo clippy
|
|
||||||
--all-features
|
|
||||||
--message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
- name: Upload analysis results to GitHub
|
|
||||||
uses: github/codeql-action/upload-sarif@v1
|
|
||||||
with:
|
|
||||||
sarif_file: rust-clippy-results.sarif
|
|
||||||
wait-for-processing: true
|
|
22
.github/workflows/rust.yml
vendored
22
.github/workflows/rust.yml
vendored
|
@ -1,22 +0,0 @@
|
||||||
name: Rust
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ master ]
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Build
|
|
||||||
run: cargo build --verbose
|
|
||||||
- name: Run tests
|
|
||||||
run: cargo test --verbose
|
|
14
.gitignore
vendored
14
.gitignore
vendored
|
@ -1,14 +0,0 @@
|
||||||
# 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
2444
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
32
Cargo.toml
32
Cargo.toml
|
@ -1,41 +1,15 @@
|
||||||
[package]
|
[package]
|
||||||
name = "karton"
|
name = "microbin"
|
||||||
version = "2.0.1"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
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]
|
[dependencies]
|
||||||
actix-web = "4"
|
actix-web = "4"
|
||||||
actix-files = "0.6.0"
|
actix-files = "0.6.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0.80"
|
|
||||||
bytesize = { version = "1.1", features = ["serde"] }
|
|
||||||
askama = "0.10"
|
askama = "0.10"
|
||||||
askama-filters = { version = "0.1.3", features = ["chrono"] }
|
askama-filters = { version = "0.1.3", features = ["chrono"] }
|
||||||
chrono = "0.4.19"
|
chrono = "0.4.19"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
linkify = "0.8.1"
|
linkify = "0.8.1"
|
||||||
clap = { version = "3.1.12", features = ["derive", "env"] }
|
clap = { version = "3.1.12", features = ["derive"] }
|
||||||
actix-multipart = "0.4.0"
|
|
||||||
futures = "0.3"
|
|
||||||
sanitize-filename = "0.3.0"
|
|
||||||
log = "0.4"
|
|
||||||
env_logger = "0.9.0"
|
|
||||||
actix-web-httpauth = "0.6.0"
|
|
||||||
lazy_static = "1.4.0"
|
|
||||||
syntect = "5.0"
|
|
||||||
qrcode-generator = "4.1.6"
|
|
||||||
rust-embed = "6.4.2"
|
|
||||||
mime_guess = "2.0.4"
|
|
||||||
harsh = "0.2"
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
lto = true
|
|
||||||
strip = true
|
|
||||||
|
|
37
Dockerfile
37
Dockerfile
|
@ -1,37 +0,0 @@
|
||||||
FROM docker.io/rust:latest as build
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN \
|
|
||||||
DEBIAN_FRONTEND=noninteractive \
|
|
||||||
apt-get update &&\
|
|
||||||
apt-get -y install ca-certificates tzdata &&\
|
|
||||||
CARGO_NET_GIT_FETCH_WITH_CLI=true \
|
|
||||||
cargo build --release
|
|
||||||
|
|
||||||
# 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"]
|
|
29
LICENSE
29
LICENSE
|
@ -1,29 +0,0 @@
|
||||||
BSD 3-Clause License
|
|
||||||
|
|
||||||
Copyright (c) 2022, Dániel Szabó
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
1. Redistributions of source code must retain the above copyright notice, this
|
|
||||||
list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer in the documentation
|
|
||||||
and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
3. Neither the name of the copyright holder nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
39
README.MD
Normal file
39
README.MD
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# MicroBin
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
MicroBin is a super tiny and simple self hosted pastebin app written in Rust. The executable is only a few megabytes and uses very little memory (plus your pastas).
|
||||||
|
|
||||||
|
### 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 but you can change this with `-p` or `--port` arguments.
|
||||||
|
|
||||||
|
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 value of `ExecStart` replaced with the actual path to microbin on your machine.
|
||||||
|
|
||||||
|
```
|
||||||
|
[Unit]
|
||||||
|
Description=MicroBin
|
||||||
|
After=network.target
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Restart=always
|
||||||
|
ExecStart=/home/pi/microbin/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`.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Very little CSS and no JS, super lightweight and simple (see [water.css](https://github.com/kognise/water.css))
|
||||||
|
- Animal names instead of random numbers for pasta identifiers
|
||||||
|
- Automatically expiring pastas
|
||||||
|
- Never expiring pastas
|
||||||
|
- Listing and manually removing pastas
|
||||||
|
- URL shortening and redirection
|
||||||
|
|
||||||
|
### Needed improvements
|
||||||
|
- Persisting pastas on disk (currently they are lost on restart)
|
||||||
|
- Removing pasta after N reads
|
||||||
|
- File uploads
|
||||||
|
- ~~URL shortening~~ (added on 23 April 2022)
|
||||||
|
|
76
README.md
76
README.md
|
@ -1,76 +0,0 @@
|
||||||
# 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
54
TODO.md
|
@ -1,54 +0,0 @@
|
||||||
# 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
|
|
|
@ -1,7 +0,0 @@
|
||||||
(import (
|
|
||||||
fetchTarball {
|
|
||||||
url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz";
|
|
||||||
sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; }
|
|
||||||
) {
|
|
||||||
src = ./.;
|
|
||||||
}).defaultNix
|
|
77
flake.lock
generated
77
flake.lock
generated
|
@ -1,77 +0,0 @@
|
||||||
{
|
|
||||||
"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
21
flake.nix
|
@ -1,21 +0,0 @@
|
||||||
{
|
|
||||||
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
Normal file
BIN
git/index.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 625 KiB |
|
@ -1,7 +0,0 @@
|
||||||
(import (
|
|
||||||
fetchTarball {
|
|
||||||
url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz";
|
|
||||||
sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; }
|
|
||||||
) {
|
|
||||||
src = ./.;
|
|
||||||
}).shellNix
|
|
61
src/animalnumbers.rs
Normal file
61
src/animalnumbers.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
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 n: u64) -> String {
|
||||||
|
let mut result: Vec<&str> = Vec::new();
|
||||||
|
|
||||||
|
if n == 0 {
|
||||||
|
return animal_names[0].parse().unwrap();
|
||||||
|
} else if n == 1 {
|
||||||
|
return animal_names[1].parse().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// max 4 animals so 6 * 6 = 64 bits
|
||||||
|
let mut power = 6;
|
||||||
|
loop {
|
||||||
|
let d = n / animal_names.len().pow(power) as u64;
|
||||||
|
|
||||||
|
if !(result.is_empty() && d == 0) {
|
||||||
|
result.push(animal_names[d as usize]);
|
||||||
|
}
|
||||||
|
|
||||||
|
n -= d * animal_names.len().pow(power) as u64;
|
||||||
|
|
||||||
|
if power > 0 {
|
||||||
|
power -= 1;
|
||||||
|
} else { break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
result.join("-")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_u64(n: &str) -> u64 {
|
||||||
|
let mut result: u64 = 0;
|
||||||
|
|
||||||
|
let mut animals: Vec<&str> = n.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
166
src/args.rs
|
@ -1,166 +0,0 @@
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,214 +0,0 @@
|
||||||
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())
|
|
||||||
}
|
|
|
@ -1,110 +0,0 @@
|
||||||
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()))
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
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()))
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
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(),
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,213 +0,0 @@
|
||||||
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! :-(")
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
use actix_web::{get, web, HttpResponse};
|
|
||||||
use askama::Template;
|
|
||||||
|
|
||||||
use crate::args::{Args, ARGS};
|
|
||||||
use crate::pasta::Pasta;
|
|
||||||
use crate::util::misc::remove_expired;
|
|
||||||
use crate::AppState;
|
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "pastalist.html")]
|
|
||||||
struct PastaListTemplate<'a> {
|
|
||||||
pastas: &'a Vec<Pasta>,
|
|
||||||
args: &'a Args,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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(),
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
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())
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
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())
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
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())
|
|
||||||
}
|
|
297
src/main.rs
297
src/main.rs
|
@ -1,120 +1,227 @@
|
||||||
extern crate core;
|
extern crate core;
|
||||||
|
|
||||||
use crate::args::ARGS;
|
use actix_files as fs;
|
||||||
use crate::endpoints::{
|
use actix_web::web::Data;
|
||||||
create, edit, errors, info, pasta as pasta_endpoint, pastalist, qr, remove, static_resources,
|
use actix_web::{get, post, web, App, HttpRequest, HttpResponse, HttpServer, Responder, Result};
|
||||||
|
use askama::Template;
|
||||||
|
use clap::Parser;
|
||||||
|
use linkify::{LinkFinder, LinkKind};
|
||||||
|
use rand::Rng;
|
||||||
|
use regex::Regex;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use crate::animalnumbers::{to_animal_names, to_u64};
|
||||||
|
use crate::pasta::{Pasta, PastaFormData};
|
||||||
|
|
||||||
|
mod animalnumbers;
|
||||||
|
mod pasta;
|
||||||
|
|
||||||
|
struct AppState {
|
||||||
|
pastas: Mutex<Vec<Pasta>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[clap(author, version, about, long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
#[clap(short, long, default_value_t = 8080)]
|
||||||
|
port: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "index.html")]
|
||||||
|
struct IndexTemplate {}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "pasta.html")]
|
||||||
|
struct PastaTemplate<'a> {
|
||||||
|
pasta: &'a Pasta,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "pastalist.html")]
|
||||||
|
struct PastaListTemplate<'a> {
|
||||||
|
pastas: &'a Vec<Pasta>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
async fn index() -> impl Responder {
|
||||||
|
HttpResponse::Found()
|
||||||
|
.content_type("text/html")
|
||||||
|
.body(IndexTemplate {}.render().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/create")]
|
||||||
|
async fn create(data: web::Data<AppState>, pasta: web::Form<PastaFormData>) -> impl Responder {
|
||||||
|
let mut pastas = data.pastas.lock().unwrap();
|
||||||
|
|
||||||
|
let inner_pasta = pasta.into_inner();
|
||||||
|
|
||||||
|
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||||
|
Ok(n) => n.as_secs(),
|
||||||
|
Err(_) => panic!("SystemTime before UNIX EPOCH!"),
|
||||||
|
} as i64;
|
||||||
|
|
||||||
|
let expiration = match inner_pasta.expiration.as_str() {
|
||||||
|
"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!"),
|
||||||
};
|
};
|
||||||
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;
|
|
||||||
|
|
||||||
pub mod args;
|
let pasta_type = if is_valid_url(inner_pasta.content.as_str()) {
|
||||||
pub mod pasta;
|
String::from("url")
|
||||||
|
} else {
|
||||||
|
String::from("text")
|
||||||
|
};
|
||||||
|
|
||||||
pub mod util {
|
let new_pasta = Pasta {
|
||||||
pub mod pasta_id_converter;
|
id: rand::thread_rng().gen::<u16>() as u64,
|
||||||
pub mod auth;
|
content: inner_pasta.content,
|
||||||
pub mod dbio;
|
created: timenow,
|
||||||
pub mod hashids;
|
pasta_type,
|
||||||
pub mod misc;
|
expiration,
|
||||||
pub mod syntaxhighlighter;
|
};
|
||||||
|
|
||||||
|
let id = new_pasta.id;
|
||||||
|
|
||||||
|
pastas.push(new_pasta);
|
||||||
|
|
||||||
|
HttpResponse::Found()
|
||||||
|
.append_header(("Location", format!("/pasta/{}", to_animal_names(id))))
|
||||||
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod endpoints {
|
#[get("/pasta/{id}")]
|
||||||
pub mod create;
|
async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
|
||||||
pub mod edit;
|
let mut pastas = data.pastas.lock().unwrap();
|
||||||
pub mod errors;
|
let id = to_u64(&*id.into_inner());
|
||||||
pub mod info;
|
|
||||||
pub mod pasta;
|
remove_expired(&mut pastas);
|
||||||
pub mod pastalist;
|
|
||||||
pub mod qr;
|
for pasta in pastas.iter() {
|
||||||
pub mod remove;
|
if pasta.id == id {
|
||||||
pub mod static_resources;
|
return HttpResponse::Found()
|
||||||
|
.content_type("text/html")
|
||||||
|
.body(PastaTemplate { pasta }.render().unwrap());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppState {
|
HttpResponse::Found().body("Pasta not found! :-(")
|
||||||
pub pastas: Mutex<Vec<Pasta>>,
|
}
|
||||||
|
|
||||||
|
#[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().body("This is not a valid URL. :-(");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponse::Found().body("Pasta not found! :-(")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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().body("Pasta not found! :-(")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/pastalist")]
|
||||||
|
async fn list(data: web::Data<AppState>) -> HttpResponse {
|
||||||
|
let mut pastas = data.pastas.lock().unwrap();
|
||||||
|
|
||||||
|
remove_expired(&mut pastas);
|
||||||
|
|
||||||
|
HttpResponse::Found()
|
||||||
|
.content_type("text/html")
|
||||||
|
.body(PastaListTemplate { pastas: &pastas }.render().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
Builder::new()
|
let args = Args::parse();
|
||||||
.format(|buf, record| {
|
println!(
|
||||||
writeln!(
|
"{}",
|
||||||
buf,
|
format!("Listening on http://127.0.0.1:{}", args.port.to_string())
|
||||||
"{} [{}] - {}",
|
|
||||||
Local::now().format("%Y-%m-%dT%H:%M:%S"),
|
|
||||||
record.level(),
|
|
||||||
record.args()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.filter(None, LevelFilter::Info)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"MicroBin starting on http://{}:{}",
|
|
||||||
ARGS.bind.to_string(),
|
|
||||||
ARGS.port.to_string()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
match fs::create_dir_all("./pasta_data/public") {
|
|
||||||
Ok(dir) => dir,
|
|
||||||
Err(error) => {
|
|
||||||
log::error!("Couldn't create data directory ./pasta_data/public/: {error:?}");
|
|
||||||
panic!("Couldn't create data directory ./pasta_data/public/: {error:?}");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let data = web::Data::new(AppState {
|
let data = web::Data::new(AppState {
|
||||||
pastas: Mutex::new(dbio::load_from_file().unwrap()),
|
pastas: Mutex::new(Vec::new()),
|
||||||
});
|
});
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
.app_data(data.clone())
|
.app_data(data.clone())
|
||||||
.wrap(middleware::NormalizePath::trim())
|
.service(index)
|
||||||
.service(create::index)
|
.service(create)
|
||||||
.service(info::info)
|
.service(getpasta)
|
||||||
.route(
|
.service(redirecturl)
|
||||||
&format!("/{}/{{id}}", ARGS.pasta_endpoint),
|
.service(getrawpasta)
|
||||||
web::get().to(pasta_endpoint::getpasta)
|
.service(remove)
|
||||||
)
|
.service(list)
|
||||||
.route(
|
.service(fs::Files::new("/static", "./static"))
|
||||||
&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::remove)
|
|
||||||
.service(pastalist::list)
|
|
||||||
.wrap(Condition::new(
|
|
||||||
ARGS.auth_username.is_some(),
|
|
||||||
HttpAuthentication::basic(util::auth::auth_validator),
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
.bind((ARGS.bind, ARGS.port))?
|
.bind(format!("127.0.0.1:{}", args.port.to_string()))?
|
||||||
.workers(ARGS.threads as usize)
|
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remove_expired(pastas: &mut Vec<Pasta>) {
|
||||||
|
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| p.expiration == 0 || p.expiration > timenow);
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
154
src/pasta.rs
154
src/pasta.rs
|
@ -1,74 +1,34 @@
|
||||||
use bytesize::ByteSize;
|
|
||||||
use chrono::{Datelike, Local, TimeZone, Timelike};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::path::Path;
|
use actix_web::cookie::time::macros::format_description;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use chrono::{Datelike, DateTime, NaiveDateTime, Timelike, Utc};
|
||||||
|
use serde::Deserialize;
|
||||||
|
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 struct Pasta {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub file: Option<PastaFile>,
|
|
||||||
pub extension: String,
|
|
||||||
pub private: bool,
|
|
||||||
pub editable: bool,
|
|
||||||
pub created: i64,
|
pub created: i64,
|
||||||
pub expiration: i64,
|
pub expiration: i64,
|
||||||
pub last_read: i64,
|
pub pasta_type: String
|
||||||
pub read_count: u64,
|
}
|
||||||
pub burn_after_reads: u64,
|
|
||||||
// what types can there be?
|
#[derive(Deserialize)]
|
||||||
// `url`, `text`,
|
pub struct PastaFormData {
|
||||||
pub pasta_type: String,
|
pub content: String,
|
||||||
|
pub expiration: String
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pasta {
|
impl Pasta {
|
||||||
pub fn id_as_animals(&self) -> String {
|
|
||||||
if ARGS.hash_ids {
|
pub fn idAsAnimals(&self) -> String {
|
||||||
to_hashids(self.id)
|
to_animal_names(self.id)
|
||||||
} else {
|
|
||||||
CONVERTER.to_names(self.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn created_as_string(&self) -> String {
|
pub fn createdAsString(&self) -> String {
|
||||||
let date = Local.timestamp(self.created, 0);
|
let date = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(self.created, 0), Utc);
|
||||||
format!(
|
format!(
|
||||||
"{:02}-{:02} {:02}:{:02}",
|
"{}-{:02}-{:02} {}:{}",
|
||||||
|
date.year(),
|
||||||
date.month(),
|
date.month(),
|
||||||
date.day(),
|
date.day(),
|
||||||
date.hour(),
|
date.hour(),
|
||||||
|
@ -76,88 +36,20 @@ impl Pasta {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn expiration_as_string(&self) -> String {
|
pub fn expirationAsString(&self) -> String {
|
||||||
if self.expiration == 0 {
|
let date = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(self.expiration, 0), Utc);
|
||||||
String::from("Never")
|
|
||||||
} else {
|
|
||||||
let date = Local.timestamp(self.expiration, 0);
|
|
||||||
format!(
|
format!(
|
||||||
"{:02}-{:02} {:02}:{:02}",
|
"{}-{:02}-{:02} {}:{}",
|
||||||
|
date.year(),
|
||||||
date.month(),
|
date.month(),
|
||||||
date.day(),
|
date.day(),
|
||||||
date.hour(),
|
date.hour(),
|
||||||
date.minute(),
|
date.minute(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
impl fmt::Display for Pasta {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
use actix_web::dev::ServiceRequest;
|
|
||||||
use actix_web::{error, Error};
|
|
||||||
use actix_web_httpauth::extractors::basic::BasicAuth;
|
|
||||||
|
|
||||||
use crate::args::ARGS;
|
|
||||||
|
|
||||||
pub async fn auth_validator(
|
|
||||||
req: ServiceRequest,
|
|
||||||
credentials: BasicAuth,
|
|
||||||
) -> Result<ServiceRequest, Error> {
|
|
||||||
// check if username matches
|
|
||||||
if credentials.user_id().as_ref() == ARGS.auth_username.as_ref().unwrap() {
|
|
||||||
return match ARGS.auth_password.as_ref() {
|
|
||||||
Some(cred_pass) => match credentials.password() {
|
|
||||||
None => Err(error::ErrorBadRequest("Invalid login details.")),
|
|
||||||
Some(arg_pass) => {
|
|
||||||
if arg_pass == cred_pass {
|
|
||||||
Ok(req)
|
|
||||||
} else {
|
|
||||||
Err(error::ErrorBadRequest("Invalid login details."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => Ok(req),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
Err(error::ErrorBadRequest("Invalid login details."))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
use std::fs::File;
|
|
||||||
use std::io;
|
|
||||||
use std::io::{BufReader, BufWriter};
|
|
||||||
|
|
||||||
use crate::Pasta;
|
|
||||||
|
|
||||||
static DATABASE_PATH: &str = "pasta_data/database.json";
|
|
||||||
|
|
||||||
pub fn save_to_file(pasta_data: &Vec<Pasta>) {
|
|
||||||
let mut file = File::create(DATABASE_PATH);
|
|
||||||
match file {
|
|
||||||
Ok(_) => {
|
|
||||||
let writer = BufWriter::new(file.unwrap());
|
|
||||||
serde_json::to_writer(writer, &pasta_data).expect("Failed to create JSON writer");
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
log::info!("Database file {DATABASE_PATH} not found!");
|
|
||||||
file = File::create(DATABASE_PATH);
|
|
||||||
match file {
|
|
||||||
Ok(_) => {
|
|
||||||
log::info!("Database file {DATABASE_PATH} created.");
|
|
||||||
save_to_file(pasta_data);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::error!(
|
|
||||||
"Failed to create database file {}: {}!",
|
|
||||||
&DATABASE_PATH,
|
|
||||||
&err
|
|
||||||
);
|
|
||||||
panic!("Failed to create database file {DATABASE_PATH}: {err}!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_from_file() -> io::Result<Vec<Pasta>> {
|
|
||||||
let file = File::open(DATABASE_PATH);
|
|
||||||
match file {
|
|
||||||
Ok(_) => {
|
|
||||||
let reader = BufReader::new(file.unwrap());
|
|
||||||
let data: Vec<Pasta> = match serde_json::from_reader(reader) {
|
|
||||||
Ok(t) => t,
|
|
||||||
_ => Vec::new(),
|
|
||||||
};
|
|
||||||
Ok(data)
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
log::info!("Database file {DATABASE_PATH} not found!");
|
|
||||||
save_to_file(&Vec::<Pasta>::new());
|
|
||||||
|
|
||||||
log::info!("Database file {DATABASE_PATH} created.");
|
|
||||||
load_from_file()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
|
@ -1,98 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
use syntect::easy::HighlightLines;
|
|
||||||
use syntect::highlighting::{Style, ThemeSet};
|
|
||||||
use syntect::html::append_highlighted_html_for_styled_line;
|
|
||||||
use syntect::html::IncludeBackground::No;
|
|
||||||
use syntect::parsing::SyntaxSet;
|
|
||||||
use syntect::util::LinesWithEndings;
|
|
||||||
|
|
||||||
pub fn html_highlight(text: &str, extension: &str) -> String {
|
|
||||||
let ps = SyntaxSet::load_defaults_newlines();
|
|
||||||
let ts = ThemeSet::load_defaults();
|
|
||||||
|
|
||||||
let syntax = ps
|
|
||||||
.find_syntax_by_extension(extension)
|
|
||||||
.or_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
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
<?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>
|
|
Before Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 3.1 KiB |
|
@ -1,81 +0,0 @@
|
||||||
<?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>
|
|
Before Width: | Height: | Size: 3.3 KiB |
|
@ -1,626 +0,0 @@
|
||||||
/*
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
{% include "header.html" %}
|
|
||||||
<form method="POST" enctype="multipart/form-data">
|
|
||||||
<h4>
|
|
||||||
Editing pasta '{{ pasta.id_as_animals() }}'
|
|
||||||
</h4>
|
|
||||||
<label>Content</label>
|
|
||||||
<br>
|
|
||||||
<textarea style="width: 100%; min-height: 100px" name="content" id="content" autofocus>{{ pasta.content }}</textarea>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
{% if args.readonly %}
|
|
||||||
<input style="width: 140px; background-color: limegreen" disabled type="submit" value="Read Only"/>
|
|
||||||
{%- else %}
|
|
||||||
<input style="width: 140px; background-color: limegreen" type="submit" value="Save"/>
|
|
||||||
{%- endif %}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
</form>
|
|
||||||
{% include "footer.html" %}
|
|
|
@ -1,10 +0,0 @@
|
||||||
{% include "header.html" %}
|
|
||||||
<br>
|
|
||||||
<h2>{{ status_code.as_u16() }}</h2>
|
|
||||||
<b>{{ status_code.canonical_reason().unwrap_or("Unknown error") }}</b>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<a href="{{ args.public_path }}/"> Go Home</a>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
{% include "footer.html" %}
|
|
|
@ -1,18 +1,7 @@
|
||||||
{% if !args.hide_footer %}
|
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<p style="font-size: smaller">
|
<p style="font-size: smaller">
|
||||||
{% if args.footer_text.as_ref().is_none() %}
|
MicroBin by Daniel Szabo. Fork me on <a href="https://github.com/szabodanika/microbin">GitHub</a>!
|
||||||
<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>!
|
Let's keep the Web <b>compact</b>, <b>accessible</b> and <b>humane</b>!
|
||||||
{%- else %}
|
|
||||||
{{ args.footer_text.as_ref().unwrap() }}
|
|
||||||
{%- endif %}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
@ -1,65 +1,28 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>{{ args.title }}</title>
|
<title>MicroBin</title>
|
||||||
|
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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">
|
<link rel="stylesheet" href="/static/water.css">
|
||||||
{% if !args.pure_html %}
|
|
||||||
{% if args.custom_css.as_ref().is_none() %}
|
|
||||||
<link rel="stylesheet" href="{{ args.public_path }}/static/water.css">
|
|
||||||
{%- else %}
|
|
||||||
<link rel="stylesheet" href="{{ args.custom_css.as_ref().unwrap() }}">
|
|
||||||
{%- endif %}
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
{% if args.wide %}
|
|
||||||
|
|
||||||
<body style="
|
<body style="
|
||||||
max-width: 1080px;
|
max-width: 720px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding-left:0.5rem;
|
padding-left:0.5rem;
|
||||||
padding-right:0.5rem;
|
padding-right:0.5rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-size: 1.1em;
|
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>
|
<br>
|
||||||
|
|
||||||
{% if !args.hide_header %}
|
|
||||||
|
|
||||||
<b style="margin-right: 0.5rem">
|
<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>
|
</b>
|
||||||
|
|
||||||
<a href="{{ args.public_path }}/" style="margin-right: 0.5rem; margin-left: 0.5rem">New
|
<a href="/" style="margin-right: 0.5rem; margin-left: 0.5rem">New Pasta</a>
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="{{ args.public_path }}/pastalist" style="margin-right: 0.5rem; margin-left: 0.5rem">List</a>
|
<a href="/pastalist" style="margin-right: 0.5rem; margin-left: 0.5rem">Pasta List</a>
|
||||||
|
|
||||||
<a href="{{ args.public_path }}/info" style="margin-right: 0.5rem; margin-left: 0.5rem">Info</a>
|
<a href="https://github.com/szabodanika/microbin" style="margin-right: 0.5rem; margin-left: 0.5rem">GitHub</a>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
{%- endif %}
|
|
||||||
|
|
|
@ -1,252 +1,23 @@
|
||||||
{% include "header.html" %}
|
{% include "header.html" %}
|
||||||
|
<form action="create" method="POST">
|
||||||
<form id="pasta-form" action="upload" method="POST" enctype="multipart/form-data">
|
|
||||||
<br>
|
<br>
|
||||||
<div id="settings">
|
|
||||||
<div>
|
|
||||||
<label for="expiration">Expiration</label><br>
|
<label for="expiration">Expiration</label><br>
|
||||||
<select style="width: 100%;" name="expiration" id="expiration">
|
<select name="expiration" id="expiration">
|
||||||
<optgroup label="Expire after">
|
<optgroup label="Expire">
|
||||||
{% if args.default_expiry == "1min" %}
|
<option value="1min">1 minute</option>
|
||||||
<option selected value="1min">
|
<option value="10min">10 minutes</option>
|
||||||
{%- else %}
|
<option value="1hour">1 hour</option>
|
||||||
<option value="1min">
|
<option selected value="24hour">24 hours</option>
|
||||||
{%- endif %}
|
<option value="1week">1 week</option>
|
||||||
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>
|
</optgroup>
|
||||||
{% if !args.no_eternal_pasta %}
|
|
||||||
<option value="never">Never Expire</option>
|
<option value="never">Never Expire</option>
|
||||||
{%- endif %}
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
|
||||||
{% if args.enable_burn_after %}
|
|
||||||
<div>
|
|
||||||
<label for="expiration">Burn After</label><br>
|
|
||||||
<select style="width: 100%;" name="burn_after" id="burn_after">
|
|
||||||
<optgroup label="Burn after">
|
|
||||||
{% if args.default_burn_after == 1 %}
|
|
||||||
<option selected value="1">
|
|
||||||
{%- else %}
|
|
||||||
<option value="1">
|
|
||||||
{%- endif %}
|
|
||||||
First Read
|
|
||||||
</option>
|
|
||||||
{% if args.default_burn_after == 10 %}
|
|
||||||
<option selected value="10">
|
|
||||||
{%- else %}
|
|
||||||
<option value="10">
|
|
||||||
{%- endif %}
|
|
||||||
10th Read
|
|
||||||
</option>
|
|
||||||
{% if args.default_burn_after == 100 %}
|
|
||||||
<option selected value="100">
|
|
||||||
{%- else %}
|
|
||||||
<option value="100">
|
|
||||||
{%- endif %}
|
|
||||||
100th Read
|
|
||||||
</option>
|
|
||||||
{% if args.default_burn_after == 1000 %}
|
|
||||||
<option selected value="1000">
|
|
||||||
{%- else %}
|
|
||||||
<option value="1000">
|
|
||||||
{%- endif %}
|
|
||||||
1000th Read
|
|
||||||
</option>
|
|
||||||
{% if args.default_burn_after == 10000 %}
|
|
||||||
<option selected value="10000">
|
|
||||||
{%- else %}
|
|
||||||
<option value="10000">
|
|
||||||
{%- endif %}
|
|
||||||
10000th Read
|
|
||||||
</option>
|
|
||||||
</optgroup>
|
|
||||||
{% if args.default_burn_after == 0 %}
|
|
||||||
<option selected value="0">
|
|
||||||
{%- else %}
|
|
||||||
<option value="0">
|
|
||||||
{%- endif %}
|
|
||||||
No Limit
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
{% if args.highlightsyntax %}
|
|
||||||
<div>
|
|
||||||
<label for="syntax-highlight">Syntax</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>
|
<br>
|
||||||
<input type="file" id="file" name="file" />
|
<label>Content</label>
|
||||||
</div>
|
<br>
|
||||||
{% endif %}
|
<textarea style="width: 100%; min-height: 100px" name="content" autofocus></textarea>
|
||||||
{% if args.readonly %}
|
<br>
|
||||||
<b>
|
<input style="width: 100px; background-color: limegreen"; type="submit" value="Save"/>
|
||||||
<!--<input style="width: 140px; float: right; background-color: #0076d18f;" disabled type="submit"-->
|
<br>
|
||||||
<!--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>
|
</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" %}
|
{% include "footer.html" %}
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
{% 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,149 +1,4 @@
|
||||||
{% include "header.html" %}
|
{% include "header.html" %}
|
||||||
<div style="float: left">
|
<a href="/raw/{{pasta.idAsAnimals()}}">Raw Pasta</a>
|
||||||
{% if pasta.content != "No Text Content" %}
|
<pre><code>{{pasta}}</code></pre>
|
||||||
<button id="copy-text-button" class="copy-button" style="margin-right: 0.5rem">
|
|
||||||
Copy Text
|
|
||||||
</button>
|
|
||||||
{% if args.public_path.to_string() != "" && pasta.pasta_type == "url" %}
|
|
||||||
<button id="copy-redirect-button" class="copy-button" style="margin-right: 0.5rem">
|
|
||||||
Copy Redirect
|
|
||||||
</button>
|
|
||||||
{%- endif %}
|
|
||||||
<a style="margin-right: 1rem" href="{{ args.public_path }}/{{ 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" %}
|
{% include "footer.html" %}
|
||||||
|
|
|
@ -2,54 +2,47 @@
|
||||||
|
|
||||||
|
|
||||||
{% if pastas.is_empty() %}
|
{% if pastas.is_empty() %}
|
||||||
<br>
|
|
||||||
<p>
|
<p>
|
||||||
No pastas yet. 😔 Create one <a href="{{ args.public_path }}/">here</a>.
|
No pastas yet. 😔 Create one <a href="/">here</a>.
|
||||||
</p>
|
</p>
|
||||||
<br>
|
|
||||||
{%- else %}
|
{%- else %}
|
||||||
<h3>Pastas</h3>
|
<br>
|
||||||
{% if args.pure_html %}
|
|
||||||
<table border="1" style="width: 100%; white-space: nowrap;">
|
|
||||||
{% else %}
|
|
||||||
<table style="width: 100%">
|
<table style="width: 100%">
|
||||||
{% endif %}
|
|
||||||
<thead>
|
<thead>
|
||||||
<th style="width: 30%">
|
<tr>
|
||||||
|
<th colspan="4">Pastas</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
Key
|
Key
|
||||||
</th>
|
</th>
|
||||||
<th style="width: 20%">
|
<th>
|
||||||
Created
|
Created
|
||||||
</th>
|
</th>
|
||||||
<th style="width: 20%">
|
<th>
|
||||||
Expiration
|
Expiration
|
||||||
</th>
|
</th>
|
||||||
<th style="width: 30%">
|
<th>
|
||||||
|
|
||||||
</th>
|
</th>
|
||||||
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for pasta in pastas %}
|
{% for pasta in pastas %}
|
||||||
{% if pasta.pasta_type == "text" && !pasta.private %}
|
{% if pasta.pasta_type == "text" %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ args.public_path }}/{{ args.pasta_endpoint }}/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
|
<a href="/pasta/{{pasta.idAsAnimals()}}">{{pasta.idAsAnimals()}}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{pasta.created_as_string()}}
|
{{pasta.createdAsString()}}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{pasta.expiration_as_string()}}
|
{{pasta.expirationAsString()}}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a style="margin-right:1rem" href="{{ args.public_path }}/{{ args.raw_endpoint }}/{{pasta.id_as_animals()}}">Raw</a>
|
<a style="margin-right:1rem" href="/raw/{{pasta.idAsAnimals()}}">Raw</a>
|
||||||
{% if pasta.file.is_some() %}
|
<a href="/remove/{{pasta.idAsAnimals()}}">Remove</a>
|
||||||
<a style="margin-right:1rem"
|
|
||||||
href="{{ args.public_path }}/file/{{pasta.id_as_animals()}}/{{pasta.file.as_ref().unwrap().name()}}">File</a>
|
|
||||||
{%- endif %}
|
|
||||||
{% if pasta.editable %}
|
|
||||||
<a style="margin-right:1rem" href="{{ args.public_path }}/edit/{{pasta.id_as_animals()}}">Edit</a>
|
|
||||||
{%- endif %}
|
|
||||||
<a href="{{ args.public_path }}/remove/{{pasta.id_as_animals()}}">Remove</a>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
@ -57,45 +50,41 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<br>
|
<br>
|
||||||
<h3>URL Redirects</h3>
|
<table>
|
||||||
{% if args.pure_html %}
|
|
||||||
<table border="1" style="width: 100%">
|
|
||||||
{% else %}
|
|
||||||
<table style="width: 100%">
|
|
||||||
{% endif %}
|
|
||||||
<thead>
|
<thead>
|
||||||
<th style="width: 30%">
|
<tr>
|
||||||
|
<th colspan="4">URL Redirects</th>
|
||||||
|
</tr>
|
||||||
|
<tr >
|
||||||
|
<th>
|
||||||
Key
|
Key
|
||||||
</th>
|
</th>
|
||||||
<th style="width: 20%">
|
<th>
|
||||||
Created
|
Created
|
||||||
</th>
|
</th>
|
||||||
<th style="width: 20%">
|
<th>
|
||||||
Expiration
|
Expiration
|
||||||
</th>
|
</th>
|
||||||
<th style="width: 30%">
|
<th>
|
||||||
|
|
||||||
</th>
|
</th>
|
||||||
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
{% for pasta in pastas %}
|
{% for pasta in pastas %}
|
||||||
{% if pasta.pasta_type == "url" && !pasta.private %}
|
{% if pasta.pasta_type == "url" %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ args.public_path }}/{{ args.pasta_endpoint }}/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
|
<a href="/url/{{pasta.idAsAnimals()}}">{{pasta.idAsAnimals()}}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{pasta.created_as_string()}}
|
{{pasta.createdAsString()}}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{pasta.expiration_as_string()}}
|
{{pasta.expirationAsString()}}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a style="margin-right:1rem" href="{{ args.public_path }}/{{ args.url_endpoint }}/{{pasta.id_as_animals()}}">Open</a>
|
<a style="margin-right:1rem" href="/raw/{{pasta.idAsAnimals()}}">Raw</a>
|
||||||
<a style="margin-right:1rem; cursor: pointer;" id="copy-button"
|
<a href="/remove/{{pasta.idAsAnimals()}}">Remove</a>
|
||||||
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
@ -104,17 +93,4 @@
|
||||||
</table>
|
</table>
|
||||||
<br>
|
<br>
|
||||||
{%- endif %}
|
{%- 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" %}
|
{% include "footer.html" %}
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
{% 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…
Add table
Reference in a new issue