Compare commits
No commits in common. "master" and "v2.0.1-beta2" have entirely different histories.
master
...
v2.0.1-bet
44 changed files with 1332 additions and 3092 deletions
|
@ -1,10 +0,0 @@
|
|||
# specifies the linker for compiling to these targets
|
||||
# this needs to be done to allow cross compiling
|
||||
|
||||
# may need more entries for more architectures, if you run into
|
||||
# issues on something else then aarch64 musl open an issue and
|
||||
# point to this comment. This will no longer be necessary when
|
||||
# rust-lld is stabilizes.
|
||||
|
||||
[target.aarch64-unknown-linux-musl]
|
||||
rustflags = ["-Clinker=rust-lld"]
|
50
.env
50
.env
|
@ -38,31 +38,29 @@ export MICROBIN_ADMIN_PASSWORD=m1cr0b1n
|
|||
# finalised pastas but there will be an extra checkbox to
|
||||
# make your new pasta editable from the pasta list or the
|
||||
# pasta view page.
|
||||
# Default value: true
|
||||
# Default value: 8080
|
||||
export MICROBIN_EDITABLE=true
|
||||
|
||||
# Replaces the default footer text with your own. If you
|
||||
# want to hide the footer, use the hide footer option instead.
|
||||
# Note that you can also embed HTML here, so you may want to escape
|
||||
# '<', '>' and so on.
|
||||
# export MICROBIN_FOOTER_TEXT=
|
||||
|
||||
# Hides the navigation bar on every page.
|
||||
# Default value: false
|
||||
# Default value: 8080
|
||||
export MICROBIN_HIDE_HEADER=false
|
||||
|
||||
# Hides the footer on every page.
|
||||
# Default value: false
|
||||
# Default value: 8080
|
||||
export MICROBIN_HIDE_FOOTER=false
|
||||
|
||||
# Hides the MicroBin logo from the navigation bar on every
|
||||
# page.
|
||||
# Default value: false
|
||||
# Default value: 8080
|
||||
export MICROBIN_HIDE_LOGO=false
|
||||
|
||||
# Disables the /pastalist endpoint, essentially making all
|
||||
# pastas private.
|
||||
# Default value: false
|
||||
# Default value: 8080
|
||||
export MICROBIN_NO_LISTING=false
|
||||
|
||||
# Enables syntax highlighting support. When creating a new
|
||||
|
@ -84,20 +82,14 @@ export MICROBIN_BIND="0.0.0.0"
|
|||
# pasta private, which then won't show up on the pastalist
|
||||
# page. With the URL to your pasta, it will still be
|
||||
# accessible.
|
||||
# Default value: true
|
||||
# Default value: false
|
||||
export MICROBIN_PRIVATE=true
|
||||
|
||||
# DEPRECATED: Will be removed soon. If you want to change styling (incl. removal), use custom CSS variable instead.
|
||||
# Disables main CSS styling, just uses a few in-line
|
||||
# stylings for the layout. With this option you will lose
|
||||
# dark-mode support.
|
||||
# dark-mode support.
|
||||
export MICROBIN_PURE_HTML=false
|
||||
|
||||
# Sets the name of the directory where MicroBin creates
|
||||
# its database and stores attachments.
|
||||
# Default value: microbin_data
|
||||
export MICROBIN_DATA_DIR="microbin_data"
|
||||
|
||||
# Enables storing pasta data (not attachments and files) in
|
||||
# a JSON file instead of the SQLite database.
|
||||
# Default value: false
|
||||
|
@ -117,11 +109,8 @@ export MICROBIN_JSON_DB=false
|
|||
# unset.Example value: https://b.in/ export
|
||||
# MICROBIN_SHORT_PATH=
|
||||
|
||||
# The password required for uploading, if read-only mode is enabled
|
||||
# Default value: unset
|
||||
# export MICROBIN_UPLOADER_PASSWORD=
|
||||
|
||||
# If set to true, authentication required for uploading
|
||||
# If set to true, disables adding/editing/removing pastas
|
||||
# entirely.
|
||||
# Default value: false
|
||||
export MICROBIN_READONLY=false
|
||||
|
||||
|
@ -166,9 +155,9 @@ export MICROBIN_WIDE=false
|
|||
# Default value: false
|
||||
export MICROBIN_QR=true
|
||||
|
||||
# Toggles "Never" expiry settings for pastas. Default
|
||||
# Disables "Never" expiry settings for pastas. Default
|
||||
# value: false
|
||||
export MICROBIN_ETERNAL_PASTA=false
|
||||
export MICROBIN_NO_ETERNAL_PASTA=true
|
||||
|
||||
# Enables "Read-only" uploads. These are unlisted and
|
||||
# unencrypted, but can be viewed without password if you
|
||||
|
@ -219,19 +208,4 @@ export MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB=256
|
|||
# encryption (more strain on your server than without
|
||||
# encryption, so the limit should be lower. Secrets tend to
|
||||
# be tiny files usually anyways.) Default value: 2048.
|
||||
export MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB=2048
|
||||
|
||||
# Disables the feature that checks for available updates
|
||||
# when opening the admin screen.
|
||||
# Default value: false
|
||||
export MICROBIN_DISABLE_UPDATE_CHECKING=false
|
||||
|
||||
# Disables telemetry if set to true.
|
||||
# Telemetry includes your configuration and helps development.
|
||||
# It does not include any sensitive data.
|
||||
# Default value: false
|
||||
export MICROBIN_DISABLE_TELEMETRY=false
|
||||
|
||||
# Enables listing your server in the public MicroBin server list.
|
||||
# Default value: false
|
||||
export MICROBIN_LIST_SERVER=false
|
||||
export MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB=2048
|
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
@ -1,4 +1,4 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: szabodanika
|
||||
|
||||
ko_fi: dani_sz
|
||||
|
|
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -1,38 +0,0 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -1,20 +0,0 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
|
@ -93,10 +93,6 @@ jobs:
|
|||
target: ${{ matrix.target }}
|
||||
toolchain: stable
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
|
||||
- name: Install OpenSSL
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get install -y libssl-dev
|
||||
|
||||
- name: Show Version Information (Rust, cargo, GCC)
|
||||
shell: bash
|
||||
|
@ -193,4 +189,4 @@ jobs:
|
|||
linux/arm64
|
||||
push: ${{ github.ref_type == 'tag' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -10,6 +10,5 @@ target/
|
|||
*.pdb
|
||||
|
||||
pasta_data/*
|
||||
microbin_data/*
|
||||
*.env
|
||||
**/**/microbin-data
|
||||
|
|
2523
Cargo.lock
generated
2523
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
71
Cargo.toml
71
Cargo.toml
|
@ -1,70 +1,43 @@
|
|||
[package]
|
||||
name = "microbin"
|
||||
version = "2.0.4"
|
||||
version = "2.0.1"
|
||||
edition = "2021"
|
||||
rust-version = "1.74.0"
|
||||
authors = ["Daniel Szabo <daniel@microbin.eu>"]
|
||||
authors = ["Daniel Szabo <daniel.szabo99@outlook.com>"]
|
||||
license = "BSD-3-Clause"
|
||||
description = "Simple, performant, configurable, entirely self-contained Pastebin and URL shortener."
|
||||
readme = "README.md"
|
||||
homepage = "https://microbin.eu"
|
||||
repository = "https://github.com/szabodanika/microbin"
|
||||
keywords = ["pastebin", "filesharing", "microbin", "actix", "selfhosted"]
|
||||
keywords = ["pastebin", "pastabin", "microbin", "actix", "selfhosted"]
|
||||
categories = ["pastebins"]
|
||||
|
||||
[dependencies]
|
||||
actix-files = "0.6.6"
|
||||
actix-multipart = "0.7.2"
|
||||
actix-web = { version = "4", default-features = false, features = [
|
||||
"compat","compress-brotli", "compress-gzip", "cookies", "http2", "macros", "unicode"] }
|
||||
actix-web-httpauth = "0.8.2"
|
||||
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"] }
|
||||
bytesize = { version = "1.1", features = ["serde"] }
|
||||
chrono = "0.4.19"
|
||||
rand = "0.8.5"
|
||||
linkify = "0.8.1"
|
||||
clap = { version = "3.1.12", features = ["derive", "env"] }
|
||||
env_logger = "0.9.0"
|
||||
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"
|
||||
html-escape = "0.2.13"
|
||||
lazy_static = "1.4.0"
|
||||
linkify = "0.10.0"
|
||||
log = "0.4.21"
|
||||
magic-crypt = "3.1.13"
|
||||
mime_guess = "2.0.4"
|
||||
once_cell = "1.19.0"
|
||||
qrcode-generator = "4.1.9"
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["charset",
|
||||
"http2", "macos-system-configuration", "json", "blocking"] }
|
||||
rusqlite = { version = "0.32", features = ["bundled"], optional = true }
|
||||
rust-embed = "8.3.0"
|
||||
|
||||
# The rustls-rustcrypto version must support the rustls version and the
|
||||
# rustls version must match the one expected by reqwest;
|
||||
rustls = { version = "0.23", default-features = false, features = ["custom-provider"], optional = true }
|
||||
rustls-rustcrypto = { version = "0.0.2-alpha", optional = true }
|
||||
|
||||
sanitize-filename = "0.5.0"
|
||||
serde_json = "1.0.114"
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
syntect = { version = "5.2.0", default-features = false }
|
||||
webpki-roots = { version = "0.26", optional = true }
|
||||
|
||||
[dependencies.openssl]
|
||||
version = "0.10.64"
|
||||
features = ["vendored"]
|
||||
optional = true
|
||||
|
||||
[features]
|
||||
default = ["__default-tls", "__zstd", "__syntect-fast", "dep:rusqlite"]
|
||||
no-c-deps = ["__rustcrypto-tls", "__syntect-rust"]
|
||||
|
||||
__default-tls = ["reqwest/default-tls", "dep:openssl"]
|
||||
__rustcrypto-tls = ["reqwest/rustls-tls-manual-roots-no-provider", "dep:rustls", "dep:rustls-rustcrypto", "webpki-roots"]
|
||||
__syntect-fast = ["syntect/default-onig"]
|
||||
__syntect-rust = ["syntect/default-fancy"]
|
||||
__zstd = ["actix-web/compress-zstd"]
|
||||
magic-crypt = "3.1.12"
|
||||
rusqlite = { version = "0.29.0", features = ["bundled"] }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2022-2023, Dániel Szabó
|
||||
Copyright (c) 2022, Dániel Szabó
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
|
48
README.md
48
README.md
|
@ -7,8 +7,9 @@
|
|||
[](https://crates.io/crates/microbin)
|
||||
[](https://hub.docker.com/r/danielszabo99/microbin)
|
||||
[](https://img.shields.io/docker/pulls/danielszabo99/microbin?label=Docker%20pulls)
|
||||
[](https://discord.gg/3DsyTN7T)
|
||||
|
||||
MicroBin is a super tiny, feature-rich, configurable, self-contained and self-hosted paste bin web application. It is very easy to set up and use, and will only require a few megabytes of memory and disk storage. It takes only a couple minutes to set it up, why not give it a try now?
|
||||
MicroBin is a super tiny, feature rich, configurable, self-contained and self-hosted paste bin web application. It is very easy to set up and use, and will only require a few megabytes of memory and disk storage. It takes only a couple minutes to set it up, why not give it a try now?
|
||||
|
||||
### Check out the Public Test Server at [pub.microbin.eu](https://pub.microbin.eu)!
|
||||
|
||||
|
@ -23,55 +24,58 @@ Or install it manually from [Cargo](https://crates.io/crates/microbin):
|
|||
|
||||
```bash
|
||||
cargo install microbin;
|
||||
curl -L -O https://raw.githubusercontent.com/szabodanika/microbin/master/.env;
|
||||
curl -L -O https://raw.githubusercontent.com/;szabodanika/microbin/master/.env;
|
||||
source .env;
|
||||
microbin
|
||||
```
|
||||
|
||||
On our website [microbin.eu](https://microbin.eu), you will find the following:
|
||||
On our website [microbin.eu](https://microbin.eu) you will find the following:
|
||||
|
||||
- [Screenshots](https://microbin.eu/screenshots/)
|
||||
- [Guide and Documentation](https://microbin.eu/docs/intro)
|
||||
- [Donations and Sponsorships](https://microbin.eu/sponsorship)
|
||||
- [Roadmap](https://microbin.eu/roadmap)
|
||||
- [Quickstart Guide](https://microbin.eu/quickstart/)
|
||||
- [Documentation](https://microbin.eu/documentation/)
|
||||
- [Donations and Sponsorships](https://microbin.eu/donate/)
|
||||
- [Community](https://microbin.eu/community/)
|
||||
|
||||
## Features
|
||||
|
||||
- Is very small
|
||||
- Entirely self-contained executable, MicroBin is a single file!
|
||||
- Animal names instead of random numbers for pasta identifiers (64 animals)
|
||||
- Server-side and client-side encryption
|
||||
- File uploads (e.g. `server.com/file/pig-dog-cat`)
|
||||
- Raw text serving (e.g. `server.com/raw/pig-dog-cat`)
|
||||
- QR code support
|
||||
- File uploads (eg. `server.com/file/pig-dog-cat`)
|
||||
- Raw text serving (eg. `server.com/raw/pig-dog-cat`)
|
||||
- URL shortening and redirection
|
||||
- Animal names instead of random numbers for upload identifiers (64 animals)
|
||||
- SQLite and JSON database support
|
||||
- Private and public, editable and uneditable, automatically and never expiring uploads
|
||||
- QR code support
|
||||
- Very simple database (JSON + files) for portability, easy backups and integration
|
||||
- SQLite support
|
||||
- Private and public, editable and final, automatically and never expiring uploads
|
||||
- Syntax highlighting
|
||||
- Automatic dark mode and custom styling support with very little CSS and only vanilla JS (see [`water.css`](https://github.com/kognise/water.css))
|
||||
- And much more!
|
||||
- Most of the above can be toggled on and off!
|
||||
|
||||
## What is an upload?
|
||||
|
||||
In MicroBin, an upload can be:
|
||||
|
||||
- A text that you want to paste from one machine to another, e.g. some code,
|
||||
- A file that you want to share, e.g. a video that is too large for Discord, a zip with a code project in it or an image,
|
||||
- A URL redirection.
|
||||
- A text that you want to paste from one machine to another, eg. some code,
|
||||
- A file that you want to share, eg. a video that is too large for Discord, a zip with a code project in it or an image,
|
||||
- A URL redirect.
|
||||
|
||||
## When is MicroBin useful?
|
||||
|
||||
You can use MicroBin:
|
||||
|
||||
- As a URL shortener/redirect service,
|
||||
- To send long texts to other people,
|
||||
- To send large files to other people,
|
||||
- To share secrets or sensitive documents securely,
|
||||
- As a URL shortener/redirect service,
|
||||
- To serve content on the web, eg . configuration files for testing, images, or any other file content using the Raw functionality,
|
||||
- To serve content on the web, eg. configuration files for testing, images, or any other file content using the Raw functionality,
|
||||
- To move files between your desktop and a server you access from the console,
|
||||
- As a "postbox" service where people can upload their files or texts, but they cannot see or remove what others sent you,
|
||||
- Or even to take quick notes.
|
||||
- As a "postbox" service where people can upload their files or texts, but they cannot see or remove what others sent you - just disable the upload page
|
||||
- To take notes! Simply create an editable upload.
|
||||
|
||||
...and many other things, why not get creative?
|
||||
|
||||
MicroBin and MicroBin.eu are available under the [BSD 3-Clause License](LICENSE).
|
||||
|
||||
© Dániel Szabó 2022-2024
|
||||
© Dániel Szabó 2022-2023
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
services:
|
||||
microbin:
|
||||
image: danielszabo99/microbin:latest
|
||||
image: danielszabo99/microbin:2.0.0-beta1
|
||||
restart: always
|
||||
ports:
|
||||
- "${MICROBIN_PORT}:8080"
|
||||
volumes:
|
||||
- ./microbin-data:/app/microbin_data
|
||||
- ./microbin-data:/app/pasta_data
|
||||
environment:
|
||||
MICROBIN_BASIC_AUTH_USERNAME: ${MICROBIN_BASIC_AUTH_USERNAME}
|
||||
MICROBIN_BASIC_AUTH_PASSWORD: ${MICROBIN_BASIC_AUTH_PASSWORD}
|
||||
|
@ -21,12 +21,10 @@ services:
|
|||
MICROBIN_BIND: ${MICROBIN_BIND}
|
||||
MICROBIN_PRIVATE: ${MICROBIN_PRIVATE}
|
||||
MICROBIN_PURE_HTML: ${MICROBIN_PURE_HTML}
|
||||
MICROBIN_DATA_DIR: ${MICROBIN_DATA_DIR}
|
||||
MICROBIN_JSON_DB: ${MICROBIN_JSON_DB}
|
||||
MICROBIN_PUBLIC_PATH: ${MICROBIN_PUBLIC_PATH}
|
||||
MICROBIN_SHORT_PATH: ${MICROBIN_SHORT_PATH}
|
||||
MICROBIN_READONLY: ${MICROBIN_READONLY}
|
||||
MICROBIN_UPLOADER_PASSWORD: ${MICROBIN_UPLOADER_PASSWORD}
|
||||
MICROBIN_SHOW_READ_STATS: ${MICROBIN_SHOW_READ_STATS}
|
||||
MICROBIN_TITLE: ${MICROBIN_TITLE}
|
||||
MICROBIN_THREADS: ${MICROBIN_THREADS}
|
||||
|
@ -44,4 +42,4 @@ services:
|
|||
MICROBIN_ENCRYPTION_CLIENT_SIDE: ${MICROBIN_ENCRYPTION_CLIENT_SIDE}
|
||||
MICROBIN_ENCRYPTION_SERVER_SIDE: ${MICROBIN_ENCRYPTION_SERVER_SIDE}
|
||||
MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB: ${MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB}
|
||||
MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB: ${MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB}
|
||||
MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB: ${MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB}
|
67
src/args.rs
67
src/args.rs
|
@ -1,6 +1,5 @@
|
|||
use clap::Parser;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Serialize;
|
||||
use std::convert::Infallible;
|
||||
use std::fmt;
|
||||
use std::net::IpAddr;
|
||||
|
@ -10,7 +9,7 @@ lazy_static! {
|
|||
pub static ref ARGS: Args = Args::parse();
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone, Serialize)]
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
pub struct Args {
|
||||
#[clap(long, env = "MICROBIN_BASIC_AUTH_USERNAME")]
|
||||
|
@ -67,9 +66,6 @@ pub struct Args {
|
|||
#[clap(long, env = "MICROBIN_SHORT_PATH")]
|
||||
pub short_path: Option<PublicUrl>,
|
||||
|
||||
#[clap(long, env = "MICROBIN_UPLOADER_PASSWORD")]
|
||||
pub uploader_password: Option<String>,
|
||||
|
||||
#[clap(long, env = "MICROBIN_READONLY")]
|
||||
pub readonly: bool,
|
||||
|
||||
|
@ -106,9 +102,6 @@ pub struct Args {
|
|||
#[clap(long, env = "MICROBIN_DEFAULT_EXPIRY", default_value = "24hour")]
|
||||
pub default_expiry: String,
|
||||
|
||||
#[clap(long, env = "MICROBIN_DATA_DIR", default_value = "microbin_data")]
|
||||
pub data_dir: String,
|
||||
|
||||
#[clap(short, long, env = "MICROBIN_NO_FILE_UPLOAD")]
|
||||
pub no_file_upload: bool,
|
||||
|
||||
|
@ -118,15 +111,6 @@ pub struct Args {
|
|||
#[clap(long, env = "MICROBIN_HASH_IDS")]
|
||||
pub hash_ids: bool,
|
||||
|
||||
#[clap(long, env = "MICROBIN_LIST_SERVER")]
|
||||
pub list_server: bool,
|
||||
|
||||
#[clap(long, env = "MICROBIN_DISABLE_TELEMETRY")]
|
||||
pub disable_telemetry: bool,
|
||||
|
||||
#[clap(long, env = "MICROBIN_DISABLE_UPDATE_CHECKING")]
|
||||
pub disable_update_checking: bool,
|
||||
|
||||
#[clap(long, env = "MICROBIN_ENCRYPTION_CLIENT_SIDE")]
|
||||
pub encryption_client_side: bool,
|
||||
|
||||
|
@ -166,56 +150,9 @@ impl Args {
|
|||
String::from("")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn without_secrets(self) -> Args {
|
||||
Args {
|
||||
auth_basic_username: None,
|
||||
auth_basic_password: None,
|
||||
auth_admin_username: String::from(""),
|
||||
auth_admin_password: String::from(""),
|
||||
editable: self.editable,
|
||||
footer_text: self.footer_text,
|
||||
hide_footer: self.hide_footer,
|
||||
hide_header: self.hide_header,
|
||||
hide_logo: self.hide_logo,
|
||||
no_listing: self.no_listing,
|
||||
highlightsyntax: self.highlightsyntax,
|
||||
port: self.port,
|
||||
bind: self.bind,
|
||||
private: self.private,
|
||||
pure_html: self.pure_html,
|
||||
json_db: self.json_db,
|
||||
public_path: self.public_path,
|
||||
short_path: self.short_path,
|
||||
uploader_password: None,
|
||||
readonly: self.readonly,
|
||||
show_read_stats: self.show_read_stats,
|
||||
title: self.title,
|
||||
list_server: self.list_server,
|
||||
threads: self.threads,
|
||||
gc_days: self.gc_days,
|
||||
enable_burn_after: self.enable_burn_after,
|
||||
default_burn_after: self.default_burn_after,
|
||||
wide: self.wide,
|
||||
qr: self.qr,
|
||||
eternal_pasta: self.eternal_pasta,
|
||||
enable_readonly: self.enable_readonly,
|
||||
default_expiry: self.default_expiry,
|
||||
data_dir: String::from(""),
|
||||
no_file_upload: self.no_file_upload,
|
||||
custom_css: self.custom_css,
|
||||
hash_ids: self.hash_ids,
|
||||
disable_telemetry: self.disable_telemetry,
|
||||
encryption_client_side: self.encryption_client_side,
|
||||
encryption_server_side: self.encryption_server_side,
|
||||
max_file_size_encrypted_mb: self.max_file_size_encrypted_mb,
|
||||
max_file_size_unencrypted_mb: self.max_file_size_unencrypted_mb,
|
||||
disable_update_checking: self.disable_update_checking,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PublicUrl(pub String);
|
||||
|
||||
impl fmt::Display for PublicUrl {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use crate::args::{Args, ARGS};
|
||||
use crate::pasta::Pasta;
|
||||
use crate::util::misc::remove_expired;
|
||||
use crate::util::version::{fetch_latest_version, Version, CURRENT_VERSION};
|
||||
use crate::AppState;
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::{get, post, web, Error, HttpResponse};
|
||||
|
@ -16,7 +15,6 @@ struct AdminTemplate<'a> {
|
|||
status: &'a String,
|
||||
version_string: &'a String,
|
||||
message: &'a String,
|
||||
update: &'a Option<Version>,
|
||||
}
|
||||
|
||||
#[get("/admin")]
|
||||
|
@ -35,11 +33,11 @@ pub async fn post_admin(
|
|||
let mut password = String::from("");
|
||||
|
||||
while let Some(mut field) = payload.try_next().await? {
|
||||
if field.name() == Some("username") {
|
||||
if field.name() == "username" {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
username.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
|
||||
}
|
||||
} else if field.name() == Some("password") {
|
||||
} else if field.name() == "password" {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
password.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
|
||||
}
|
||||
|
@ -73,32 +71,13 @@ pub async fn post_admin(
|
|||
message = "Warning: You are using the default admin login details. This is a security risk, please change them."
|
||||
}
|
||||
|
||||
let update;
|
||||
|
||||
if !ARGS.disable_update_checking {
|
||||
let latest_version_res = fetch_latest_version().await;
|
||||
if latest_version_res.is_ok() {
|
||||
let latest_version = latest_version_res.unwrap();
|
||||
if latest_version.newer_than_current() {
|
||||
update = Some(latest_version);
|
||||
} else {
|
||||
update = None;
|
||||
}
|
||||
} else {
|
||||
update = None;
|
||||
}
|
||||
} else {
|
||||
update = None;
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(
|
||||
AdminTemplate {
|
||||
pastas: &pastas,
|
||||
args: &ARGS,
|
||||
status: &String::from(status),
|
||||
version_string: &format!("{}", CURRENT_VERSION.long_title),
|
||||
version_string: &String::from("2.0.1-20230704"),
|
||||
message: &String::from(message),
|
||||
update: &update,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
|
|
|
@ -8,7 +8,7 @@ use actix_web::{get, web, HttpResponse};
|
|||
use askama::Template;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "auth_upload.html")]
|
||||
#[template(path = "auth_pasta.html")]
|
||||
struct AuthPasta<'a> {
|
||||
args: &'a Args,
|
||||
id: String,
|
||||
|
@ -19,7 +19,7 @@ struct AuthPasta<'a> {
|
|||
}
|
||||
|
||||
#[get("/auth/{id}")]
|
||||
pub async fn auth_upload(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
|
||||
pub async fn auth_pasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
|
||||
// get access to the pasta collection
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
|
@ -40,7 +40,7 @@ pub async fn auth_upload(data: web::Data<AppState>, id: web::Path<String>) -> Ht
|
|||
status: String::from(""),
|
||||
encrypted_key: pasta.encrypted_key.to_owned().unwrap_or_default(),
|
||||
encrypt_client: pasta.encrypt_client,
|
||||
path: String::from("upload"),
|
||||
path: String::from("pasta"),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
|
@ -54,7 +54,7 @@ pub async fn auth_upload(data: web::Data<AppState>, id: web::Path<String>) -> Ht
|
|||
}
|
||||
|
||||
#[get("/auth/{id}/{status}")]
|
||||
pub async fn auth_upload_with_status(
|
||||
pub async fn auth_pasta_with_status(
|
||||
data: web::Data<AppState>,
|
||||
param: web::Path<(String, String)>,
|
||||
) -> HttpResponse {
|
||||
|
@ -80,7 +80,7 @@ pub async fn auth_upload_with_status(
|
|||
status,
|
||||
encrypted_key: pasta.encrypted_key.to_owned().unwrap_or_default(),
|
||||
encrypt_client: pasta.encrypt_client,
|
||||
path: String::from("upload"),
|
||||
path: String::from("pasta"),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
|
@ -317,78 +317,3 @@ pub async fn auth_file_with_status(
|
|||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
|
||||
}
|
||||
|
||||
#[get("/auth_remove_private/{id}")]
|
||||
pub async fn auth_remove_private(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
|
||||
// get access to the pasta collection
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
let intern_id = if ARGS.hash_ids {
|
||||
hashid_to_u64(&id).unwrap_or(0)
|
||||
} else {
|
||||
to_u64(&id).unwrap_or(0)
|
||||
};
|
||||
|
||||
for (_, pasta) in pastas.iter().enumerate() {
|
||||
if pasta.id == intern_id {
|
||||
return HttpResponse::Ok().content_type("text/html").body(
|
||||
AuthPasta {
|
||||
args: &ARGS,
|
||||
id: id.into_inner(),
|
||||
status: String::from(""),
|
||||
encrypted_key: pasta.encrypted_key.to_owned().unwrap_or_default(),
|
||||
encrypt_client: pasta.encrypt_client,
|
||||
path: String::from("remove"),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
|
||||
}
|
||||
|
||||
#[get("/auth_remove_private/{id}/{status}")]
|
||||
pub async fn auth_remove_private_with_status(
|
||||
data: web::Data<AppState>,
|
||||
param: web::Path<(String, String)>,
|
||||
) -> HttpResponse {
|
||||
// get access to the pasta collection
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
let (id, status) = param.into_inner();
|
||||
|
||||
let intern_id = if ARGS.hash_ids {
|
||||
hashid_to_u64(&id).unwrap_or(0)
|
||||
} else {
|
||||
to_u64(&id).unwrap_or(0)
|
||||
};
|
||||
|
||||
for (_i, pasta) in pastas.iter().enumerate() {
|
||||
if pasta.id == intern_id {
|
||||
return HttpResponse::Ok().content_type("text/html").body(
|
||||
AuthPasta {
|
||||
args: &ARGS,
|
||||
id,
|
||||
status,
|
||||
encrypted_key: pasta.encrypted_key.to_owned().unwrap_or_default(),
|
||||
encrypt_client: pasta.encrypt_client,
|
||||
path: String::from("remove"),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
|
||||
}
|
|
@ -19,33 +19,13 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||
#[template(path = "index.html")]
|
||||
struct IndexTemplate<'a> {
|
||||
args: &'a ARGS,
|
||||
status: String,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
pub async fn index() -> impl Responder {
|
||||
HttpResponse::Ok().content_type("text/html").body(
|
||||
IndexTemplate {
|
||||
args: &ARGS,
|
||||
status: String::from(""),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/{status}")]
|
||||
pub async fn index_with_status(param: web::Path<String>) -> HttpResponse {
|
||||
let status = param.into_inner();
|
||||
|
||||
return HttpResponse::Ok().content_type("text/html").body(
|
||||
IndexTemplate {
|
||||
args: &ARGS,
|
||||
status,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
);
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(IndexTemplate { args: &ARGS }.render().unwrap())
|
||||
}
|
||||
|
||||
pub fn expiration_to_timestamp(expiration: &str, timenow: i64) -> i64 {
|
||||
|
@ -58,9 +38,9 @@ pub fn expiration_to_timestamp(expiration: &str, timenow: i64) -> i64 {
|
|||
"1week" => timenow + 60 * 60 * 24 * 7,
|
||||
"never" => {
|
||||
if ARGS.eternal_pasta {
|
||||
0
|
||||
} else {
|
||||
timenow + 60 * 60 * 24 * 7
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
|
@ -70,14 +50,16 @@ pub fn expiration_to_timestamp(expiration: &str, timenow: i64) -> i64 {
|
|||
}
|
||||
}
|
||||
|
||||
/// receives a file through http Post on url /upload/a-b-c with a, b and c
|
||||
/// different animals. The client sends the post in response to a form.
|
||||
// TODO: form field order might need to be changed. In my testing the attachment
|
||||
// data is nestled between password encryption key etc <21-10-24, dvdsk>
|
||||
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_as_str())))
|
||||
.finish());
|
||||
}
|
||||
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||
|
@ -95,7 +77,7 @@ pub async fn create(
|
|||
extension: String::from(""),
|
||||
private: false,
|
||||
readonly: false,
|
||||
editable: ARGS.editable,
|
||||
editable: true,
|
||||
encrypt_server: false,
|
||||
encrypted_key: Some(String::from("")),
|
||||
encrypt_client: false,
|
||||
|
@ -109,20 +91,9 @@ pub async fn create(
|
|||
|
||||
let mut random_key: String = String::from("");
|
||||
let mut plain_key: String = String::from("");
|
||||
let mut uploader_password = String::from("");
|
||||
|
||||
while let Some(mut field) = payload.try_next().await? {
|
||||
let Some(field_name) = field.name() else {
|
||||
continue;
|
||||
};
|
||||
match field_name {
|
||||
"uploader_password" => {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
uploader_password
|
||||
.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
match field.name() {
|
||||
"random_key" => {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
random_key = std::str::from_utf8(&chunk).unwrap().to_string();
|
||||
|
@ -208,7 +179,7 @@ pub async fn create(
|
|||
}
|
||||
continue;
|
||||
}
|
||||
"syntax_highlight" => {
|
||||
"syntax-highlight" => {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
new_pasta.extension = std::str::from_utf8(&chunk).unwrap().to_string();
|
||||
}
|
||||
|
@ -219,7 +190,7 @@ pub async fn create(
|
|||
continue;
|
||||
}
|
||||
|
||||
let path = field.content_disposition().and_then(|cd| cd.get_filename());
|
||||
let path = field.content_disposition().get_filename();
|
||||
|
||||
let path = match path {
|
||||
Some("") => continue,
|
||||
|
@ -236,15 +207,13 @@ pub async fn create(
|
|||
};
|
||||
|
||||
std::fs::create_dir_all(format!(
|
||||
"{}/attachments/{}",
|
||||
ARGS.data_dir,
|
||||
"./pasta_data/attachments/{}",
|
||||
&new_pasta.id_as_animals()
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let filepath = format!(
|
||||
"{}/attachments/{}/{}",
|
||||
ARGS.data_dir,
|
||||
"./pasta_data/attachments/{}/{}",
|
||||
&new_pasta.id_as_animals(),
|
||||
&file.name()
|
||||
);
|
||||
|
@ -273,14 +242,6 @@ pub async fn create(
|
|||
}
|
||||
}
|
||||
|
||||
if ARGS.readonly && ARGS.uploader_password.is_some() {
|
||||
if uploader_password != ARGS.uploader_password.as_ref().unwrap().to_owned() {
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header(("Location", "/incorrect"))
|
||||
.finish());
|
||||
}
|
||||
}
|
||||
|
||||
let id = new_pasta.id;
|
||||
|
||||
if plain_key != *"" && new_pasta.readonly {
|
||||
|
@ -297,8 +258,7 @@ pub async fn create(
|
|||
|
||||
if new_pasta.file.is_some() && new_pasta.encrypt_server && !new_pasta.readonly {
|
||||
let filepath = format!(
|
||||
"{}/attachments/{}/{}",
|
||||
ARGS.data_dir,
|
||||
"./pasta_data/attachments/{}/{}",
|
||||
&new_pasta.id_as_animals(),
|
||||
&new_pasta.file.as_ref().unwrap().name()
|
||||
);
|
||||
|
@ -309,8 +269,6 @@ pub async fn create(
|
|||
}
|
||||
}
|
||||
|
||||
let encrypt_server = new_pasta.encrypt_server;
|
||||
|
||||
pastas.push(new_pasta);
|
||||
|
||||
for (_, pasta) in pastas.iter().enumerate() {
|
||||
|
@ -325,16 +283,10 @@ pub async fn create(
|
|||
to_animal_names(id)
|
||||
};
|
||||
|
||||
if encrypt_server {
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("Location", format!("/auth/{}/success", slug)))
|
||||
.finish())
|
||||
} else {
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header((
|
||||
"Location",
|
||||
format!("{}/upload/{}", ARGS.public_path_as_str(), slug),
|
||||
))
|
||||
.finish())
|
||||
}
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header((
|
||||
"Location",
|
||||
format!("{}/pasta/{}", ARGS.public_path_as_str(), slug),
|
||||
))
|
||||
.finish())
|
||||
}
|
||||
|
|
|
@ -136,7 +136,7 @@ pub async fn post_edit_private(
|
|||
let mut password = String::from("");
|
||||
|
||||
while let Some(mut field) = payload.try_next().await? {
|
||||
if field.name() == Some("password") {
|
||||
if field.name() == "password" {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
password.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
|
||||
}
|
||||
|
@ -157,7 +157,7 @@ pub async fn post_edit_private(
|
|||
}
|
||||
}
|
||||
|
||||
if found && !pastas[index].encrypt_client {
|
||||
if found {
|
||||
let original_content = pastas[index].content.to_owned();
|
||||
|
||||
// decrypt content temporarily
|
||||
|
@ -224,12 +224,12 @@ pub async fn post_submit_edit_private(
|
|||
let mut new_content = String::from("");
|
||||
|
||||
while let Some(mut field) = payload.try_next().await? {
|
||||
if field.name() == Some("content") {
|
||||
if field.name() == "content" {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
new_content.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
|
||||
}
|
||||
}
|
||||
if field.name() == Some("password") {
|
||||
if field.name() == "password" {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
password = std::str::from_utf8(&chunk).unwrap().to_string();
|
||||
}
|
||||
|
@ -250,7 +250,7 @@ pub async fn post_submit_edit_private(
|
|||
}
|
||||
}
|
||||
|
||||
if found && pastas[index].editable && !pastas[index].encrypt_client {
|
||||
if found && pastas[index].editable {
|
||||
if pastas[index].readonly {
|
||||
let res = decrypt(pastas[index].encrypted_key.as_ref().unwrap(), &password);
|
||||
if res.is_ok() {
|
||||
|
@ -289,7 +289,11 @@ pub async fn post_submit_edit_private(
|
|||
return Ok(HttpResponse::Found()
|
||||
.append_header((
|
||||
"Location",
|
||||
format!("/auth/{}/success", pastas[index].id_as_animals()),
|
||||
format!(
|
||||
"{}/pasta/{}",
|
||||
ARGS.public_path_as_str(),
|
||||
pastas[index].id_as_animals()
|
||||
),
|
||||
))
|
||||
.finish());
|
||||
}
|
||||
|
@ -304,6 +308,12 @@ pub async fn post_edit(
|
|||
id: web::Path<String>,
|
||||
mut payload: Multipart,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
if ARGS.readonly {
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header(("Location", format!("{}/", ARGS.public_path_as_str())))
|
||||
.finish());
|
||||
}
|
||||
|
||||
let id = if ARGS.hash_ids {
|
||||
hashid_to_u64(&id).unwrap_or(0)
|
||||
} else {
|
||||
|
@ -318,12 +328,12 @@ pub async fn post_edit(
|
|||
let mut password = String::from("");
|
||||
|
||||
while let Some(mut field) = payload.try_next().await? {
|
||||
if field.name() == Some("content") {
|
||||
if field.name() == "content" {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
new_content.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
|
||||
}
|
||||
}
|
||||
if field.name() == Some("password") {
|
||||
if field.name() == "password" {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
password = std::str::from_utf8(&chunk).unwrap().to_string();
|
||||
}
|
||||
|
@ -332,7 +342,7 @@ pub async fn post_edit(
|
|||
|
||||
for (i, pasta) in pastas.iter().enumerate() {
|
||||
if pasta.id == id {
|
||||
if pasta.editable && !pasta.encrypt_client {
|
||||
if pasta.editable {
|
||||
if pastas[i].readonly || pastas[i].encrypt_server {
|
||||
if password != *"" {
|
||||
let res = decrypt(pastas[i].encrypted_key.as_ref().unwrap(), &password);
|
||||
|
@ -366,7 +376,7 @@ pub async fn post_edit(
|
|||
.append_header((
|
||||
"Location",
|
||||
format!(
|
||||
"{}/upload/{}",
|
||||
"{}/pasta/{}",
|
||||
ARGS.public_path_as_str(),
|
||||
pastas[i].id_as_animals()
|
||||
),
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
use std::fs::File;
|
||||
use std::fs::{self, File};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::args::ARGS;
|
||||
use crate::util::auth;
|
||||
use crate::util::hashids::to_u64 as hashid_to_u64;
|
||||
use crate::util::misc::remove_expired;
|
||||
use crate::util::{animalnumbers::to_u64, misc::decrypt_file};
|
||||
use crate::AppState;
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::http::header;
|
||||
use actix_web::{get, post, web, Error, HttpResponse};
|
||||
use futures::TryStreamExt;
|
||||
|
||||
#[post("/secure_file/{id}")]
|
||||
pub async fn post_secure_file(
|
||||
data: web::Data<AppState>,
|
||||
id: web::Path<String>,
|
||||
payload: Multipart,
|
||||
mut payload: Multipart,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
// get access to the pasta collection
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
@ -40,18 +39,23 @@ pub async fn post_secure_file(
|
|||
}
|
||||
}
|
||||
|
||||
let password = auth::password_from_multipart(payload).await?;
|
||||
let mut password = String::from("");
|
||||
|
||||
while let Some(mut field) = payload.try_next().await? {
|
||||
if field.name() == "password" {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
password.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
if let Some(ref pasta_file) = pastas[index].file {
|
||||
let file = File::open(format!(
|
||||
"{}/attachments/{}/data.enc",
|
||||
ARGS.data_dir,
|
||||
"./pasta_data/attachments/{}/data.enc",
|
||||
pastas[index].id_as_animals()
|
||||
))?;
|
||||
|
||||
// Not compatible with NamedFile from actix_files (it needs a File
|
||||
// to work therefore secure files do not support streaming
|
||||
let decrypted_data: Vec<u8> = decrypt_file(&password, &file)?;
|
||||
|
||||
// Set the content type based on the file extension
|
||||
|
@ -66,7 +70,6 @@ pub async fn post_secure_file(
|
|||
"Content-Disposition",
|
||||
format!("attachment; filename=\"{}\"", pasta_file.name()),
|
||||
))
|
||||
// TODO: make streaming <21-10-24, dvdsk>
|
||||
.body(decrypted_data);
|
||||
return Ok(response);
|
||||
}
|
||||
|
@ -76,9 +79,8 @@ pub async fn post_secure_file(
|
|||
|
||||
#[get("/file/{id}")]
|
||||
pub async fn get_file(
|
||||
request: actix_web::HttpRequest,
|
||||
id: web::Path<String>,
|
||||
data: web::Data<AppState>,
|
||||
id: web::Path<String>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
// get access to the pasta collection
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
@ -116,25 +118,34 @@ pub async fn get_file(
|
|||
|
||||
// Construct the path to the file
|
||||
let file_path = format!(
|
||||
"{}/attachments/{}/{}",
|
||||
ARGS.data_dir,
|
||||
"./pasta_data/attachments/{}/{}",
|
||||
pastas[index].id_as_animals(),
|
||||
pasta_file.name()
|
||||
);
|
||||
let file_path = PathBuf::from(file_path);
|
||||
|
||||
// This will stream the file and set the content type based on the
|
||||
// file path
|
||||
let file_reponse = actix_files::NamedFile::open(file_path)?;
|
||||
let file_reponse = file_reponse.set_content_disposition(header::ContentDisposition {
|
||||
disposition: header::DispositionType::Attachment,
|
||||
parameters: vec![header::DispositionParam::Filename(
|
||||
pasta_file.name().to_string(),
|
||||
)],
|
||||
});
|
||||
// This takes care of streaming/seeking using the Range
|
||||
// header in the request.
|
||||
return Ok(file_reponse.into_response(&request));
|
||||
// Read the contents of the file into memory
|
||||
// let mut file_content = Vec::new();
|
||||
// let mut file = File::open(&file_path)?;
|
||||
// file.read_exact(&mut file_content)?;
|
||||
|
||||
let file_contents = fs::read(&file_path)?;
|
||||
|
||||
// Set the content type based on the file extension
|
||||
let content_type = mime_guess::from_path(&file_path)
|
||||
.first_or_octet_stream()
|
||||
.to_string();
|
||||
|
||||
// Create an HttpResponse object with the file contents as the response body
|
||||
let response = HttpResponse::Ok()
|
||||
.content_type(content_type)
|
||||
.append_header((
|
||||
"Content-Disposition",
|
||||
format!("attachment; filename=\"{}\"", pasta_file.name()),
|
||||
))
|
||||
.body(file_contents);
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ use crate::args::{Args, ARGS};
|
|||
use crate::endpoints::errors::ErrorTemplate;
|
||||
use crate::pasta::Pasta;
|
||||
use crate::util::animalnumbers::to_u64;
|
||||
use crate::util::auth;
|
||||
use crate::util::db::update;
|
||||
use crate::util::hashids::to_u64 as hashid_to_u64;
|
||||
use crate::util::misc::remove_expired;
|
||||
|
@ -10,11 +9,12 @@ use crate::AppState;
|
|||
use actix_multipart::Multipart;
|
||||
use actix_web::{get, post, web, Error, HttpResponse};
|
||||
use askama::Template;
|
||||
use futures::TryStreamExt;
|
||||
use magic_crypt::{new_magic_crypt, MagicCryptTrait};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "upload.html", escape = "none")]
|
||||
#[template(path = "pasta.html", escape = "none")]
|
||||
struct PastaTemplate<'a> {
|
||||
pasta: &'a Pasta,
|
||||
args: &'a Args,
|
||||
|
@ -121,13 +121,22 @@ fn pastaresponse(
|
|||
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
|
||||
}
|
||||
|
||||
#[post("/upload/{id}")]
|
||||
#[post("/pasta/{id}")]
|
||||
pub async fn postpasta(
|
||||
data: web::Data<AppState>,
|
||||
id: web::Path<String>,
|
||||
payload: Multipart,
|
||||
mut payload: Multipart,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let password = auth::password_from_multipart(payload).await?;
|
||||
let mut password = String::from("");
|
||||
|
||||
while let Some(mut field) = payload.try_next().await? {
|
||||
if field.name() == "password" {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
password.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(pastaresponse(data, id, password))
|
||||
}
|
||||
|
||||
|
@ -135,13 +144,22 @@ pub async fn postpasta(
|
|||
pub async fn postshortpasta(
|
||||
data: web::Data<AppState>,
|
||||
id: web::Path<String>,
|
||||
payload: Multipart,
|
||||
mut payload: Multipart,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let password = auth::password_from_multipart(payload).await?;
|
||||
let mut password = String::from("");
|
||||
|
||||
while let Some(mut field) = payload.try_next().await? {
|
||||
if field.name() == "password" {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
password.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(pastaresponse(data, id, password))
|
||||
}
|
||||
|
||||
#[get("/upload/{id}")]
|
||||
#[get("/pasta/{id}")]
|
||||
pub async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
|
||||
pastaresponse(data, id, String::from(""))
|
||||
}
|
||||
|
@ -287,7 +305,7 @@ pub async fn getrawpasta(
|
|||
|
||||
// send raw content of pasta
|
||||
let response = Ok(HttpResponse::NotFound()
|
||||
.content_type("text/plain; charset=utf-8")
|
||||
.content_type("text/plain")
|
||||
.body(pastas[index].content.to_owned()));
|
||||
|
||||
return response;
|
||||
|
@ -296,16 +314,24 @@ pub async fn getrawpasta(
|
|||
// otherwise send pasta not found error as raw text
|
||||
Ok(HttpResponse::NotFound()
|
||||
.content_type("text/html")
|
||||
.body(String::from("Upload not found! :-(")))
|
||||
.body(String::from("Pasta not found! :-(")))
|
||||
}
|
||||
|
||||
#[post("/raw/{id}")]
|
||||
pub async fn postrawpasta(
|
||||
data: web::Data<AppState>,
|
||||
id: web::Path<String>,
|
||||
payload: Multipart,
|
||||
mut payload: Multipart,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let password = auth::password_from_multipart(payload).await?;
|
||||
let mut password = String::from("");
|
||||
|
||||
while let Some(mut field) = payload.try_next().await? {
|
||||
if field.name() == "password" {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
password.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get access to the pasta collection
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
@ -395,7 +421,7 @@ pub async fn postrawpasta(
|
|||
// otherwise send pasta not found error as raw text
|
||||
Ok(HttpResponse::NotFound()
|
||||
.content_type("text/html")
|
||||
.body(String::from("Upload not found! :-(")))
|
||||
.body(String::from("Pasta not found! :-(")))
|
||||
}
|
||||
|
||||
fn decrypt(text_str: &str, key_str: &str) -> Result<String, magic_crypt::MagicCryptError> {
|
||||
|
|
|
@ -7,13 +7,13 @@ use crate::util::misc::remove_expired;
|
|||
use crate::AppState;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "list.html")]
|
||||
struct ListTemplate<'a> {
|
||||
#[template(path = "pastalist.html")]
|
||||
struct PastaListTemplate<'a> {
|
||||
pastas: &'a Vec<Pasta>,
|
||||
args: &'a Args,
|
||||
}
|
||||
|
||||
#[get("/list")]
|
||||
#[get("/pastalist")]
|
||||
pub async fn list(data: web::Data<AppState>) -> HttpResponse {
|
||||
if ARGS.no_listing {
|
||||
return HttpResponse::Found()
|
||||
|
@ -29,7 +29,7 @@ pub async fn list(data: web::Data<AppState>) -> HttpResponse {
|
|||
pastas.sort_by(|a, b| b.created.cmp(&a.created));
|
||||
|
||||
HttpResponse::Ok().content_type("text/html").body(
|
||||
ListTemplate {
|
||||
PastaListTemplate {
|
||||
pastas: &pastas,
|
||||
args: &ARGS,
|
||||
}
|
|
@ -42,13 +42,13 @@ pub async fn getqr(data: web::Data<AppState>, id: web::Path<String>) -> HttpResp
|
|||
}
|
||||
|
||||
if found {
|
||||
// generate the QR code as an SVG - if its a file or text pastas, this will point to the /upload endpoint, otherwise to the /url endpoint, essentially directly taking the user to the url stored in the pasta
|
||||
// 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_as_str(), &id).as_str(),
|
||||
),
|
||||
_ => misc::string_to_qr_svg(
|
||||
format!("{}/upload/{}", &ARGS.public_path_as_str(), &id).as_str(),
|
||||
format!("{}/pasta/{}", &ARGS.public_path_as_str(), &id).as_str(),
|
||||
),
|
||||
};
|
||||
|
||||
|
|
|
@ -1,20 +1,24 @@
|
|||
use actix_multipart::Multipart;
|
||||
use actix_web::{get, post, web, Error, HttpResponse};
|
||||
use actix_web::{get, web, HttpResponse};
|
||||
|
||||
use crate::args::ARGS;
|
||||
use crate::endpoints::errors::ErrorTemplate;
|
||||
use crate::pasta::PastaFile;
|
||||
use crate::util::animalnumbers::to_u64;
|
||||
use crate::util::auth;
|
||||
use crate::util::db::delete;
|
||||
use crate::util::hashids::to_u64 as hashid_to_u64;
|
||||
use crate::util::misc::{decrypt, remove_expired};
|
||||
use crate::util::misc::remove_expired;
|
||||
use crate::AppState;
|
||||
use askama::Template;
|
||||
use std::fs;
|
||||
|
||||
#[get("/remove/{id}")]
|
||||
pub async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
|
||||
if ARGS.readonly {
|
||||
return HttpResponse::Found()
|
||||
.append_header(("Location", format!("{}/", ARGS.public_path_as_str())))
|
||||
.finish();
|
||||
}
|
||||
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
let id = if ARGS.hash_ids {
|
||||
|
@ -25,21 +29,10 @@ pub async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpRes
|
|||
|
||||
for (i, pasta) in pastas.iter().enumerate() {
|
||||
if pasta.id == id {
|
||||
// if it's encrypted or read-only, it needs password to be deleted
|
||||
if pasta.encrypt_server || pasta.readonly {
|
||||
return HttpResponse::Found()
|
||||
.append_header((
|
||||
"Location",
|
||||
format!("/auth_remove_private/{}", pasta.id_as_animals()),
|
||||
))
|
||||
.finish();
|
||||
}
|
||||
|
||||
// remove the file itself
|
||||
if let Some(PastaFile { name, .. }) = &pasta.file {
|
||||
if fs::remove_file(format!(
|
||||
"{}/attachments/{}/{}",
|
||||
ARGS.data_dir,
|
||||
"./pasta_data/attachments/{}/{}",
|
||||
pasta.id_as_animals(),
|
||||
name
|
||||
))
|
||||
|
@ -50,8 +43,7 @@ pub async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpRes
|
|||
|
||||
// and remove the containing directory
|
||||
if fs::remove_dir(format!(
|
||||
"{}/attachments/{}/",
|
||||
ARGS.data_dir,
|
||||
"./pasta_data/attachments/{}/",
|
||||
pasta.id_as_animals()
|
||||
))
|
||||
.is_err()
|
||||
|
@ -66,7 +58,10 @@ pub async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpRes
|
|||
delete(Some(&pastas), Some(id));
|
||||
|
||||
return HttpResponse::Found()
|
||||
.append_header(("Location", format!("{}/list", ARGS.public_path_as_str())))
|
||||
.append_header((
|
||||
"Location",
|
||||
format!("{}/pastalist", ARGS.public_path_as_str()),
|
||||
))
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
@ -77,99 +72,3 @@ pub async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpRes
|
|||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
|
||||
}
|
||||
|
||||
#[post("/remove/{id}")]
|
||||
pub async fn post_remove(
|
||||
data: web::Data<AppState>,
|
||||
id: web::Path<String>,
|
||||
payload: Multipart,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let id = if ARGS.hash_ids {
|
||||
hashid_to_u64(&id).unwrap_or(0)
|
||||
} else {
|
||||
to_u64(&id.into_inner()).unwrap_or(0)
|
||||
};
|
||||
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
let password = auth::password_from_multipart(payload).await?;
|
||||
|
||||
for (i, pasta) in pastas.iter().enumerate() {
|
||||
if pasta.id == id {
|
||||
if pastas[i].readonly || pastas[i].encrypt_server {
|
||||
if password != *"" {
|
||||
let res = decrypt(pastas[i].content.to_owned().as_str(), &password);
|
||||
if res.is_ok() {
|
||||
// remove the file itself
|
||||
if let Some(PastaFile { name, .. }) = &pasta.file {
|
||||
if fs::remove_file(format!(
|
||||
"{}/attachments/{}/{}",
|
||||
ARGS.data_dir,
|
||||
pasta.id_as_animals(),
|
||||
name
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
log::error!("Failed to delete file {}!", name)
|
||||
}
|
||||
|
||||
// and remove the containing directory
|
||||
if fs::remove_dir(format!(
|
||||
"{}/attachments/{}/",
|
||||
ARGS.data_dir,
|
||||
pasta.id_as_animals()
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
log::error!("Failed to delete directory {}!", name)
|
||||
}
|
||||
}
|
||||
|
||||
// remove it from in-memory pasta list
|
||||
pastas.remove(i);
|
||||
|
||||
delete(Some(&pastas), Some(id));
|
||||
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header((
|
||||
"Location",
|
||||
format!("{}/list", ARGS.public_path_as_str()),
|
||||
))
|
||||
.finish());
|
||||
} else {
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header((
|
||||
"Location",
|
||||
format!("/auth_remove_private/{}/incorrect", pasta.id_as_animals()),
|
||||
))
|
||||
.finish());
|
||||
}
|
||||
} else {
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header((
|
||||
"Location",
|
||||
format!("/auth_remove_private/{}/incorrect", pasta.id_as_animals()),
|
||||
))
|
||||
.finish());
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header((
|
||||
"Location",
|
||||
format!(
|
||||
"{}/upload/{}",
|
||||
ARGS.public_path_as_str(),
|
||||
pastas[i].id_as_animals()
|
||||
),
|
||||
))
|
||||
.finish());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap()))
|
||||
}
|
||||
|
|
48
src/main.rs
48
src/main.rs
|
@ -2,12 +2,11 @@ extern crate core;
|
|||
|
||||
use crate::args::ARGS;
|
||||
use crate::endpoints::{
|
||||
admin, auth_admin, auth_upload, create, edit, errors, file, guide, list,
|
||||
pasta as pasta_endpoint, qr, remove, static_resources,
|
||||
admin, auth_admin, auth_pasta, create, edit, errors, file, guide, pasta as pasta_endpoint,
|
||||
pastalist, qr, remove, static_resources,
|
||||
};
|
||||
use crate::pasta::Pasta;
|
||||
use crate::util::db::read_all;
|
||||
use crate::util::telemetry::start_telemetry_thread;
|
||||
use actix_web::middleware::Condition;
|
||||
use actix_web::{middleware, web, App, HttpServer};
|
||||
use actix_web_httpauth::middleware::HttpAuthentication;
|
||||
|
@ -26,27 +25,23 @@ pub mod util {
|
|||
pub mod auth;
|
||||
pub mod db;
|
||||
pub mod db_json;
|
||||
#[cfg(feature = "default")]
|
||||
pub mod db_sqlite;
|
||||
pub mod hashids;
|
||||
pub mod misc;
|
||||
pub mod syntaxhighlighter;
|
||||
pub mod telemetry;
|
||||
pub mod version;
|
||||
pub mod http_client;
|
||||
}
|
||||
|
||||
pub mod endpoints {
|
||||
pub mod admin;
|
||||
pub mod auth_admin;
|
||||
pub mod auth_upload;
|
||||
pub mod auth_pasta;
|
||||
pub mod create;
|
||||
pub mod edit;
|
||||
pub mod errors;
|
||||
pub mod file;
|
||||
pub mod guide;
|
||||
pub mod list;
|
||||
pub mod pasta;
|
||||
pub mod pastalist;
|
||||
pub mod qr;
|
||||
pub mod remove;
|
||||
pub mod static_resources;
|
||||
|
@ -77,17 +72,16 @@ async fn main() -> std::io::Result<()> {
|
|||
ARGS.port.to_string()
|
||||
);
|
||||
|
||||
match fs::create_dir_all(format!("{}/public", ARGS.data_dir)) {
|
||||
match fs::create_dir_all("./pasta_data/public") {
|
||||
Ok(dir) => dir,
|
||||
Err(error) => {
|
||||
log::error!(
|
||||
"Couldn't create data directory {}/attachments/: {:?}",
|
||||
ARGS.data_dir,
|
||||
"Couldn't create data directory ./pasta_data/attachments/: {:?}",
|
||||
error
|
||||
);
|
||||
panic!(
|
||||
"Couldn't create data directory {}/attachments/: {:?}",
|
||||
ARGS.data_dir, error
|
||||
"Couldn't create data directory ./pasta_data/attachments/: {:?}",
|
||||
error
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -96,10 +90,6 @@ async fn main() -> std::io::Result<()> {
|
|||
pastas: Mutex::new(read_all()),
|
||||
});
|
||||
|
||||
if !ARGS.disable_telemetry {
|
||||
start_telemetry_thread();
|
||||
}
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(data.clone())
|
||||
|
@ -107,17 +97,15 @@ async fn main() -> std::io::Result<()> {
|
|||
.service(create::index)
|
||||
.service(guide::guide)
|
||||
.service(auth_admin::auth_admin)
|
||||
.service(auth_upload::auth_file_with_status)
|
||||
.service(auth_admin::auth_admin_with_status)
|
||||
.service(auth_upload::auth_upload_with_status)
|
||||
.service(auth_upload::auth_raw_pasta_with_status)
|
||||
.service(auth_upload::auth_edit_private_with_status)
|
||||
.service(auth_upload::auth_remove_private_with_status)
|
||||
.service(auth_upload::auth_file)
|
||||
.service(auth_upload::auth_upload)
|
||||
.service(auth_upload::auth_raw_pasta)
|
||||
.service(auth_upload::auth_edit_private)
|
||||
.service(auth_upload::auth_remove_private)
|
||||
.service(auth_pasta::auth_pasta_with_status)
|
||||
.service(auth_pasta::auth_raw_pasta_with_status)
|
||||
.service(auth_pasta::auth_edit_private_with_status)
|
||||
.service(auth_pasta::auth_file)
|
||||
.service(auth_pasta::auth_pasta)
|
||||
.service(auth_pasta::auth_raw_pasta)
|
||||
.service(auth_pasta::auth_edit_private)
|
||||
.service(auth_pasta::auth_file_with_status)
|
||||
.service(pasta_endpoint::getpasta)
|
||||
.service(pasta_endpoint::postpasta)
|
||||
.service(pasta_endpoint::getshortpasta)
|
||||
|
@ -141,9 +129,7 @@ async fn main() -> std::io::Result<()> {
|
|||
.default_service(web::route().to(errors::not_found))
|
||||
.wrap(middleware::Logger::default())
|
||||
.service(remove::remove)
|
||||
.service(remove::post_remove)
|
||||
.service(list::list)
|
||||
.service(create::index_with_status)
|
||||
.service(pastalist::list)
|
||||
.wrap(Condition::new(
|
||||
ARGS.auth_basic_username.is_some()
|
||||
&& ARGS.auth_basic_username.as_ref().unwrap().trim() != "",
|
||||
|
|
44
src/pasta.rs
44
src/pasta.rs
|
@ -10,7 +10,7 @@ use crate::util::animalnumbers::to_animal_names;
|
|||
use crate::util::hashids::to_hashids;
|
||||
use crate::util::syntaxhighlighter::html_highlight;
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Eq)]
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct PastaFile {
|
||||
pub name: String,
|
||||
pub size: ByteSize,
|
||||
|
@ -49,11 +49,11 @@ impl PastaFile {
|
|||
}
|
||||
|
||||
pub fn embeddable(&self) -> bool {
|
||||
self.is_image() || self.is_video()
|
||||
self.is_image() && !self.is_video()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Pasta {
|
||||
pub id: u64,
|
||||
pub content: String,
|
||||
|
@ -111,7 +111,21 @@ impl Pasta {
|
|||
}
|
||||
|
||||
pub fn created_as_string(&self) -> String {
|
||||
Local.timestamp_opt(self.created, 0).map(|date| {
|
||||
let date = Local.timestamp(self.created, 0);
|
||||
format!(
|
||||
"{:02}-{:02} {:02}:{:02}",
|
||||
date.month(),
|
||||
date.day(),
|
||||
date.hour(),
|
||||
date.minute(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn expiration_as_string(&self) -> String {
|
||||
if self.expiration == 0 {
|
||||
String::from("Never")
|
||||
} else {
|
||||
let date = Local.timestamp(self.expiration, 0);
|
||||
format!(
|
||||
"{:02}-{:02} {:02}:{:02}",
|
||||
date.month(),
|
||||
|
@ -119,28 +133,6 @@ impl Pasta {
|
|||
date.hour(),
|
||||
date.minute(),
|
||||
)
|
||||
}).earliest().unwrap_or_else(|| {
|
||||
log::error!("Failed to process created date");
|
||||
String::from("Unknow")
|
||||
})
|
||||
}
|
||||
|
||||
pub fn expiration_as_string(&self) -> String {
|
||||
if self.expiration == 0 {
|
||||
String::from("Never")
|
||||
} else {
|
||||
Local.timestamp_opt(self.expiration, 0).map(|date| {
|
||||
format!(
|
||||
"{:02}-{:02} {:02}:{:02}",
|
||||
date.month(),
|
||||
date.day(),
|
||||
date.hour(),
|
||||
date.minute(),
|
||||
)
|
||||
}).earliest().unwrap_or_else(|| {
|
||||
log::error!("Failed to process expiration");
|
||||
String::from("Never")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,57 +6,48 @@ const ANIMAL_NAMES: &[&str] = &[
|
|||
"deer", "horse", "rat", "wasp", "dog", "jaguar", "raven", "whale", "dove", "koala", "seal",
|
||||
"wolf", "duck", "lion", "shark", "worm", "eagle", "lizard", "sheep", "zebra",
|
||||
];
|
||||
const ANIMAL_COUNT: u64 = ANIMAL_NAMES.len() as u64;
|
||||
|
||||
pub fn to_animal_names(number: u64) -> String {
|
||||
pub fn to_animal_names(mut number: u64) -> String {
|
||||
let mut result: Vec<&str> = Vec::new();
|
||||
|
||||
if number == 0 {
|
||||
return ANIMAL_NAMES[0].parse().unwrap();
|
||||
}
|
||||
|
||||
let mut value = number;
|
||||
while value != 0 {
|
||||
let digit = (value % ANIMAL_COUNT) as usize;
|
||||
value /= ANIMAL_COUNT;
|
||||
result.push(ANIMAL_NAMES[digit]);
|
||||
let mut power = 6;
|
||||
|
||||
loop {
|
||||
let digit = number / ANIMAL_NAMES.len().pow(power) as u64;
|
||||
if !(result.is_empty() && digit == 0) {
|
||||
result.push(ANIMAL_NAMES[digit as usize]);
|
||||
}
|
||||
number -= digit * ANIMAL_NAMES.len().pow(power) as u64;
|
||||
if power > 0 {
|
||||
power -= 1;
|
||||
} else if power == 0 || number == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We calculated the numbers in Little-Endian,
|
||||
// now convert to Big-Endian for backwards compatibility with old data.
|
||||
result.reverse();
|
||||
|
||||
result.join("-")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_animal_names() {
|
||||
assert_eq!(to_animal_names(0), "ant");
|
||||
assert_eq!(to_animal_names(1), "eel");
|
||||
assert_eq!(to_animal_names(64), "eel-ant");
|
||||
assert_eq!(to_animal_names(12345), "sloth-ant-lion");
|
||||
}
|
||||
|
||||
pub fn to_u64(animal_names: &str) -> Result<u64, &str> {
|
||||
let mut result: u64 = 0;
|
||||
|
||||
for animal in animal_names.split('-') {
|
||||
let animals: Vec<&str> = animal_names.split('-').collect();
|
||||
|
||||
let mut pow = animals.len();
|
||||
for animal in animals {
|
||||
pow -= 1;
|
||||
let animal_index = ANIMAL_NAMES.iter().position(|&r| r == animal);
|
||||
match animal_index {
|
||||
None => return Err("Failed to convert animal name to u64!"),
|
||||
Some(idx) => {
|
||||
result = result * ANIMAL_COUNT + (idx as u64);
|
||||
Some(_) => {
|
||||
result += (animal_index.unwrap() * ANIMAL_NAMES.len().pow(pow as u32)) as u64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_animal_name_to_u64() {
|
||||
assert_eq!(to_u64("ant"), Ok(0));
|
||||
assert_eq!(to_u64("eel"), Ok(1));
|
||||
assert_eq!(to_u64("eel-ant"), Ok(64));
|
||||
assert_eq!(to_u64("sloth-ant-lion"), Ok(12345));
|
||||
}
|
||||
|
|
|
@ -1,38 +1,29 @@
|
|||
use actix_multipart::Multipart;
|
||||
use actix_web::dev::ServiceRequest;
|
||||
use actix_web::web::Bytes;
|
||||
use actix_web::{error, Error};
|
||||
use actix_web_httpauth::extractors::basic::BasicAuth;
|
||||
use futures::TryStreamExt;
|
||||
|
||||
use crate::args::ARGS;
|
||||
|
||||
pub async fn auth_validator(
|
||||
req: ServiceRequest,
|
||||
creds: BasicAuth,
|
||||
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
|
||||
match (
|
||||
ARGS.auth_basic_username.as_ref(),
|
||||
ARGS.auth_basic_password.as_ref(),
|
||||
creds.password(),
|
||||
) {
|
||||
(Some(conf_user), Some(conf_pwd), Some(cred_pwd))
|
||||
if creds.user_id() == conf_user && conf_pwd == cred_pwd =>
|
||||
{
|
||||
Ok(req)
|
||||
}
|
||||
_ => Err((error::ErrorBadRequest("Invalid login details."), req)),
|
||||
credentials: BasicAuth,
|
||||
) -> Result<ServiceRequest, Error> {
|
||||
// check if username matches
|
||||
if credentials.user_id().as_ref() == ARGS.auth_basic_username.as_ref().unwrap() {
|
||||
return match ARGS.auth_basic_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."))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn password_from_multipart(mut payload: Multipart) -> Result<String, Error> {
|
||||
let mut password = String::new();
|
||||
|
||||
while let Some(mut field) = payload.try_next().await? {
|
||||
if field.name() == Some("password") {
|
||||
let password_bytes = field.bytes(1024).await.unwrap_or(Ok(Bytes::new()))?;
|
||||
password = String::from_utf8_lossy(&password_bytes).to_string();
|
||||
}
|
||||
}
|
||||
Ok(password)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
use crate::{args::ARGS, pasta::Pasta};
|
||||
|
||||
#[cfg(not(feature = "default"))]
|
||||
const PANIC_MSG: &'static str = "Can not run without argument json-db, this version of microbin was compiled without rusqlite support. Make sure you do not pass in no-default-features during compilation";
|
||||
|
||||
#[cfg(feature = "default")]
|
||||
pub fn read_all() -> Vec<Pasta> {
|
||||
if ARGS.json_db {
|
||||
super::db_json::read_all()
|
||||
|
@ -12,59 +8,34 @@ pub fn read_all() -> Vec<Pasta> {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "default"))]
|
||||
pub fn read_all() -> Vec<Pasta> {
|
||||
if ARGS.json_db {
|
||||
super::db_json::read_all()
|
||||
} else {
|
||||
panic!("{}", PANIC_MSG);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn insert(pastas: Option<&Vec<Pasta>>, pasta: Option<&Pasta>) {
|
||||
if ARGS.json_db {
|
||||
super::db_json::update_all(pastas.expect("Called insert() without passing Pasta vector"));
|
||||
} else {
|
||||
#[cfg(feature = "default")]
|
||||
super::db_sqlite::insert(pasta.expect("Called insert() without passing new Pasta"));
|
||||
#[cfg(not(feature = "default"))]
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn update(pastas: Option<&Vec<Pasta>>, pasta: Option<&Pasta>) {
|
||||
if ARGS.json_db {
|
||||
super::db_json::update_all(pastas.expect("Called update() without passing Pasta vector"));
|
||||
} else {
|
||||
#[cfg(feature = "default")]
|
||||
super::db_sqlite::update(pasta.expect("Called insert() without passing Pasta to update"));
|
||||
#[cfg(not(feature = "default"))]
|
||||
panic!("{}", PANIC_MSG);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn update_all(pastas: &Vec<Pasta>) {
|
||||
if ARGS.json_db {
|
||||
super::db_json::update_all(pastas);
|
||||
} else {
|
||||
#[cfg(feature = "default")]
|
||||
super::db_sqlite::update_all(pastas);
|
||||
#[cfg(not(feature = "default"))]
|
||||
panic!("{}", PANIC_MSG);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn delete(pastas: Option<&Vec<Pasta>>, id: Option<u64>) {
|
||||
if ARGS.json_db {
|
||||
super::db_json::update_all(pastas.expect("Called delete() without passing Pasta vector"));
|
||||
} else {
|
||||
#[cfg(feature = "default")]
|
||||
super::db_sqlite::delete_by_id(id.expect("Called delete() without passing Pasta id"));
|
||||
#[cfg(not(feature = "default"))]
|
||||
panic!("{}", PANIC_MSG);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,19 +15,31 @@ pub fn update_all(pastas: &Vec<Pasta>) {
|
|||
}
|
||||
|
||||
fn save_to_file(pasta_data: &Vec<Pasta>) {
|
||||
// This uses a two stage write. First we write to a new file, if this fails
|
||||
// only the new pasta's are lost. Then we replace the current database with
|
||||
// the new file. This either succeeds or fails. The database is never left
|
||||
// in an undefined state.
|
||||
let tmp_file_path = DATABASE_PATH.to_string() + ".tmp";
|
||||
let tmp_file = File::create(&tmp_file_path).expect(&format!(
|
||||
"failed to create temporary database file for writing. path: {tmp_file_path}"
|
||||
));
|
||||
|
||||
let writer = BufWriter::new(tmp_file);
|
||||
serde_json::to_writer(writer, &pasta_data)
|
||||
.expect("Should be able to write out data to database file");
|
||||
std::fs::rename(tmp_file_path, DATABASE_PATH).expect("Could not update database");
|
||||
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 {} not found!", DATABASE_PATH);
|
||||
file = File::create(DATABASE_PATH);
|
||||
match file {
|
||||
Ok(_) => {
|
||||
log::info!("Database file {} created.", DATABASE_PATH);
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_from_file() -> io::Result<Vec<Pasta>> {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use bytesize::ByteSize;
|
||||
use rusqlite::{params, Connection};
|
||||
|
||||
use crate::{args::ARGS, pasta::PastaFile, Pasta};
|
||||
use crate::{pasta::PastaFile, Pasta};
|
||||
|
||||
static DATABASE_PATH: &str = "pasta_data/database.sqlite";
|
||||
|
||||
pub fn read_all() -> Vec<Pasta> {
|
||||
select_all_from_db()
|
||||
|
@ -12,8 +14,7 @@ pub fn update_all(pastas: &[Pasta]) {
|
|||
}
|
||||
|
||||
pub fn rewrite_all_to_db(pasta_data: &[Pasta]) {
|
||||
let conn = Connection::open(format!("{}/database.sqlite", ARGS.data_dir))
|
||||
.expect("Failed to open SQLite database!");
|
||||
let conn = Connection::open(DATABASE_PATH).expect("Failed to open SQLite database!");
|
||||
|
||||
conn.execute(
|
||||
"
|
||||
|
@ -94,8 +95,7 @@ pub fn rewrite_all_to_db(pasta_data: &[Pasta]) {
|
|||
}
|
||||
|
||||
pub fn select_all_from_db() -> Vec<Pasta> {
|
||||
let conn = Connection::open(format!("{}/database.sqlite", ARGS.data_dir))
|
||||
.expect("Failed to open SQLite database!");
|
||||
let conn = Connection::open(DATABASE_PATH).expect("Failed to open SQLite database!");
|
||||
|
||||
conn.execute(
|
||||
"
|
||||
|
@ -167,8 +167,7 @@ pub fn select_all_from_db() -> Vec<Pasta> {
|
|||
}
|
||||
|
||||
pub fn insert(pasta: &Pasta) {
|
||||
let conn = Connection::open(format!("{}/database.sqlite", ARGS.data_dir))
|
||||
.expect("Failed to open SQLite database!");
|
||||
let conn = Connection::open(DATABASE_PATH).expect("Failed to open SQLite database!");
|
||||
|
||||
conn.execute(
|
||||
"
|
||||
|
@ -239,8 +238,7 @@ pub fn insert(pasta: &Pasta) {
|
|||
}
|
||||
|
||||
pub fn update(pasta: &Pasta) {
|
||||
let conn = Connection::open(format!("{}/database.sqlite", ARGS.data_dir))
|
||||
.expect("Failed to open SQLite database!");
|
||||
let conn = Connection::open(DATABASE_PATH).expect("Failed to open SQLite database!");
|
||||
|
||||
conn.execute(
|
||||
"UPDATE pasta SET
|
||||
|
@ -285,8 +283,7 @@ pub fn update(pasta: &Pasta) {
|
|||
}
|
||||
|
||||
pub fn delete_by_id(id: u64) {
|
||||
let conn = Connection::open(format!("{}/database.sqlite", ARGS.data_dir))
|
||||
.expect("Failed to open SQLite database!");
|
||||
let conn = Connection::open(DATABASE_PATH).expect("Failed to open SQLite database!");
|
||||
|
||||
conn.execute(
|
||||
"DELETE FROM pasta
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
#[cfg(not(any(feature = "default", feature = "__rustcrypto-tls")))]
|
||||
compile_error! {"You must either have the default feature enabled (remove
|
||||
the no-default-features rust argument) or the no-c-deps feature"}
|
||||
|
||||
#[cfg(feature = "default")]
|
||||
pub fn new() -> reqwest::blocking::Client {
|
||||
reqwest::blocking::Client::new()
|
||||
}
|
||||
|
||||
#[cfg(feature = "default")]
|
||||
pub fn new_async() -> reqwest::Client {
|
||||
reqwest::Client::new()
|
||||
}
|
||||
|
||||
#[cfg(feature = "__rustcrypto-tls")]
|
||||
pub fn new() -> reqwest::blocking::Client {
|
||||
reqwest::blocking::Client::builder()
|
||||
.use_preconfigured_tls(tls_config())
|
||||
.build()
|
||||
.expect("Could not create HTTP client.")
|
||||
}
|
||||
|
||||
#[cfg(feature = "__rustcrypto-tls")]
|
||||
pub fn new_async() -> reqwest::Client {
|
||||
reqwest::Client::builder()
|
||||
.use_preconfigured_tls(tls_config())
|
||||
.build()
|
||||
.expect("Could not create HTTP client.")
|
||||
}
|
||||
|
||||
#[cfg(feature = "__rustcrypto-tls")]
|
||||
fn tls_config() -> rustls::ClientConfig {
|
||||
use std::sync::Arc;
|
||||
|
||||
let root_store = rustls::RootCertStore {
|
||||
roots: webpki_roots::TLS_SERVER_ROOTS.into(),
|
||||
};
|
||||
|
||||
let provider = Arc::new(rustls_rustcrypto::provider());
|
||||
rustls::ClientConfig::builder_with_provider(provider)
|
||||
.with_safe_default_protocol_versions()
|
||||
.expect("Should support safe default protocols")
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth()
|
||||
}
|
|
@ -41,8 +41,7 @@ pub fn remove_expired(pastas: &mut Vec<Pasta>) {
|
|||
// remove the file itself
|
||||
if let Some(file) = &p.file {
|
||||
if fs::remove_file(format!(
|
||||
"{}/attachments/{}/{}",
|
||||
ARGS.data_dir,
|
||||
"./pasta_data/attachments/{}/{}",
|
||||
p.id_as_animals(),
|
||||
file.name()
|
||||
))
|
||||
|
@ -52,12 +51,8 @@ pub fn remove_expired(pastas: &mut Vec<Pasta>) {
|
|||
}
|
||||
|
||||
// and remove the containing directory
|
||||
if fs::remove_dir(format!(
|
||||
"{}/attachments/{}/",
|
||||
ARGS.data_dir,
|
||||
p.id_as_animals()
|
||||
))
|
||||
.is_err()
|
||||
if fs::remove_dir(format!("./pasta_data/attachments/{}/", p.id_as_animals()))
|
||||
.is_err()
|
||||
{
|
||||
log::error!("Failed to delete directory {}!", file.name())
|
||||
}
|
||||
|
@ -143,6 +138,7 @@ pub fn decrypt_file(
|
|||
let res = mc.decrypt_bytes_to_bytes(&ciphertext[..]);
|
||||
|
||||
if res.is_err() {
|
||||
println!("{}", res.err().unwrap());
|
||||
return Err("Failed to decrypt file".into());
|
||||
}
|
||||
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
use std::{
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::args::ARGS;
|
||||
|
||||
pub fn start_telemetry_thread() {
|
||||
// Start a new thread that calls the send_telemetry function every 24 hours
|
||||
thread::spawn(|| {
|
||||
let mut last_run = Instant::now();
|
||||
loop {
|
||||
let _ = send_telemetry();
|
||||
|
||||
// Wait for 24 hours since the last run
|
||||
let next_run = last_run + Duration::from_secs(60 * 60 * 24);
|
||||
let now = Instant::now();
|
||||
if next_run > now {
|
||||
thread::sleep(next_run - now);
|
||||
}
|
||||
last_run = Instant::now();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn send_telemetry() -> Result<(), reqwest::Error> {
|
||||
// Convert the telemetry object to JSON
|
||||
let json_body = json!(ARGS.to_owned().without_secrets().to_owned()).to_string();
|
||||
|
||||
// Send the telemetry data to the API
|
||||
crate::util::http_client::new()
|
||||
.post("https://api.microbin.eu/telemetry/")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(json_body)
|
||||
.send()?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Version {
|
||||
pub major: u32,
|
||||
pub minor: u32,
|
||||
pub patch: u32,
|
||||
pub title: Cow<'static, str>,
|
||||
pub long_title: Cow<'static, str>,
|
||||
pub description: Cow<'static, str>,
|
||||
pub date: Cow<'static, str>,
|
||||
pub update_type: Cow<'static, str>,
|
||||
}
|
||||
|
||||
pub static CURRENT_VERSION: Version = Version {
|
||||
major: 2,
|
||||
minor: 0,
|
||||
patch: 4,
|
||||
title: Cow::Borrowed("2.0.4"),
|
||||
long_title: Cow::Borrowed("Version 2.0.4, Build 20230711"),
|
||||
description: Cow::Borrowed("This version includes bug fixes and performance improvements."),
|
||||
date: Cow::Borrowed("2023-07-11"),
|
||||
update_type: Cow::Borrowed("beta"),
|
||||
};
|
||||
|
||||
impl Version {
|
||||
pub fn newer_than(&self, other: &Version) -> bool {
|
||||
if self.major != other.major {
|
||||
self.major > other.major
|
||||
} else if self.minor != other.minor {
|
||||
self.minor > other.minor
|
||||
} else {
|
||||
self.patch > other.patch
|
||||
}
|
||||
}
|
||||
|
||||
pub fn newer_than_current(&self) -> bool {
|
||||
self.newer_than(&CURRENT_VERSION)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_latest_version() -> Result<Version, reqwest::Error> {
|
||||
let url = "https://api.microbin.eu/version/";
|
||||
let http_client = crate::util::http_client::new_async();
|
||||
let response = http_client.get(url).send().await?;
|
||||
let version = response.json::<Version>().await?;
|
||||
|
||||
Ok(version)
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
<div style="height: 200px;">
|
||||
<div style="float: left">
|
||||
<h4>Links</h4>
|
||||
<a href="https://microbin.eu/docs/intro" style="margin-right: 1rem">Documentation and Help</a>
|
||||
<a href="https://microbin.eu/documentation" style="margin-right: 1rem">Documentation and Help</a>
|
||||
<br>
|
||||
<a href="https://github.com/szabodanika/microbin" style="margin-right: 1rem">Source Code</a>
|
||||
<br>
|
||||
|
@ -25,24 +25,13 @@
|
|||
<td>{{status}} </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Uploads</b></td>
|
||||
<td><b>Pastas</b></td>
|
||||
<td>{{pastas.len()}} </td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>Update</h4>
|
||||
{% if update.is_some() %}
|
||||
<p><b>Update available</b> {{update.as_ref().unwrap().long_title}}</p>
|
||||
<p><b>Date</b> {{update.as_ref().unwrap().date}}</p>
|
||||
<p><b>Update type</b> {{update.as_ref().unwrap().update_type}}</p>
|
||||
<p><b>Description</b> {{update.as_ref().unwrap().description}}</p>
|
||||
{%- else %}
|
||||
<p>No updates available.</p>
|
||||
{%- endif %}
|
||||
|
||||
|
||||
{% if message != "" %}
|
||||
<h4>Messages</h4>
|
||||
<p>{{message}}</p>
|
||||
|
@ -90,7 +79,7 @@
|
|||
<tr>
|
||||
<td>
|
||||
<a
|
||||
href="{{ args.public_path_as_str()}}/upload/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
|
||||
href="{{ args.public_path_as_str()}}/pasta/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.created_as_string()}}
|
||||
|
@ -200,7 +189,7 @@
|
|||
<tr>
|
||||
<td>
|
||||
<a
|
||||
href="{{ args.public_path_as_str()}}/upload/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
|
||||
href="{{ args.public_path_as_str()}}/pasta/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.created_as_string()}}
|
||||
|
@ -284,14 +273,14 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>auth_basic_username</td>
|
||||
<td>auth_username</td>
|
||||
{% if args.auth_basic_username.as_ref().is_some() %}
|
||||
<td>set</td>
|
||||
{% else %}
|
||||
<td>unset</td>
|
||||
{% endif %}
|
||||
|
||||
<td>auth_basic_password</td>
|
||||
<td>auth_password</td>
|
||||
{% if args.auth_basic_password.as_ref().is_some() %}
|
||||
<td>set</td>
|
||||
{% else %}
|
||||
|
@ -418,12 +407,6 @@
|
|||
<tr>
|
||||
<td>max_file_size_unencrypted_mb</td>
|
||||
<td>{{ args.max_file_size_unencrypted_mb }} MB</td>
|
||||
<td>uploader_password</td>
|
||||
{% if args.uploader_password.as_ref().is_some() %}
|
||||
<td>set</td>
|
||||
{% else %}
|
||||
<td>unset</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -445,4 +428,4 @@
|
|||
</script>
|
||||
<style>
|
||||
|
||||
</style>
|
||||
</style>
|
|
@ -16,7 +16,7 @@
|
|||
{% include "footer.html" %} {% if !args.pure_html %}
|
||||
<style>
|
||||
#auth-form {
|
||||
background-color: var(--background-alt);
|
||||
background-color: #f7f7f7;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
width: fit-content;
|
||||
|
|
|
@ -3,23 +3,11 @@
|
|||
{% if encrypt_client %}
|
||||
|
||||
<form id="auth-form" method="POST" action="/{{path}}/{{id}}" enctype="multipart/form-data">
|
||||
{% if status == "success" %}
|
||||
<b>
|
||||
Success!
|
||||
</b> <br>
|
||||
{% endif %}
|
||||
<label for="password"> Please enter the
|
||||
password to access or modify this upload. <sup>
|
||||
<label for="password"> Please enter the password to open the upload. <sup>
|
||||
<a href="/guide#encryption">﹖</a></sup></label>
|
||||
<input id="password-field" required placeholder="Password" type="password" autocomplete="off">
|
||||
<input id="password-field" placeholder="Password" type="password" autocomplete="off">
|
||||
<input id="password-hidden" name="password" type="hidden">
|
||||
<button>Okay</button>
|
||||
|
||||
{% if status == "incorrect" %}
|
||||
<b>
|
||||
Incorrect password.
|
||||
</b>
|
||||
{% endif %}
|
||||
<button>Open</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
|
@ -29,18 +17,11 @@
|
|||
const passwordHiddenField = document.getElementById("password-hidden");
|
||||
|
||||
form.onsubmit = function () {
|
||||
|
||||
if (passwordField.value.trim() == "") {
|
||||
passwordField.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
let key = decryptWithPassword(passwordField.value, "{{ encrypted_key }}");
|
||||
|
||||
if (key) {
|
||||
passwordHiddenField.value = key;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function decryptWithPassword(password, encryptedHex) {
|
||||
|
@ -61,11 +42,6 @@
|
|||
{% else %}
|
||||
|
||||
<form id="auth-form" method="POST" action="/{{path}}/{{id}}" enctype="multipart/form-data">
|
||||
{% if status == "success" %}
|
||||
<b>
|
||||
Success!
|
||||
</b> <br>
|
||||
{% endif %}
|
||||
<label for="password" style="margin-bottom: 0.5rem;"> Please enter the
|
||||
password to access or modify this upload. <sup>
|
||||
<a href="/guide#encryption">﹖</a></sup></label>
|
||||
|
@ -73,41 +49,19 @@
|
|||
<button>Okay</button>
|
||||
|
||||
{% if status == "incorrect" %}
|
||||
<b>
|
||||
<p>
|
||||
Incorrect password.
|
||||
</b>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
const form = document.getElementById("auth-form");
|
||||
const passwordField = document.getElementById("password-field");
|
||||
|
||||
form.onsubmit = function () {
|
||||
|
||||
if (passwordField.value.trim() == "") {
|
||||
passwordField.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
let key = decryptWithPassword(passwordField.value, "{{ encrypted_key }}");
|
||||
|
||||
if (key) {
|
||||
passwordHiddenField.value = key;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% include "footer.html" %} {% if !args.pure_html %}
|
||||
<style>
|
||||
#auth-form {
|
||||
background-color: var(--background-alt);
|
||||
background-color: #f7f7f7;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
width: fit-content;
|
|
@ -1,7 +1,7 @@
|
|||
{% include "header.html" %}
|
||||
<form action="/{{ path }}/{{ pasta.id_as_animals() }}" method="POST" enctype="multipart/form-data">
|
||||
<h4>
|
||||
Editing upload '{{ pasta.id_as_animals() }}'
|
||||
Editing pasta '{{ pasta.id_as_animals() }}'
|
||||
</h4>
|
||||
<label>Content</label>
|
||||
<br>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{% if args.footer_text.as_ref().is_none() %}
|
||||
<a href="https://microbin.eu">MicroBin</a> by Dániel Szabó and the FOSS
|
||||
Community. Let's keep the Web <b>compact</b>, <b>accessible</b> and
|
||||
<b>humane</b>! {%- else %} {{ args.footer_text.as_ref().unwrap()|safe }} {%-
|
||||
<b>humane</b>! {%- else %} {{ args.footer_text.as_ref().unwrap() }} {%-
|
||||
endif %}
|
||||
</p>
|
||||
|
||||
|
@ -12,4 +12,4 @@
|
|||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
|
@ -48,7 +48,8 @@ padding-right:0.5rem; line-height: 1.5; font-size: 1.1em; padding-top: 2rem;">
|
|||
margin-left: 0.5rem">New</a>
|
||||
|
||||
{% if !args.no_listing %}
|
||||
<a href="{{ args.public_path_as_str() }}/list" style="margin-right: 0.5rem; margin-left: 0.5rem">List</a>
|
||||
<a href="{{ args.public_path_as_str() }}/pastalist"
|
||||
style="margin-right: 0.5rem; margin-left: 0.5rem">List</a>
|
||||
{%- endif %}
|
||||
|
||||
<a href="{{ args.public_path_as_str() }}/guide" style="margin-right: 0.5rem;
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
"never" %}
|
||||
<option selected value="never">
|
||||
{%- else %}
|
||||
<option value="never">{%- endif %} Never Expire
|
||||
<option value="never">a {%- endif %} Never Expire
|
||||
</option>
|
||||
{%- endif %}
|
||||
</select>
|
||||
|
@ -100,8 +100,8 @@
|
|||
|
||||
{% if args.highlightsyntax %}
|
||||
<div>
|
||||
<label for="syntax_highlight">Syntax <sup> <a href="/guide#syntax">﹖</a></sup></label><br>
|
||||
<select style="width: 100%;" name="syntax_highlight" id="syntax_highlight">
|
||||
<label for="syntax-highlight">Syntax <sup> <a href="/guide#syntax">﹖</a></sup></label><br>
|
||||
<select style="width: 100%;" name="syntax-highlight" id="syntax-highlight">
|
||||
<option value="none">None</option>
|
||||
<optgroup label="Client-Rendered">
|
||||
<option value="auto">Automatic</option>
|
||||
|
@ -138,39 +138,27 @@
|
|||
</select>
|
||||
</div>
|
||||
{%- else %}
|
||||
<input type="hidden" name="syntax_highlight" value="none">
|
||||
<input type="hidden" name="syntax-highlight" value="none">
|
||||
{%- endif %}
|
||||
|
||||
{% if args.encryption_client_side || args.encryption_server_side || args.enable_readonly || args.private %}
|
||||
<div>
|
||||
<label for="privacy">Privacy <sup> <a href="/guide#privacy">﹖</a></sup></label><br>
|
||||
<label for="syntax-highlight">Privacy <sup> <a href="/guide#privacy">﹖</a></sup></label><br>
|
||||
<select style="width: 100%;" name="privacy" id="privacy">
|
||||
<optgroup label="Unencrypted (no password)">
|
||||
<option value="public">Public</option>
|
||||
{% if args.private %}
|
||||
<option value="unlisted">Unlisted</option>
|
||||
{%- endif %}
|
||||
</optgroup>
|
||||
{% if args.enable_readonly %}
|
||||
<optgroup label="Unencrypted (protected)">
|
||||
<option value="readonly">Read-only</option>
|
||||
</optgroup>
|
||||
{%- endif %}
|
||||
{% if args.encryption_client_side || args.encryption_server_side %}
|
||||
<optgroup label="Encrypted">
|
||||
{% if args.encryption_server_side %}
|
||||
<option value="private">Private</option>
|
||||
{%- endif %}
|
||||
{% if args.encryption_client_side%}
|
||||
<option value="secret">Secret</option>
|
||||
{%- endif %}
|
||||
</optgroup>
|
||||
{%- endif %}
|
||||
</select>
|
||||
</div>
|
||||
{%- endif %}
|
||||
|
||||
{% if args.encryption_client_side || args.encryption_server_side || args.enable_readonly %}
|
||||
{% if args.encryption_client_side || args.encryption_server_side %}
|
||||
<div>
|
||||
<label for="password_field">Password <sup><a href="/guide#password">﹖</a></sup></label><br>
|
||||
<input style="width: 130px; height: 28px;" type="password" id="password_field" autocomplete="off" />
|
||||
|
@ -190,28 +178,21 @@
|
|||
<br>
|
||||
<input type="file" id="file" name="file" />
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %} {% if args.readonly %}
|
||||
<b>
|
||||
<input style="width: 140px; float: right; background-color:
|
||||
#2975D2; color: white;" disabled type="submit" value="Read Only" /></b>
|
||||
{%- else %}
|
||||
<b>
|
||||
<input style="width: 140px; float: right; background-color:
|
||||
#2975D2; color: white;" id="submit-button" type="submit" value="Save" />
|
||||
{% if args.readonly %}
|
||||
{% if status == "incorrect" %}
|
||||
<input style="width: 160px; float: right; background-color: rgba(255, 0, 0, 0.137);" type="password"
|
||||
id="uploader_password" name="uploader_password" placeholder="Incorrect password!" />
|
||||
{% else %}
|
||||
<input style="width: 160px; float: right;" type="password" id="uploader_password" name="uploader_password"
|
||||
placeholder="Uploader Password" />
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</b>
|
||||
{%- endif %}
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="content" id="content">
|
||||
<input type="hidden" name="encrypt_client" id="encrypt_client">
|
||||
{% if args.encryption_server_side || args.enable_readonly %}
|
||||
<input name="encrypted_random_key" type="hidden" id="encrypted_random_key" autocomplete="off" />
|
||||
{%- endif %}
|
||||
{% if args.encryption_server_side %}
|
||||
<input name="encrypted_random_key" type="hidden" id="encrypted_random_key" autocomplete="off" /> {%- endif %}
|
||||
<input type="hidden" name="random_key" id="random_key">
|
||||
<input type="hidden" name="plain_key" id="plain_key">
|
||||
</form>
|
||||
|
@ -237,11 +218,8 @@
|
|||
form.onsubmit = async function (event) {
|
||||
event.preventDefault(); // prevent default form submission
|
||||
|
||||
// {% if args.encryption_client_side || args.encryption_server_side || args.enable_readonly %}
|
||||
if (passwordField.value.trim() != "") {
|
||||
// {% if !args.no_file_upload %}
|
||||
if (fileOversized()) return false;
|
||||
// {%- endif %}
|
||||
|
||||
if (privacyDropdown.value == "secret") {
|
||||
let randomKey = Array.from(Array(16), () => Math.floor(Math.random() * 36).toString(36)).join('');
|
||||
|
@ -253,9 +231,7 @@
|
|||
content.value = contentInput.value;
|
||||
}
|
||||
hiddenPlainKeyField.name = "";
|
||||
// {% if !args.no_file_upload %}
|
||||
await encryptFile();
|
||||
// {%- endif %}
|
||||
} else {
|
||||
hiddenPlainKeyField.value = passwordField.value;
|
||||
hiddenEncryptedClientSide.name = "";
|
||||
|
@ -271,47 +247,13 @@
|
|||
hiddenEncryptedClientSide.name = "";
|
||||
content.value = contentInput.value;
|
||||
}
|
||||
// {%- else %}
|
||||
hiddenEncryptedClientSide.name = "";
|
||||
content.value = contentInput.value;
|
||||
// {%- endif %}
|
||||
|
||||
if (contentInput.value.trim() == "" && (hiddenFileButton == undefined || hiddenFileButton.files.length == 0)) {
|
||||
if (contentInput.value.trim() == "" && hiddenFileButton.files.length == 0) {
|
||||
contentInput.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
let showProgress = false;
|
||||
|
||||
submitButton.disabled = true;
|
||||
submitButton.textContent = 'Uploading...';
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/upload', true);
|
||||
|
||||
xhr.upload.onprogress = function (event) {
|
||||
if (showProgress) {
|
||||
const progressPercent = Math.round((event.loaded / event.total) * 100);
|
||||
submitButton.value = `${progressPercent}%`;
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200 || xhr.status === 302) {
|
||||
window.location.href = xhr.responseURL;
|
||||
} else {
|
||||
console.log('Request failed with status:', xhr.status);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const formData = new FormData(form);
|
||||
xhr.send(formData);
|
||||
|
||||
showProgressTimeout = setTimeout(() => {
|
||||
showProgress = true;
|
||||
}, 1000);
|
||||
form.submit();
|
||||
};
|
||||
|
||||
function encryptWithPassword(password, plaintext) {
|
||||
|
@ -322,14 +264,6 @@
|
|||
return aesjs.utils.hex.fromBytes(encryptedBytes);
|
||||
}
|
||||
|
||||
// {% if !args.no_file_upload %}
|
||||
function encryptFileWithPassword(password, bytes) {
|
||||
const passwordBytes = aesjs.utils.utf8.toBytes(password.padStart(32, "#"));
|
||||
const aesCtr = new aesjs.ModeOfOperation.ctr(passwordBytes);
|
||||
const encryptedBytes = aesCtr.encrypt(bytes);
|
||||
return aesjs.utils.hex.fromBytes(encryptedBytes);
|
||||
}
|
||||
|
||||
function fileOversized() {
|
||||
if (hiddenFileButton.files.length > 0) {
|
||||
const fileSize = hiddenFileButton.files.item(0).size;
|
||||
|
@ -361,7 +295,8 @@
|
|||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (event) {
|
||||
const encryptedContents = encryptFileWithPassword(passwordField.value.trim(), new Uint8Array(event.target.result));
|
||||
const fileContents = event.target.result;
|
||||
const encryptedContents = encryptWithPassword(passwordField.value.trim(), fileContents);
|
||||
|
||||
// Replace selected file with its encrypted version
|
||||
const encryptedFile = new File([encryptedContents], file.name, { type: file.type });
|
||||
|
@ -371,7 +306,7 @@
|
|||
hiddenFileButton.files = container.files;
|
||||
resolve(encryptedFile);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
reader.readAsText(file);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
|
@ -399,7 +334,6 @@
|
|||
attachFileButton.textContent = "Attached: " + hiddenFileButton.files[0].name;
|
||||
evt.preventDefault();
|
||||
};
|
||||
// {%- endif %}
|
||||
|
||||
</script>
|
||||
|
||||
|
@ -445,4 +379,4 @@
|
|||
}
|
||||
</style>
|
||||
|
||||
{% include "footer.html" %}
|
||||
{% include "footer.html" %}
|
|
@ -1,197 +0,0 @@
|
|||
{% include "header.html" %}
|
||||
|
||||
|
||||
{% if pastas.is_empty() %}
|
||||
<br>
|
||||
<p>
|
||||
No uploads yet. 😔 Create one <a href="{{ args.public_path_as_str() }}/">here</a>.
|
||||
</p>
|
||||
<br>
|
||||
{%- else %}
|
||||
<h3>Uploads</h3>
|
||||
<div style="width: 100%; overflow-x: auto;">
|
||||
{% if args.pure_html %}
|
||||
<table border="1" style="width: 100%; min-width: 720px; white-space: nowrap;">
|
||||
{% else %}
|
||||
<table style="width: 100%; min-width: 720px;">
|
||||
{% endif %}
|
||||
<thead>
|
||||
<th style="width: 25%">
|
||||
Key
|
||||
</th>
|
||||
<th style="width: 10%">
|
||||
</th>
|
||||
<th style="width: 15%">
|
||||
Created
|
||||
</th>
|
||||
<th style="width: 15%">
|
||||
Expiration
|
||||
</th>
|
||||
<th style="width: 15%">
|
||||
Contents
|
||||
</th>
|
||||
|
||||
<th style="width: 10%">
|
||||
</th>
|
||||
<th style="width: 10%">
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for pasta in pastas %}
|
||||
{% if pasta.pasta_type == "text" && !pasta.private %}
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
href="{{ args.public_path_as_str()}}/upload/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if args.public_path_as_str() != "" %}
|
||||
{% if args.short_path_as_str() == "" %}
|
||||
<a style="margin-right:1rem; cursor: pointer;" class="copy-button" null
|
||||
data-url="{{ args.public_path_as_str()}}/upload/{{pasta.id_as_animals()}}">Copy</a>
|
||||
{% else %}
|
||||
<a style="margin-right:1rem; cursor: pointer;" class="copy-button" data-url="{{ args.short_path_as_str()
|
||||
}}/p/{{pasta.id_as_animals()}}">Copy</a>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.created_as_string()}}
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.expiration_as_string()}}
|
||||
</td>
|
||||
<td>
|
||||
{% if pasta.content != "" %}
|
||||
<a style="margin-right:1rem"
|
||||
href="{{ args.public_path_as_str()}}/raw/{{pasta.id_as_animals()}}">Text</a>
|
||||
{%- endif %}
|
||||
{% if pasta.file.is_some() %}
|
||||
<a style="margin-right:1rem"
|
||||
href="{{ args.public_path_as_str() }}/file/{{pasta.id_as_animals()}}">
|
||||
{% if pasta.file.as_ref().unwrap().is_image() %}
|
||||
Image
|
||||
{%- else if pasta.file.as_ref().unwrap().is_video() %}
|
||||
Video
|
||||
{%- else %}
|
||||
File
|
||||
{%- endif %}
|
||||
</a>
|
||||
{%- endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{% if pasta.editable %}
|
||||
<a style="margin-right:1rem"
|
||||
href="{{ args.public_path_as_str() }}/edit/{{pasta.id_as_animals()}}">Edit</a>
|
||||
{%- endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ args.public_path_as_str() }}/remove/{{pasta.id_as_animals()}}">Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
<h3>URL Redirects</h3>
|
||||
{% if args.pure_html %}
|
||||
<table border="1" style="width: 100%; min-width: 720px; ">
|
||||
{% else %}
|
||||
<table style="width: 100%; min-width: 720px;">
|
||||
{% endif %}
|
||||
<thead>
|
||||
<th style=" width: 25%">
|
||||
Key
|
||||
</th>
|
||||
<th style="width: 10%">
|
||||
</th>
|
||||
<th style="width: 15%">
|
||||
Created
|
||||
</th>
|
||||
<th style="width: 15%">
|
||||
Expiration
|
||||
</th>
|
||||
<th style="width: 15%">
|
||||
</th>
|
||||
<th style="width: 10%">
|
||||
</th>
|
||||
<th style="width: 10%">
|
||||
</th>
|
||||
</thead>
|
||||
{% for pasta in pastas %}
|
||||
{% if pasta.pasta_type == "url" && !pasta.private %}
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
href="{{ args.public_path_as_str() }}/upload/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if args.short_path_as_str() == "" %}
|
||||
<a style="margin-right:1rem; cursor: pointer;" class="copy-button"
|
||||
data-url="{{ args.public_path_as_str() }}/url/{{pasta.id_as_animals()}}">Copy</a>
|
||||
{% else %}
|
||||
<a style="margin-right:1rem; cursor: pointer;" class="copy-button" data-url="{{ args.short_path_as_str()
|
||||
}}/u/{{pasta.id_as_animals()}}">Copy</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.created_as_string()}}
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.expiration_as_string()}}
|
||||
</td>
|
||||
<td>
|
||||
<a style="margin-right:1rem"
|
||||
href="{{ args.public_path_as_str() }}/url/{{pasta.id_as_animals()}}">Redirect</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if pasta.editable %}
|
||||
<a style="margin-right:1rem"
|
||||
href="{{ args.public_path_as_str() }}/edit/{{pasta.id_as_animals()}}">Edit</a>
|
||||
{%- endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ args.public_path_as_str() }}/remove/{{pasta.id_as_animals()}}">Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
{%- endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const copyURLBtns = document.getElementsByClassName("copy-button");
|
||||
|
||||
for (var i = 0; i < copyURLBtns.length; i++) {
|
||||
copyURLBtns.item(i).addEventListener("click", event => {
|
||||
event.srcElement
|
||||
navigator.clipboard.writeText(event.srcElement.getAttribute("data-url"))
|
||||
event.srcElement.innerHTML = "Copied"
|
||||
setTimeout(() => {
|
||||
event.srcElement.innerHTML = "Copy"
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.copy-url-button {
|
||||
font-size: small;
|
||||
padding: 4px;
|
||||
padding-left: 0.8rem;
|
||||
padding-right: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% include "footer.html" %}
|
|
@ -16,14 +16,14 @@
|
|||
Content</a>
|
||||
{%- endif %} {% if args.qr && args.public_path_as_str() != "" %}
|
||||
<a style="margin-right: 1rem" href="{{ args.public_path_as_str() }}/qr/{{pasta.id_as_animals()}}">QR</a>
|
||||
{%- endif %} {% if pasta.editable && !pasta.encrypt_client %}
|
||||
{%- endif %} {% if pasta.editable %}
|
||||
<a style="margin-right: 1rem" href="{{ args.public_path_as_str() }}/edit/{{pasta.id_as_animals()}}">Edit</a>
|
||||
{%- endif %}
|
||||
<a style="margin-right: 1rem" href="{{ args.public_path_as_str() }}/remove/{{pasta.id_as_animals()}}">Remove</a>
|
||||
</div>
|
||||
<div style="float: right">
|
||||
<a style="margin-right: 0.5rem"
|
||||
href="{{ args.public_path_as_str() }}/upload/{{pasta.id_as_animals()}}"><i>{{pasta.id_as_animals()}}</i></a>
|
||||
href="{{ args.public_path_as_str() }}/pasta/{{pasta.id_as_animals()}}"><i>{{pasta.id_as_animals()}}</i></a>
|
||||
{% if args.public_path_as_str() != "" %}
|
||||
<button id="copy-url-button" class="small-button" style="margin-right: 0">
|
||||
Copy URL
|
||||
|
@ -33,26 +33,23 @@
|
|||
|
||||
<br>
|
||||
<br>
|
||||
{% if pasta.encrypt_client %}
|
||||
{% if pasta.encrypt_client && pasta.file.is_some() && !pasta.file_embeddable() %}
|
||||
<span style="margin-left: auto; margin-right: auto; display: flex;
|
||||
justify-content: center; align-items: center;">
|
||||
<div id="decryption">
|
||||
{% if pasta.encrypt_client %}
|
||||
<label for="password-field" style="margin-bottom: 0.5em;">
|
||||
Please enter your key to decrypt this upload. <sup> <a href="/guide#encryption">﹖</a></sup>
|
||||
</label>
|
||||
<input class="small-button" placeholder="Key" style="margin-right: 0.5rem" type="password" id="password-field"
|
||||
autocomplete="off" />
|
||||
{% if pasta.content != "" %}
|
||||
|
||||
<button class="small-button" id="decrypt-button" style="margin-right:
|
||||
0.5rem">
|
||||
0.5rem">
|
||||
<b>
|
||||
Decrypt text
|
||||
</b>
|
||||
</button>
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{% if pasta.file.is_some() && !pasta.file_embeddable() %}
|
||||
|
||||
<button class="small-button" id="download-button" style="margin-right:
|
||||
0.5rem">
|
||||
<b>
|
||||
|
@ -60,11 +57,10 @@ justify-content: center; align-items: center;">
|
|||
[{{pasta.file.as_ref().unwrap().size}}]
|
||||
</b>
|
||||
</button>
|
||||
{%- endif %}
|
||||
</div>
|
||||
</span>
|
||||
{%- endif %}
|
||||
|
||||
{%- endif %}
|
||||
<br>
|
||||
|
||||
{% if pasta.content != "" %}
|
||||
|
@ -81,19 +77,6 @@ justify-content: center; align-items: center;">
|
|||
</div>
|
||||
{%- endif %}
|
||||
|
||||
{% if pasta.file.is_some() && !pasta.file_embeddable() && !pasta.encrypt_client %}
|
||||
<span style="margin-left: auto; margin-right: auto; display: flex;
|
||||
justify-content: center; align-items: center;">
|
||||
<p style="font-size: small;">{{pasta.file.as_ref().unwrap().name()}}
|
||||
[{{pasta.file.as_ref().unwrap().size}}]</p>
|
||||
<a href="{{ args.public_path_as_str()}}/file/{{pasta.id_as_animals()}}" id="download-link">
|
||||
<button class="download-button" autofocus>
|
||||
Download
|
||||
</button>
|
||||
</a>
|
||||
</span>
|
||||
{%- endif %}
|
||||
|
||||
|
||||
{% if pasta.file.is_some() && pasta.file.as_ref().unwrap().is_image() &&
|
||||
pasta.file_embeddable() && !pasta.encrypt_client %}
|
||||
|
@ -110,7 +93,6 @@ pasta.file_embeddable() && !pasta.encrypt_client %}
|
|||
</span>
|
||||
{%- endif %}
|
||||
|
||||
|
||||
{% if pasta.file.is_some() && pasta.file.as_ref().unwrap().is_video() &&
|
||||
pasta.file_embeddable() && !pasta.encrypt_client %}
|
||||
<video id="embed" controls src="{{ args.public_path_as_str()}}/file/{{pasta.id_as_animals()}}" height="300"></video>
|
||||
|
@ -148,7 +130,7 @@ pasta.file_embeddable() && !pasta.encrypt_client %}
|
|||
const copyRedirectBtn = document.getElementById("copy-redirect-button")
|
||||
var content = `{{ pasta.content_escaped() }}`
|
||||
const contentElement = document.getElementById("code");
|
||||
const url = (`{{ args.short_path_as_str()}}` === "") ? `{{ args.public_path_as_str() }}/upload/{{pasta.id_as_animals()}}` : `{{ args.short_path_as_str()}}/p/{{pasta.id_as_animals()}}`
|
||||
const url = (`{{ args.short_path_as_str()}}` === "") ? `{{ args.public_path_as_str() }}/pasta/{{pasta.id_as_animals()}}` : `{{ args.short_path_as_str()}}/p/{{pasta.id_as_animals()}}`
|
||||
const redirect_url = (`{{ args.short_path_as_str()}}` === "") ? `{{ args.public_path_as_str() }}/url/{{pasta.id_as_animals()}}` : `{{ args.short_path_as_str()}}/u/{{pasta.id_as_animals()}}`
|
||||
|
||||
const te = new TextEncoder();
|
||||
|
@ -223,7 +205,6 @@ pasta.file_embeddable() && !pasta.encrypt_client %}
|
|||
const passwordField = document.getElementById("password-field");
|
||||
const downloadButton = document.getElementById('download-button');
|
||||
|
||||
// {% if pasta.file.is_some() %}
|
||||
// Set up event listener for download link click
|
||||
downloadButton.addEventListener('click', async (event) => {
|
||||
event.preventDefault(); // prevent default click behavior
|
||||
|
@ -250,29 +231,26 @@ pasta.file_embeddable() && !pasta.encrypt_client %}
|
|||
const encryptedFile = await response.text();
|
||||
|
||||
// Decrypt file contents
|
||||
const decryptedContents = decryptFileWithPassword(passwordField.value.trim(), encryptedFile);
|
||||
const decryptedContents = decryptWithPassword(passwordField.value.trim(), encryptedFile);
|
||||
if (!decryptedContents) {
|
||||
throw new Error('Failed to decrypt file');
|
||||
}
|
||||
|
||||
// Create blob from decrypted file contents
|
||||
const decryptedBlob = new Blob([decryptedContents], { type: 'application/octet-stream' });
|
||||
|
||||
// Create data URI for decrypted file
|
||||
// const dataUri = `data:application/octet-stream;${encodeURIComponent(decryptedContents)}`;
|
||||
const dataUri = `data:text/plain;charset=utf-8,${encodeURIComponent(decryptedContents)}`;
|
||||
|
||||
// Create temporary anchor element
|
||||
const tempAnchorEl = document.createElement('a');
|
||||
// tempAnchorEl.href = dataUri;
|
||||
tempAnchorEl.href = URL.createObjectURL(decryptedBlob);
|
||||
tempAnchorEl.href = dataUri;
|
||||
tempAnchorEl.download = '{{pasta.file.as_ref().unwrap().name()}}';
|
||||
|
||||
// Programmatically click anchor element to trigger download
|
||||
tempAnchorEl.click();
|
||||
|
||||
// Remove temporary anchor element
|
||||
document.re(tempAnchorEl);
|
||||
// {%- endif %}
|
||||
});
|
||||
// {% endif %}
|
||||
|
||||
decryptButton.addEventListener("click", () => {
|
||||
password = passwordField.value;
|
||||
|
@ -294,21 +272,12 @@ pasta.file_embeddable() && !pasta.encrypt_client %}
|
|||
const aesCtr = new aesjs.ModeOfOperation.ctr(passwordBytes);
|
||||
const decryptedBytes = aesCtr.decrypt(encryptedBytes);
|
||||
const res = aesjs.utils.utf8.fromBytes(decryptedBytes);
|
||||
|
||||
if (res.endsWith("!0K")) {
|
||||
return res.substring(0, res.length - "!0K".length);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function decryptFileWithPassword(password, encryptedHex) {
|
||||
const passwordBytes = aesjs.utils.utf8.toBytes(password.padStart(32, "#"));
|
||||
const encryptedBytes = aesjs.utils.hex.toBytes(encryptedHex);
|
||||
const aesCtr = new aesjs.ModeOfOperation.ctr(passwordBytes);
|
||||
const decryptedBytes = aesCtr.decrypt(encryptedBytes);
|
||||
return decryptedBytes;
|
||||
}
|
||||
// {% endif %}
|
||||
</script>
|
||||
|
194
templates/pastalist.html
Normal file
194
templates/pastalist.html
Normal file
|
@ -0,0 +1,194 @@
|
|||
{% include "header.html" %}
|
||||
|
||||
|
||||
{% if pastas.is_empty() %}
|
||||
<br>
|
||||
<p>
|
||||
No uploads yet. 😔 Create one <a href="{{ args.public_path_as_str() }}/">here</a>.
|
||||
</p>
|
||||
<br>
|
||||
{%- else %}
|
||||
<h3>Uploads</h3>
|
||||
{% if args.pure_html %}
|
||||
<table border="1" style="width: 100%; white-space: nowrap;">
|
||||
{% else %}
|
||||
<table style="width: 100%">
|
||||
{% endif %}
|
||||
<thead>
|
||||
<th style="width: 25%">
|
||||
Key
|
||||
</th>
|
||||
<th style="width: 10%">
|
||||
</th>
|
||||
<th style="width: 15%">
|
||||
Created
|
||||
</th>
|
||||
<th style="width: 15%">
|
||||
Expiration
|
||||
</th>
|
||||
<th style="width: 15%">
|
||||
Contents
|
||||
</th>
|
||||
|
||||
<th style="width: 10%">
|
||||
</th>
|
||||
<th style="width: 10%">
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for pasta in pastas %}
|
||||
{% if pasta.pasta_type == "text" && !pasta.private %}
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
href="{{ args.public_path_as_str()}}/pasta/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if args.public_path_as_str() != "" %}
|
||||
{% if args.short_path_as_str() == "" %}
|
||||
<a style="margin-right:1rem; cursor: pointer;" class="copy-button" null
|
||||
data-url="{{ args.public_path_as_str()}}/pasta/{{pasta.id_as_animals()}}">Copy</a>
|
||||
{% else %}
|
||||
<a style="margin-right:1rem; cursor: pointer;" class="copy-button" data-url="{{ args.short_path_as_str()
|
||||
}}/p/{{pasta.id_as_animals()}}">Copy</a>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.created_as_string()}}
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.expiration_as_string()}}
|
||||
</td>
|
||||
<td>
|
||||
{% if pasta.content != "" %}
|
||||
<a style="margin-right:1rem"
|
||||
href="{{ args.public_path_as_str()}}/raw/{{pasta.id_as_animals()}}">Text</a>
|
||||
{%- endif %}
|
||||
{% if pasta.file.is_some() %}
|
||||
<a style="margin-right:1rem" href="{{ args.public_path_as_str() }}/file/{{pasta.id_as_animals()}}">
|
||||
{% if pasta.file.as_ref().unwrap().is_image() %}
|
||||
Image
|
||||
{%- else if pasta.file.as_ref().unwrap().is_video() %}
|
||||
Video
|
||||
{%- else %}
|
||||
File
|
||||
{%- endif %}
|
||||
</a>
|
||||
{%- endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{% if pasta.editable %}
|
||||
<a style="margin-right:1rem"
|
||||
href="{{ args.public_path_as_str() }}/edit/{{pasta.id_as_animals()}}">Edit</a>
|
||||
{%- endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ args.public_path_as_str() }}/remove/{{pasta.id_as_animals()}}">Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
<h3>URL Redirects</h3>
|
||||
{% if args.pure_html %}
|
||||
<table border="1" style="width: 100%">
|
||||
{% else %}
|
||||
<table style="width: 100%">
|
||||
{% endif %}
|
||||
<thead>
|
||||
<th style="width: 25%">
|
||||
Key
|
||||
</th>
|
||||
<th style="width: 10%">
|
||||
</th>
|
||||
<th style="width: 15%">
|
||||
Created
|
||||
</th>
|
||||
<th style="width: 15%">
|
||||
Expiration
|
||||
</th>
|
||||
<th style="width: 15%">
|
||||
</th>
|
||||
<th style="width: 10%">
|
||||
</th>
|
||||
<th style="width: 10%">
|
||||
</th>
|
||||
</thead>
|
||||
{% for pasta in pastas %}
|
||||
{% if pasta.pasta_type == "url" && !pasta.private %}
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
href="{{ args.public_path_as_str() }}/pasta/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if args.short_path_as_str() == "" %}
|
||||
<a style="margin-right:1rem; cursor: pointer;" class="copy-button"
|
||||
data-url="{{ args.public_path_as_str() }}/url/{{pasta.id_as_animals()}}">Copy</a>
|
||||
{% else %}
|
||||
<a style="margin-right:1rem; cursor: pointer;" class="copy-button" data-url="{{ args.short_path_as_str()
|
||||
}}/u/{{pasta.id_as_animals()}}">Copy</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.created_as_string()}}
|
||||
</td>
|
||||
<td>
|
||||
{{pasta.expiration_as_string()}}
|
||||
</td>
|
||||
<td>
|
||||
<a style="margin-right:1rem"
|
||||
href="{{ args.public_path_as_str() }}/url/{{pasta.id_as_animals()}}">Redirect</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if pasta.editable %}
|
||||
<a style="margin-right:1rem"
|
||||
href="{{ args.public_path_as_str() }}/edit/{{pasta.id_as_animals()}}">Edit</a>
|
||||
{%- endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ args.public_path_as_str() }}/remove/{{pasta.id_as_animals()}}">Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
{%- endif %}
|
||||
|
||||
<script>
|
||||
const copyURLBtns = document.getElementsByClassName("copy-button");
|
||||
|
||||
for (var i = 0; i < copyURLBtns.length; i++) {
|
||||
copyURLBtns.item(i).addEventListener("click", event => {
|
||||
event.srcElement
|
||||
navigator.clipboard.writeText(event.srcElement.getAttribute("data-url"))
|
||||
event.srcElement.innerHTML = "Copied"
|
||||
setTimeout(() => {
|
||||
event.srcElement.innerHTML = "Copy"
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.copy-url-button {
|
||||
font-size: small;
|
||||
padding: 4px;
|
||||
padding-left: 0.8rem;
|
||||
padding-right: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% include "footer.html" %}
|
|
@ -1,7 +1,7 @@
|
|||
{% include "header.html" %}
|
||||
|
||||
<div style="float: left">
|
||||
<a href="{{ args.public_path_as_str() }}/upload/{{pasta.id_as_animals()}}">Back to Upload</a>
|
||||
<a href="{{ args.public_path_as_str() }}/pasta/{{pasta.id_as_animals()}}">Back to Pasta</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
|||
{{qr}}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ args.public_path_as_str() }}/upload/{{pasta.id_as_animals()}}">
|
||||
<a href="{{ args.public_path_as_str() }}/pasta/{{pasta.id_as_animals()}}">
|
||||
{{qr}}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
|
Loading…
Add table
Reference in a new issue