Compare commits

..

No commits in common. "master" and "v2.0.1-beta2" have entirely different histories.

44 changed files with 1332 additions and 3092 deletions

View file

@ -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
View file

@ -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
View file

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

View file

@ -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.

View file

@ -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.

View file

@ -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
View file

@ -10,6 +10,5 @@ target/
*.pdb
pasta_data/*
microbin_data/*
*.env
**/**/microbin-data

2523
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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

View file

@ -7,8 +7,9 @@
[![crates.io](https://img.shields.io/crates/v/microbin.svg)](https://crates.io/crates/microbin)
[![Docker Image](https://github.com/szabodanika/microbin/actions/workflows/release.yml/badge.svg)](https://hub.docker.com/r/danielszabo99/microbin)
[![Docker Pulls](https://img.shields.io/docker/pulls/danielszabo99/microbin?label=Docker%20pulls)](https://img.shields.io/docker/pulls/danielszabo99/microbin?label=Docker%20pulls)
[![Support Server](https://img.shields.io/discord/662017309162078267.svg?color=7289da&label=Discord&logo=discord&style=flat-square)](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

View file

@ -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}

View file

@ -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 {

View file

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

View file

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

View file

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

View file

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

View file

@ -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);
}
}

View file

@ -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> {

View file

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

View file

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

View file

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

View file

@ -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() != "",

View file

@ -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")
})
}
}

View file

@ -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));
}

View file

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

View file

@ -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);
}
}

View file

@ -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>> {

View file

@ -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

View file

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

View file

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

View file

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

View file

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

View file

@ -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>

View file

@ -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;

View file

@ -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;

View file

@ -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>

View file

@ -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>

View file

@ -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;

View file

@ -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" %}

View file

@ -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" %}

View file

@ -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
View 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" %}

View file

@ -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 %}