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]
|
||||
name = "karton"
|
||||
version = "2.0.1"
|
||||
name = "microbin"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Jade <jade@schrottkatze.de>", "Daniel Szabo <daniel.szabo99@outlook.com>"]
|
||||
license = "BSD-3-Clause"
|
||||
description = "Simple, performant, configurable, entirely self-contained Pastebin and URL shortener."
|
||||
readme = "README.md"
|
||||
homepage = "https://gitlab.com/obsidianical/microbin"
|
||||
repository = "https://gitlab.com/obsidianical/microbin"
|
||||
keywords = ["pastebin", "karton", "microbin", "actix", "selfhosted"]
|
||||
categories = ["pastebins"]
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4"
|
||||
actix-files = "0.6.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.80"
|
||||
bytesize = { version = "1.1", features = ["serde"] }
|
||||
askama = "0.10"
|
||||
askama-filters = { version = "0.1.3", features = ["chrono"] }
|
||||
chrono = "0.4.19"
|
||||
rand = "0.8.5"
|
||||
linkify = "0.8.1"
|
||||
clap = { version = "3.1.12", features = ["derive", "env"] }
|
||||
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
|
||||
clap = { version = "3.1.12", features = ["derive"] }
|
||||
|
|
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())
|
||||
}
|
299
src/main.rs
299
src/main.rs
|
@ -1,120 +1,227 @@
|
|||
extern crate core;
|
||||
|
||||
use crate::args::ARGS;
|
||||
use crate::endpoints::{
|
||||
create, edit, errors, info, pasta as pasta_endpoint, pastalist, qr, remove, static_resources,
|
||||
};
|
||||
use crate::pasta::Pasta;
|
||||
use crate::util::dbio;
|
||||
use actix_web::middleware::Condition;
|
||||
use actix_web::{middleware, web, App, HttpServer};
|
||||
use actix_web_httpauth::middleware::HttpAuthentication;
|
||||
use chrono::Local;
|
||||
use env_logger::Builder;
|
||||
use futures::lock::Mutex;
|
||||
use log::LevelFilter;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use actix_files as fs;
|
||||
use actix_web::web::Data;
|
||||
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};
|
||||
|
||||
pub mod args;
|
||||
pub mod pasta;
|
||||
use crate::animalnumbers::{to_animal_names, to_u64};
|
||||
use crate::pasta::{Pasta, PastaFormData};
|
||||
|
||||
pub mod util {
|
||||
pub mod pasta_id_converter;
|
||||
pub mod auth;
|
||||
pub mod dbio;
|
||||
pub mod hashids;
|
||||
pub mod misc;
|
||||
pub mod syntaxhighlighter;
|
||||
mod animalnumbers;
|
||||
mod pasta;
|
||||
|
||||
struct AppState {
|
||||
pastas: Mutex<Vec<Pasta>>,
|
||||
}
|
||||
|
||||
pub mod endpoints {
|
||||
pub mod create;
|
||||
pub mod edit;
|
||||
pub mod errors;
|
||||
pub mod info;
|
||||
pub mod pasta;
|
||||
pub mod pastalist;
|
||||
pub mod qr;
|
||||
pub mod remove;
|
||||
pub mod static_resources;
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
#[clap(short, long, default_value_t = 8080)]
|
||||
port: u32,
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
pub pastas: Mutex<Vec<Pasta>>,
|
||||
#[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!"),
|
||||
};
|
||||
|
||||
let pasta_type = if is_valid_url(inner_pasta.content.as_str()) {
|
||||
String::from("url")
|
||||
} else {
|
||||
String::from("text")
|
||||
};
|
||||
|
||||
let new_pasta = Pasta {
|
||||
id: rand::thread_rng().gen::<u16>() as u64,
|
||||
content: inner_pasta.content,
|
||||
created: timenow,
|
||||
pasta_type,
|
||||
expiration,
|
||||
};
|
||||
|
||||
let id = new_pasta.id;
|
||||
|
||||
pastas.push(new_pasta);
|
||||
|
||||
HttpResponse::Found()
|
||||
.append_header(("Location", format!("/pasta/{}", to_animal_names(id))))
|
||||
.finish()
|
||||
}
|
||||
|
||||
#[get("/pasta/{id}")]
|
||||
async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
let id = to_u64(&*id.into_inner());
|
||||
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
for pasta in pastas.iter() {
|
||||
if pasta.id == id {
|
||||
return HttpResponse::Found()
|
||||
.content_type("text/html")
|
||||
.body(PastaTemplate { pasta }.render().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse::Found().body("Pasta not found! :-(")
|
||||
}
|
||||
|
||||
#[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]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Builder::new()
|
||||
.format(|buf, record| {
|
||||
writeln!(
|
||||
buf,
|
||||
"{} [{}] - {}",
|
||||
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()
|
||||
let args = Args::parse();
|
||||
println!(
|
||||
"{}",
|
||||
format!("Listening on http://127.0.0.1:{}", 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 {
|
||||
pastas: Mutex::new(dbio::load_from_file().unwrap()),
|
||||
pastas: Mutex::new(Vec::new()),
|
||||
});
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(data.clone())
|
||||
.wrap(middleware::NormalizePath::trim())
|
||||
.service(create::index)
|
||||
.service(info::info)
|
||||
.route(
|
||||
&format!("/{}/{{id}}", ARGS.pasta_endpoint),
|
||||
web::get().to(pasta_endpoint::getpasta)
|
||||
)
|
||||
.route(
|
||||
&format!("/{}/{{id}}", ARGS.raw_endpoint),
|
||||
web::get().to(pasta_endpoint::getrawpasta)
|
||||
)
|
||||
.route(
|
||||
&format!("/{}/{{id}}", ARGS.url_endpoint),
|
||||
web::get().to(pasta_endpoint::redirecturl)
|
||||
)
|
||||
//.service(pasta_endpoint::getpasta)
|
||||
//.service(pasta_endpoint::getrawpasta)
|
||||
//.service(pasta_endpoint::redirecturl)
|
||||
.service(edit::get_edit)
|
||||
.service(edit::post_edit)
|
||||
.service(static_resources::static_resources)
|
||||
.service(qr::getqr)
|
||||
.service(actix_files::Files::new("/file", "./pasta_data/public/"))
|
||||
.service(web::resource("/upload").route(web::post().to(create::create)))
|
||||
.default_service(web::route().to(errors::not_found))
|
||||
.wrap(middleware::Logger::default())
|
||||
.service(remove::remove)
|
||||
.service(pastalist::list)
|
||||
.wrap(Condition::new(
|
||||
ARGS.auth_username.is_some(),
|
||||
HttpAuthentication::basic(util::auth::auth_validator),
|
||||
))
|
||||
.service(index)
|
||||
.service(create)
|
||||
.service(getpasta)
|
||||
.service(redirecturl)
|
||||
.service(getrawpasta)
|
||||
.service(remove)
|
||||
.service(list)
|
||||
.service(fs::Files::new("/static", "./static"))
|
||||
})
|
||||
.bind((ARGS.bind, ARGS.port))?
|
||||
.workers(ARGS.threads as usize)
|
||||
.bind(format!("127.0.0.1:{}", args.port.to_string()))?
|
||||
.run()
|
||||
.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::path::Path;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use actix_web::cookie::time::macros::format_description;
|
||||
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 id: u64,
|
||||
pub content: String,
|
||||
pub file: Option<PastaFile>,
|
||||
pub extension: String,
|
||||
pub private: bool,
|
||||
pub editable: bool,
|
||||
pub created: i64,
|
||||
pub expiration: i64,
|
||||
pub last_read: i64,
|
||||
pub read_count: u64,
|
||||
pub burn_after_reads: u64,
|
||||
// what types can there be?
|
||||
// `url`, `text`,
|
||||
pub pasta_type: String,
|
||||
pub pasta_type: String
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PastaFormData {
|
||||
pub content: String,
|
||||
pub expiration: String
|
||||
}
|
||||
|
||||
impl Pasta {
|
||||
pub fn id_as_animals(&self) -> String {
|
||||
if ARGS.hash_ids {
|
||||
to_hashids(self.id)
|
||||
} else {
|
||||
CONVERTER.to_names(self.id)
|
||||
}
|
||||
|
||||
pub fn idAsAnimals(&self) -> String {
|
||||
to_animal_names(self.id)
|
||||
}
|
||||
|
||||
pub fn created_as_string(&self) -> String {
|
||||
let date = Local.timestamp(self.created, 0);
|
||||
pub fn createdAsString(&self) -> String {
|
||||
let date = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(self.created, 0), Utc);
|
||||
format!(
|
||||
"{:02}-{:02} {:02}:{:02}",
|
||||
"{}-{:02}-{:02} {}:{}",
|
||||
date.year(),
|
||||
date.month(),
|
||||
date.day(),
|
||||
date.hour(),
|
||||
|
@ -76,89 +36,21 @@ impl Pasta {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn expiration_as_string(&self) -> String {
|
||||
if self.expiration == 0 {
|
||||
String::from("Never")
|
||||
} else {
|
||||
let date = Local.timestamp(self.expiration, 0);
|
||||
pub fn expirationAsString(&self) -> String {
|
||||
let date = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(self.expiration, 0), Utc);
|
||||
format!(
|
||||
"{:02}-{:02} {:02}:{:02}",
|
||||
"{}-{:02}-{:02} {}:{}",
|
||||
date.year(),
|
||||
date.month(),
|
||||
date.day(),
|
||||
date.hour(),
|
||||
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 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.content)
|
||||
|
|
|
@ -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>
|
||||
<p style="font-size: smaller">
|
||||
{% if args.footer_text.as_ref().is_none() %}
|
||||
<b>Karton</b> by Schrottkatze, based on <a href="https://microbin.eu">MicroBin</a> by Dániel Szabó and the FOSS Community.
|
||||
MicroBin by Daniel Szabo. Fork me on <a href="https://github.com/szabodanika/microbin">GitHub</a>!
|
||||
Let's keep the Web <b>compact</b>, <b>accessible</b> and <b>humane</b>!
|
||||
{%- else %}
|
||||
{{ args.footer_text.as_ref().unwrap() }}
|
||||
{%- endif %}
|
||||
</p>
|
||||
|
||||
{%- endif %}
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
|
|
|
@ -1,65 +1,28 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>{{ args.title }}</title>
|
||||
|
||||
<title>MicroBin</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" type="image/svg+xml" href="{{ args.public_path }}/static/favicon.svg">
|
||||
{% if !args.pure_html %}
|
||||
{% if args.custom_css.as_ref().is_none() %}
|
||||
<link rel="stylesheet" href="{{ args.public_path }}/static/water.css">
|
||||
{%- else %}
|
||||
<link rel="stylesheet" href="{{ args.custom_css.as_ref().unwrap() }}">
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
|
||||
</head>
|
||||
{% if args.wide %}
|
||||
|
||||
<link rel="stylesheet" href="/static/water.css">
|
||||
</head>
|
||||
<body style="
|
||||
max-width: 1080px;
|
||||
max-width: 720px;
|
||||
margin: auto;
|
||||
padding-left:0.5rem;
|
||||
padding-right:0.5rem;
|
||||
line-height: 1.5;
|
||||
font-size: 1.1em;
|
||||
">
|
||||
{%- else %}
|
||||
<br>
|
||||
|
||||
<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>
|
||||
<b style="margin-right: 0.5rem">
|
||||
<i><span style="font-size:2.2rem; margin-right:1rem">μ</span></i> MicroBin
|
||||
</b>
|
||||
|
||||
{% if !args.hide_header %}
|
||||
<a href="/" style="margin-right: 0.5rem; margin-left: 0.5rem">New Pasta</a>
|
||||
|
||||
<b style="margin-right: 0.5rem">
|
||||
<a href="/pastalist" style="margin-right: 0.5rem; margin-left: 0.5rem">Pasta List</a>
|
||||
|
||||
{% if !args.hide_logo %}
|
||||
<a href="{{ args.public_path }}/"><img
|
||||
width=48
|
||||
style="margin-bottom: -12px;"
|
||||
src="{{ args.public_path }}/static/logo.png"
|
||||
></a>
|
||||
{%- endif %}
|
||||
{{ args.title }}
|
||||
</b>
|
||||
<a href="https://github.com/szabodanika/microbin" style="margin-right: 0.5rem; margin-left: 0.5rem">GitHub</a>
|
||||
|
||||
<a href="{{ args.public_path }}/" style="margin-right: 0.5rem; margin-left: 0.5rem">New
|
||||
</a>
|
||||
|
||||
<a href="{{ args.public_path }}/pastalist" style="margin-right: 0.5rem; margin-left: 0.5rem">List</a>
|
||||
|
||||
<a href="{{ args.public_path }}/info" style="margin-right: 0.5rem; margin-left: 0.5rem">Info</a>
|
||||
|
||||
<hr>
|
||||
|
||||
{%- endif %}
|
||||
<hr>
|
||||
|
|
|
@ -1,252 +1,23 @@
|
|||
{% include "header.html" %}
|
||||
|
||||
<form id="pasta-form" action="upload" method="POST" enctype="multipart/form-data">
|
||||
<form action="create" method="POST">
|
||||
<br>
|
||||
<div id="settings">
|
||||
<div>
|
||||
<label for="expiration">Expiration</label><br>
|
||||
<select style="width: 100%;" name="expiration" id="expiration">
|
||||
<optgroup label="Expire after">
|
||||
{% if args.default_expiry == "1min" %}
|
||||
<option selected value="1min">
|
||||
{%- else %}
|
||||
<option value="1min">
|
||||
{%- endif %}
|
||||
1 minute
|
||||
</option>
|
||||
{% if args.default_expiry == "10min" %}
|
||||
<option selected value="10min">
|
||||
{%- else %}
|
||||
<option value="10min">
|
||||
{%- endif %}
|
||||
10 minutes
|
||||
</option>
|
||||
{% if args.default_expiry == "1hour" %}
|
||||
<option selected value="1hour">
|
||||
{%- else %}
|
||||
<option value="1hour">
|
||||
{%- endif %}
|
||||
1 hour
|
||||
</option>
|
||||
{% if args.default_expiry == "24hour" %}
|
||||
<option selected value="24hour">
|
||||
{%- else %}
|
||||
<option value="24hour">
|
||||
{%- endif %}
|
||||
24 hours
|
||||
</option>
|
||||
{% if args.default_expiry == "3days" %}
|
||||
<option selected value="3days">
|
||||
{%- else %}
|
||||
<option value="3days">
|
||||
{%- endif %}
|
||||
3 days
|
||||
</option>
|
||||
{% if args.default_expiry == "1week" %}
|
||||
<option selected value="1week">
|
||||
{%- else %}
|
||||
<option value="1week">
|
||||
{%- endif %}
|
||||
1 week
|
||||
</option>
|
||||
<select name="expiration" id="expiration">
|
||||
<optgroup label="Expire">
|
||||
<option value="1min">1 minute</option>
|
||||
<option value="10min">10 minutes</option>
|
||||
<option value="1hour">1 hour</option>
|
||||
<option selected value="24hour">24 hours</option>
|
||||
<option value="1week">1 week</option>
|
||||
</optgroup>
|
||||
{% if !args.no_eternal_pasta %}
|
||||
<option value="never">Never Expire</option>
|
||||
{%- endif %}
|
||||
</select>
|
||||
</div>
|
||||
{% if args.enable_burn_after %}
|
||||
<div>
|
||||
<label for="expiration">Burn After</label><br>
|
||||
<select style="width: 100%;" name="burn_after" id="burn_after">
|
||||
<optgroup label="Burn after">
|
||||
{% if args.default_burn_after == 1 %}
|
||||
<option selected value="1">
|
||||
{%- else %}
|
||||
<option value="1">
|
||||
{%- endif %}
|
||||
First Read
|
||||
</option>
|
||||
{% if args.default_burn_after == 10 %}
|
||||
<option selected value="10">
|
||||
{%- else %}
|
||||
<option value="10">
|
||||
{%- endif %}
|
||||
10th Read
|
||||
</option>
|
||||
{% if args.default_burn_after == 100 %}
|
||||
<option selected value="100">
|
||||
{%- else %}
|
||||
<option value="100">
|
||||
{%- endif %}
|
||||
100th Read
|
||||
</option>
|
||||
{% if args.default_burn_after == 1000 %}
|
||||
<option selected value="1000">
|
||||
{%- else %}
|
||||
<option value="1000">
|
||||
{%- endif %}
|
||||
1000th Read
|
||||
</option>
|
||||
{% if args.default_burn_after == 10000 %}
|
||||
<option selected value="10000">
|
||||
{%- else %}
|
||||
<option value="10000">
|
||||
{%- endif %}
|
||||
10000th Read
|
||||
</option>
|
||||
</optgroup>
|
||||
{% if args.default_burn_after == 0 %}
|
||||
<option selected value="0">
|
||||
{%- else %}
|
||||
<option value="0">
|
||||
{%- endif %}
|
||||
No Limit
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
{%- endif %}
|
||||
|
||||
{% if args.highlightsyntax %}
|
||||
<div>
|
||||
<label for="syntax-highlight">Syntax</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>
|
||||
<input type="file" id="file" name="file" />
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if args.readonly %}
|
||||
<b>
|
||||
<!--<input style="width: 140px; float: right; background-color: #0076d18f;" disabled type="submit"-->
|
||||
<!--value="Read Only" /></b>-->
|
||||
<input style="width: 140px; float: right" disabled type="submit"
|
||||
value="Read Only" /></b>
|
||||
{%- else %}
|
||||
<b>
|
||||
<!--<input style="width: 140px; float: right; background-color: #0076d18f;" type="submit" value="Save" />-->
|
||||
<input style="width: 140px; float: right" type="submit" value="Save" />
|
||||
</b>
|
||||
{%- endif %}
|
||||
</div>
|
||||
|
||||
<label>Content</label>
|
||||
<br>
|
||||
<textarea style="width: 100%; min-height: 100px" name="content" autofocus></textarea>
|
||||
<br>
|
||||
<input style="width: 100px; background-color: limegreen"; type="submit" value="Save"/>
|
||||
<br>
|
||||
</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" %}
|
||||
|
|
|
@ -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" %}
|
||||
<div style="float: left">
|
||||
{% if pasta.content != "No Text Content" %}
|
||||
<button id="copy-text-button" class="copy-button" style="margin-right: 0.5rem">
|
||||
Copy Text
|
||||
</button>
|
||||
{% if args.public_path.to_string() != "" && pasta.pasta_type == "url" %}
|
||||
<button id="copy-redirect-button" class="copy-button" style="margin-right: 0.5rem">
|
||||
Copy Redirect
|
||||
</button>
|
||||
{%- endif %}
|
||||
<a style="margin-right: 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>
|
||||
|
||||
<a href="/raw/{{pasta.idAsAnimals()}}">Raw Pasta</a>
|
||||
<pre><code>{{pasta}}</code></pre>
|
||||
{% include "footer.html" %}
|
||||
|
|
|
@ -2,54 +2,47 @@
|
|||
|
||||
|
||||
{% if pastas.is_empty() %}
|
||||
<br>
|
||||
<p>
|
||||
No pastas yet. 😔 Create one <a href="{{ args.public_path }}/">here</a>.
|
||||
No pastas yet. 😔 Create one <a href="/">here</a>.
|
||||
</p>
|
||||
<br>
|
||||
{%- else %}
|
||||
<h3>Pastas</h3>
|
||||
{% if args.pure_html %}
|
||||
<table border="1" style="width: 100%; white-space: nowrap;">
|
||||
{% else %}
|
||||
<table style="width: 100%">
|
||||
{% endif %}
|
||||
<thead>
|
||||
<th style="width: 30%">
|
||||
<br>
|
||||
<table style="width: 100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="4">Pastas</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Key
|
||||
</th>
|
||||
<th style="width: 20%">
|
||||
<th>
|
||||
Created
|
||||
</th>
|
||||
<th style="width: 20%">
|
||||
<th>
|
||||
Expiration
|
||||
</th>
|
||||
<th style="width: 30%">
|
||||
<th>
|
||||
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for pasta in pastas %}
|
||||
{% if pasta.pasta_type == "text" && !pasta.private %}
|
||||
{% if pasta.pasta_type == "text" %}
|
||||
<tr>
|
||||
<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>
|
||||
{{pasta.created_as_string()}}
|
||||
{{pasta.createdAsString()}}
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.expiration_as_string()}}
|
||||
{{pasta.expirationAsString()}}
|
||||
</td>
|
||||
<td>
|
||||
<a style="margin-right:1rem" href="{{ args.public_path }}/{{ args.raw_endpoint }}/{{pasta.id_as_animals()}}">Raw</a>
|
||||
{% if pasta.file.is_some() %}
|
||||
<a style="margin-right:1rem"
|
||||
href="{{ args.public_path }}/file/{{pasta.id_as_animals()}}/{{pasta.file.as_ref().unwrap().name()}}">File</a>
|
||||
{%- endif %}
|
||||
{% 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>
|
||||
<a style="margin-right:1rem" href="/raw/{{pasta.idAsAnimals()}}">Raw</a>
|
||||
<a href="/remove/{{pasta.idAsAnimals()}}">Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
{%- endif %}
|
||||
|
@ -57,64 +50,47 @@
|
|||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
<h3>URL Redirects</h3>
|
||||
{% if args.pure_html %}
|
||||
<table border="1" style="width: 100%">
|
||||
{% else %}
|
||||
<table style="width: 100%">
|
||||
{% endif %}
|
||||
<table>
|
||||
<thead>
|
||||
<th style="width: 30%">
|
||||
<tr>
|
||||
<th colspan="4">URL Redirects</th>
|
||||
</tr>
|
||||
<tr >
|
||||
<th>
|
||||
Key
|
||||
</th>
|
||||
<th style="width: 20%">
|
||||
<th>
|
||||
Created
|
||||
</th>
|
||||
<th style="width: 20%">
|
||||
<th>
|
||||
Expiration
|
||||
</th>
|
||||
<th style="width: 30%">
|
||||
<th>
|
||||
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for pasta in pastas %}
|
||||
{% if pasta.pasta_type == "url" && !pasta.private %}
|
||||
{% if pasta.pasta_type == "url" %}
|
||||
<tr>
|
||||
<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>
|
||||
{{pasta.created_as_string()}}
|
||||
{{pasta.createdAsString()}}
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.expiration_as_string()}}
|
||||
{{pasta.expirationAsString()}}
|
||||
</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; cursor: pointer;" id="copy-button"
|
||||
data-url="{{ args.public_path }}/{{ args.url_endpoint }}/{{pasta.id_as_animals()}}">Copy</a>
|
||||
{% if pasta.editable %}
|
||||
<a style="margin-right:1rem" href="{{ args.public_path }}/edit/{{pasta.id_as_animals()}}">Edit</a>
|
||||
{%- endif %}
|
||||
<a href="{{ args.public_path }}/remove/{{pasta.id_as_animals()}}">Remove</a>
|
||||
<a style="margin-right:1rem" href="/raw/{{pasta.idAsAnimals()}}">Raw</a>
|
||||
<a href="/remove/{{pasta.idAsAnimals()}}">Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
{%- endif %}
|
||||
|
||||
<script>
|
||||
const btn = document.querySelector("#copy-button");
|
||||
btn.addEventListener("click", () => {
|
||||
navigator.clipboard.writeText(btn.dataset.url)
|
||||
btn.innerHTML = "Copied"
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = "Copy"
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
{% include "footer.html" %}
|
||||
</table>
|
||||
<br>
|
||||
{%- endif %}
|
||||
{% 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