Added frontend code. Everything in rust. We use wasm-bindgen+web_sys
This commit is contained in:
parent
8533941047
commit
243e17610c
4
.gitignore
vendored
4
.gitignore
vendored
@ -2,4 +2,6 @@ deepseek.txt
|
|||||||
passcode.txt
|
passcode.txt
|
||||||
config.toml
|
config.toml
|
||||||
.idea
|
.idea
|
||||||
target
|
target
|
||||||
|
local_file_storage/
|
||||||
|
target_pkg/
|
||||||
109
Cargo.lock
generated
109
Cargo.lock
generated
@ -62,6 +62,7 @@ dependencies = [
|
|||||||
"matchit",
|
"matchit",
|
||||||
"memchr",
|
"memchr",
|
||||||
"mime",
|
"mime",
|
||||||
|
"multer",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
@ -173,9 +174,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.56"
|
version = "1.2.57"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex",
|
"shlex",
|
||||||
@ -366,6 +367,15 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "frontend"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@ -759,9 +769,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.11.0"
|
version = "2.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iri-string"
|
name = "iri-string"
|
||||||
@ -775,9 +785,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.17"
|
version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
@ -797,9 +807,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.181"
|
version = "0.2.183"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5"
|
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libm"
|
name = "libm"
|
||||||
@ -944,9 +954,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
@ -1182,9 +1192,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quinn-proto"
|
name = "quinn-proto"
|
||||||
version = "0.11.13"
|
version = "0.11.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
|
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
@ -1217,9 +1227,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.44"
|
version = "1.0.45"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
@ -1379,22 +1389,6 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rust_tripping"
|
|
||||||
version = "0.1.2"
|
|
||||||
dependencies = [
|
|
||||||
"axum",
|
|
||||||
"axum-extra",
|
|
||||||
"rand 0.8.5",
|
|
||||||
"reqwest",
|
|
||||||
"serde",
|
|
||||||
"tera",
|
|
||||||
"tokio",
|
|
||||||
"tokio-postgres",
|
|
||||||
"toml",
|
|
||||||
"tower-http 0.5.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
@ -1427,9 +1421,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.103.9"
|
version = "0.103.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
|
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
@ -1585,12 +1579,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.6.2"
|
version = "0.6.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
|
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1624,9 +1618,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.114"
|
version = "2.0.117"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1738,9 +1732,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.10.0"
|
version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
|
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tinyvec_macros",
|
"tinyvec_macros",
|
||||||
]
|
]
|
||||||
@ -1753,9 +1747,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.49.0"
|
version = "1.50.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
|
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
@ -1768,9 +1762,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.6.0"
|
version = "2.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1990,9 +1984,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.23"
|
version = "1.0.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e"
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-normalization"
|
name = "unicode-normalization"
|
||||||
@ -2185,6 +2179,23 @@ dependencies = [
|
|||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "website"
|
||||||
|
version = "0.1.2"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"axum-extra",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"tera",
|
||||||
|
"tokio",
|
||||||
|
"tokio-postgres",
|
||||||
|
"tokio-util",
|
||||||
|
"toml",
|
||||||
|
"tower-http 0.5.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "whoami"
|
name = "whoami"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
@ -2468,18 +2479,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.39"
|
version = "0.8.47"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a"
|
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.8.39"
|
version = "0.8.47"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517"
|
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
22
Cargo.toml
22
Cargo.toml
@ -1,16 +1,6 @@
|
|||||||
[package]
|
[workspace]
|
||||||
name = "rust_tripping"
|
name="rust_tripping"
|
||||||
version = "0.1.2"
|
members = [
|
||||||
edition = "2021"
|
"frontend",
|
||||||
|
"website",
|
||||||
[dependencies]
|
]
|
||||||
rand = "0.8"
|
|
||||||
axum = "0.7"
|
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
|
||||||
tower-http = { version = "0.5", features = ["fs"] }
|
|
||||||
tera = "1.19.1"
|
|
||||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
||||||
axum-extra = { version = "0.9", features = ["cookie"] }
|
|
||||||
tokio-postgres = "0.7"
|
|
||||||
toml = "0.8"
|
|
||||||
7
Makefile
Normal file
7
Makefile
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
find_rust = $(shell find $(1) -type f -name '*.rs' )
|
||||||
|
|
||||||
|
target/debug/frontend.wasm: $(call find_rust,frontend) frontend/Cargo.toml
|
||||||
|
cargo build -p frontend --target wasm32-unknown-unknown
|
||||||
|
|
||||||
|
target_pkg/frontend_bg.wasm: target/debug/frontend.wasm
|
||||||
|
wasm-bindgen --target web --out-dir target_pkg target/wasm32-unknown-unknown/debug/frontend.wasm
|
||||||
@ -1,3 +1,4 @@
|
|||||||
postgres_host="/run/postgresql"
|
postgres_host="/run/postgresql"
|
||||||
pg_database="DATABASE_NAME"
|
pg_database="DATABASE_NAME"
|
||||||
postgres_user="postgres"
|
postgres_user="postgres"
|
||||||
|
file_storage="file_storage"
|
||||||
|
|||||||
26
frontend/Cargo.toml
Normal file
26
frontend/Cargo.toml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[package]
|
||||||
|
name = "frontend"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
js-sys = "0.3"
|
||||||
|
|
||||||
|
[dependencies.web-sys]
|
||||||
|
version = "0.3"
|
||||||
|
features = [
|
||||||
|
"Window",
|
||||||
|
"Document",
|
||||||
|
"Event",
|
||||||
|
"EventTarget",
|
||||||
|
"KeyboardEvent",
|
||||||
|
"MouseEvent",
|
||||||
|
"WheelEvent",
|
||||||
|
"HtmlCanvasElement",
|
||||||
|
"CanvasRenderingContext2d",
|
||||||
|
"Performance",
|
||||||
|
]
|
||||||
34
frontend/pages/board.html
Normal file
34
frontend/pages/board.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>wasm_test</title>
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import init, {init_index} from "/pkg/frontend.js";
|
||||||
|
await init().catch((err) => {
|
||||||
|
console.error("Failed to initialize WASM", err);
|
||||||
|
});
|
||||||
|
init_index();
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="canvas"></canvas>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
65
frontend/pages/index.html
Normal file
65
frontend/pages/index.html
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="index-html">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Control Center</title>
|
||||||
|
<link rel="stylesheet" href="/static/css/site.css" />
|
||||||
|
</head>
|
||||||
|
<body class="index-body">
|
||||||
|
<div class="index-shell">
|
||||||
|
<header class="top-panel">
|
||||||
|
<div class="top-panel__left">
|
||||||
|
<a class="top-panel__brand" href="/">Ironlight</a>
|
||||||
|
<nav class="top-panel__nav">
|
||||||
|
<a class="top-panel__link" href="/upload">Upload</a>
|
||||||
|
<a class="top-panel__link" href="/welcome">Welcome</a>
|
||||||
|
<a class="top-panel__link" href="/login">Account</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="top-panel__right">
|
||||||
|
<span class="top-panel__user-label">Signed in</span>
|
||||||
|
<span class="top-panel__user-name">{{ logged_in_cur_user }}</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="index-main">
|
||||||
|
<section class="index-hero">
|
||||||
|
<p class="index-hero__eyebrow">Dark Material Console</p>
|
||||||
|
<h1 class="index-hero__title">Fast uploads, calm interface.</h1>
|
||||||
|
<p class="index-hero__subtitle">
|
||||||
|
Keep your files close, your workflow focused, and your storage under control.
|
||||||
|
</p>
|
||||||
|
<div class="index-hero__actions">
|
||||||
|
<a class="index-hero__button" href="/upload">Upload a file</a>
|
||||||
|
<a class="index-hero__ghost" href="/welcome">Open welcome</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="index-grid">
|
||||||
|
<article class="index-card">
|
||||||
|
<h2 class="index-card__title">Recent activity</h2>
|
||||||
|
<p class="index-card__text">
|
||||||
|
Track new files, size totals, and secure download links in one place.
|
||||||
|
</p>
|
||||||
|
<a class="index-card__link" href="/upload">Review uploads</a>
|
||||||
|
</article>
|
||||||
|
<article class="index-card">
|
||||||
|
<h2 class="index-card__title">Storage health</h2>
|
||||||
|
<p class="index-card__text">
|
||||||
|
Monitor usage and keep the 10 GiB limit comfortably in sight.
|
||||||
|
</p>
|
||||||
|
<a class="index-card__link" href="/upload">View usage</a>
|
||||||
|
</article>
|
||||||
|
<article class="index-card">
|
||||||
|
<h2 class="index-card__title">Quick actions</h2>
|
||||||
|
<p class="index-card__text">
|
||||||
|
Jump straight into file uploads or check your latest shared links.
|
||||||
|
</p>
|
||||||
|
<a class="index-card__link" href="/upload">Start upload</a>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
29
frontend/pages/upload.html
Normal file
29
frontend/pages/upload.html
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Upload file</title>
|
||||||
|
<link rel="stylesheet" href="/static/css/site.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Upload file</h1>
|
||||||
|
<p>Maximum total storage: 10 GiB</p>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<p class="error">{{ error }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if success_id %}
|
||||||
|
<p>
|
||||||
|
Uploaded {{ success_filename }} ({{ success_size }} bytes).
|
||||||
|
<a href="/get/{{ success_id }}">Download</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form action="/upload" method="post" enctype="multipart/form-data">
|
||||||
|
<input type="file" name="file" required />
|
||||||
|
<button type="submit">Upload</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
271
frontend/src/lib.rs
Normal file
271
frontend/src/lib.rs
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use wasm_bindgen::closure::Closure;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
use web_sys::{
|
||||||
|
window, CanvasRenderingContext2d, Document, HtmlCanvasElement, KeyboardEvent, WheelEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Copy)]
|
||||||
|
struct InputState {
|
||||||
|
w: bool,
|
||||||
|
a: bool,
|
||||||
|
s: bool,
|
||||||
|
d: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Camera {
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
ppm: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SceneState {
|
||||||
|
camera: Camera,
|
||||||
|
speed_pps: f64,
|
||||||
|
triangles: Vec<(&'static str, [(f64, f64); 3])>,
|
||||||
|
need_camera_setup: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SceneState {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
camera: Camera {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
ppm: 10.0,
|
||||||
|
},
|
||||||
|
speed_pps: 900.0,
|
||||||
|
triangles: vec![
|
||||||
|
("#ff6b6b", [(0.0, 0.0), (3.0, 0.0), (1.5, 2.5)]),
|
||||||
|
("#4dabf7", [(-4.0, -2.0), (-1.0, -3.5), (-2.5, 0.5)]),
|
||||||
|
("#ffd43b", [(2.0, -4.0), (5.0, -4.0), (4.0, -1.0)]),
|
||||||
|
],
|
||||||
|
need_camera_setup: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_camera_if_needed(&mut self, width: f64, height: f64) {
|
||||||
|
if !self.need_camera_setup {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if width <= 0.0 || height <= 0.0 || self.camera.ppm <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.camera.x = -width / (2.0 * self.camera.ppm);
|
||||||
|
self.camera.y = -height / (2.0 * self.camera.ppm);
|
||||||
|
self.need_camera_setup = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_key(state: &mut InputState, code: &str, is_down: bool) -> bool {
|
||||||
|
match code {
|
||||||
|
"KeyW" => {
|
||||||
|
state.w = is_down;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
"KeyA" => {
|
||||||
|
state.a = is_down;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
"KeyS" => {
|
||||||
|
state.s = is_down;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
"KeyD" => {
|
||||||
|
state.d = is_down;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn init_index() -> Result<(), JsValue> {
|
||||||
|
let document: Document = window().unwrap().document().unwrap();
|
||||||
|
let canvas: HtmlCanvasElement = document
|
||||||
|
.get_element_by_id("canvas")
|
||||||
|
.ok_or_else(|| JsValue::from_str("Missing #canvas element"))?
|
||||||
|
.dyn_into()?;
|
||||||
|
|
||||||
|
let context: CanvasRenderingContext2d = canvas
|
||||||
|
.get_context("2d")?
|
||||||
|
.ok_or_else(|| JsValue::from_str("Missing 2d context"))?
|
||||||
|
.dyn_into()?;
|
||||||
|
|
||||||
|
let window = window().unwrap();
|
||||||
|
let canvas = canvas.clone();
|
||||||
|
let context = context.clone();
|
||||||
|
|
||||||
|
let input = Rc::new(RefCell::new(InputState::default()));
|
||||||
|
let scene = Rc::new(RefCell::new(SceneState::new()));
|
||||||
|
let last_time = Rc::new(RefCell::new(
|
||||||
|
window
|
||||||
|
.performance()
|
||||||
|
.ok_or_else(|| JsValue::from_str("Missing performance"))?
|
||||||
|
.now(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let input_down = input.clone();
|
||||||
|
let keydown = Closure::<dyn FnMut(KeyboardEvent)>::wrap(Box::new(move |event: KeyboardEvent| {
|
||||||
|
let code = event.code();
|
||||||
|
let mut state = input_down.borrow_mut();
|
||||||
|
if update_key(&mut state, &code, true) {
|
||||||
|
event.prevent_default();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
window.add_event_listener_with_callback("keydown", keydown.as_ref().unchecked_ref())?;
|
||||||
|
keydown.forget();
|
||||||
|
|
||||||
|
let input_up = input.clone();
|
||||||
|
let keyup = Closure::<dyn FnMut(KeyboardEvent)>::wrap(Box::new(move |event: KeyboardEvent| {
|
||||||
|
let code = event.code();
|
||||||
|
let mut state = input_up.borrow_mut();
|
||||||
|
if update_key(&mut state, &code, false) {
|
||||||
|
event.prevent_default();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
window.add_event_listener_with_callback("keyup", keyup.as_ref().unchecked_ref())?;
|
||||||
|
keyup.forget();
|
||||||
|
|
||||||
|
let scene_zoom = scene.clone();
|
||||||
|
let canvas_zoom = canvas.clone();
|
||||||
|
let wheel = Closure::<dyn FnMut(WheelEvent)>::wrap(Box::new(move |event: WheelEvent| {
|
||||||
|
event.prevent_default();
|
||||||
|
|
||||||
|
let pointer_x = event.offset_x() as f64;
|
||||||
|
let pointer_y = event.offset_y() as f64;
|
||||||
|
|
||||||
|
let mut scene = scene_zoom.borrow_mut();
|
||||||
|
let old_ppm = scene.camera.ppm;
|
||||||
|
let zoom_factor = (-event.delta_y() * 0.001).exp();
|
||||||
|
let mut new_ppm = old_ppm * zoom_factor;
|
||||||
|
let min_ppm = 2.0;
|
||||||
|
let max_ppm = 200.0;
|
||||||
|
|
||||||
|
if new_ppm < min_ppm {
|
||||||
|
new_ppm = min_ppm;
|
||||||
|
}
|
||||||
|
if new_ppm > max_ppm {
|
||||||
|
new_ppm = max_ppm;
|
||||||
|
}
|
||||||
|
|
||||||
|
let world_x = scene.camera.x + pointer_x / old_ppm;
|
||||||
|
let world_y = scene.camera.y + pointer_y / old_ppm;
|
||||||
|
|
||||||
|
scene.camera.ppm = new_ppm;
|
||||||
|
scene.camera.x = world_x - pointer_x / new_ppm;
|
||||||
|
scene.camera.y = world_y - pointer_y / new_ppm;
|
||||||
|
}));
|
||||||
|
canvas_zoom.add_event_listener_with_callback("wheel", wheel.as_ref().unchecked_ref())?;
|
||||||
|
wheel.forget();
|
||||||
|
|
||||||
|
let draw = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
|
||||||
|
let draw_handle = draw.clone();
|
||||||
|
let window_handle = window.clone();
|
||||||
|
let input_handle = input.clone();
|
||||||
|
let scene_handle = scene.clone();
|
||||||
|
let last_time_handle = last_time.clone();
|
||||||
|
|
||||||
|
*draw_handle.borrow_mut() = Some(Closure::wrap(Box::new(move || {
|
||||||
|
let now = window_handle
|
||||||
|
.performance()
|
||||||
|
.map(|perf| perf.now())
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
let mut last = last_time_handle.borrow_mut();
|
||||||
|
let mut dt = (now - *last) / 1000.0;
|
||||||
|
*last = now;
|
||||||
|
|
||||||
|
if dt.is_nan() || dt.is_infinite() {
|
||||||
|
dt = 0.0;
|
||||||
|
}
|
||||||
|
if dt > 0.05 {
|
||||||
|
dt = 0.05;
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = window_handle
|
||||||
|
.inner_width()
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.as_f64())
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
let height = window_handle
|
||||||
|
.inner_height()
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.as_f64())
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
|
canvas.set_width(width as u32);
|
||||||
|
canvas.set_height(height as u32);
|
||||||
|
|
||||||
|
let input = input_handle.borrow();
|
||||||
|
let mut scene = scene_handle.borrow_mut();
|
||||||
|
scene.setup_camera_if_needed(width, height);
|
||||||
|
let step = (scene.speed_pps / scene.camera.ppm) * dt;
|
||||||
|
|
||||||
|
if input.w {
|
||||||
|
scene.camera.y -= step;
|
||||||
|
}
|
||||||
|
if input.s {
|
||||||
|
scene.camera.y += step;
|
||||||
|
}
|
||||||
|
if input.a {
|
||||||
|
scene.camera.x -= step;
|
||||||
|
}
|
||||||
|
if input.d {
|
||||||
|
scene.camera.x += step;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.set_fill_style(&JsValue::from_str("#0b0f17"));
|
||||||
|
context.fill_rect(0.0, 0.0, width, height);
|
||||||
|
context.set_line_width(1.0);
|
||||||
|
context.set_stroke_style(&JsValue::from_str("#111827"));
|
||||||
|
|
||||||
|
for (color, points) in scene.triangles.iter() {
|
||||||
|
let to_screen = |(x, y): (f64, f64)| -> (f64, f64) {
|
||||||
|
let sx =
|
||||||
|
(x - scene.camera.x) * scene.camera.ppm;
|
||||||
|
let sy =
|
||||||
|
(y - scene.camera.y) * scene.camera.ppm;
|
||||||
|
(sx, sy)
|
||||||
|
};
|
||||||
|
|
||||||
|
let (x0, y0) = to_screen(points[0]);
|
||||||
|
let (x1, y1) = to_screen(points[1]);
|
||||||
|
let (x2, y2) = to_screen(points[2]);
|
||||||
|
|
||||||
|
context.begin_path();
|
||||||
|
context.move_to(x0, y0);
|
||||||
|
context.line_to(x1, y1);
|
||||||
|
context.line_to(x2, y2);
|
||||||
|
context.close_path();
|
||||||
|
|
||||||
|
context.set_fill_style(&JsValue::from_str(color));
|
||||||
|
context.fill();
|
||||||
|
context.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = window_handle.request_animation_frame(
|
||||||
|
draw.borrow().as_ref().unwrap().as_ref().unchecked_ref(),
|
||||||
|
);
|
||||||
|
}) as Box<dyn FnMut()>));
|
||||||
|
|
||||||
|
window.request_animation_frame(
|
||||||
|
draw_handle
|
||||||
|
.borrow()
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.as_ref()
|
||||||
|
.unchecked_ref(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn init_tables() -> Result<(), JsValue> {
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
377
frontend/static/css/site.css
Normal file
377
frontend/static/css/site.css
Normal file
@ -0,0 +1,377 @@
|
|||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: "Georgia", "Times New Roman", serif;
|
||||||
|
color: #f7f4ef;
|
||||||
|
background: url("/static/images/skeleton.webp") top center / cover no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card,
|
||||||
|
.welcome-card {
|
||||||
|
background: rgba(10, 10, 10, 0.65);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 28px 32px;
|
||||||
|
width: min(360px, 90vw);
|
||||||
|
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.35);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card h1,
|
||||||
|
.welcome-card h1 {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 28px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: #e6e0d7;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="password"] {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
background: #f0c986;
|
||||||
|
color: #1c1200;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #f3d6a4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
color: #ff6b6b;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mild-warning {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
color: white;
|
||||||
|
font-weight: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-html,
|
||||||
|
.index-body {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-body {
|
||||||
|
margin: 0;
|
||||||
|
display: block;
|
||||||
|
min-height: 100%;
|
||||||
|
color: #f5f5f5;
|
||||||
|
font-family: "Fira Sans", "Space Grotesk", "Montserrat", sans-serif;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 20% 20%, rgba(66, 90, 120, 0.35), transparent 55%),
|
||||||
|
radial-gradient(circle at 80% 0%, rgba(120, 70, 130, 0.25), transparent 50%),
|
||||||
|
linear-gradient(145deg, #0b0f15 0%, #0f141c 45%, #151a22 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-shell {
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 28px 32px 56px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-panel {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 18px 28px;
|
||||||
|
background: rgba(17, 21, 29, 0.92);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 18px;
|
||||||
|
box-shadow:
|
||||||
|
0 16px 40px rgba(0, 0, 0, 0.45),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
animation: index-rise 0.55s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-panel__left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 28px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-panel__brand {
|
||||||
|
font-family: "Space Grotesk", "Fira Sans", sans-serif;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-panel__nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-panel__link {
|
||||||
|
color: #f5f5f5;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: border-color 0.2s ease, background 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-panel__link:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border-color: rgba(255, 255, 255, 0.18);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-panel__right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-panel__user-label {
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-panel__user-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(77, 118, 255, 0.28);
|
||||||
|
border: 1px solid rgba(110, 146, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 40px;
|
||||||
|
max-width: 1100px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
animation: index-fade 0.6s ease-out both;
|
||||||
|
animation-delay: 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-hero {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 24px 28px;
|
||||||
|
background: rgba(20, 24, 32, 0.75);
|
||||||
|
border-radius: 22px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow: 0 18px 36px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-hero__eyebrow {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
letter-spacing: 1.6px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-hero__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(32px, 4vw, 44px);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-hero__subtitle {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
max-width: 680px;
|
||||||
|
color: rgba(255, 255, 255, 0.78);
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-hero__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-hero__button {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #0b0f15;
|
||||||
|
background: linear-gradient(135deg, #f0d27a 0%, #f8b46f 100%);
|
||||||
|
padding: 12px 22px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-hero__button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 14px 24px rgba(0, 0, 0, 0.38);
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-hero__ghost {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #f5f5f5;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
transition: border-color 0.2s ease, background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-hero__ghost:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-card {
|
||||||
|
padding: 20px 22px;
|
||||||
|
background: rgba(18, 22, 30, 0.78);
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.3);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
animation: index-fade 0.6s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-card__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-card__text {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-card__link {
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
width: fit-content;
|
||||||
|
transition: background 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-card__link:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.14);
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes index-rise {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-16px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes index-fade {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(12px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.index-shell {
|
||||||
|
padding: 20px 18px 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-panel {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-panel__right {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-hero {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
@ -1,12 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<title>Main page</title>
|
|
||||||
<link rel="stylesheet" href="/static/css/site.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Main page</h1>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
html,
|
|
||||||
body {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-family: "Georgia", "Times New Roman", serif;
|
|
||||||
color: #f7f4ef;
|
|
||||||
background: url("/static/images/wool.jpg") top center / cover no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card,
|
|
||||||
.welcome-card {
|
|
||||||
background: rgba(10, 10, 10, 0.65);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 28px 32px;
|
|
||||||
width: min(360px, 90vw);
|
|
||||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.35);
|
|
||||||
backdrop-filter: blur(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card h1,
|
|
||||||
.welcome-card h1 {
|
|
||||||
margin: 0 0 16px;
|
|
||||||
font-size: 28px;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
color: #e6e0d7;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="password"] {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
||||||
padding: 10px 12px;
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 16px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 12px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
background: #f0c986;
|
|
||||||
color: #1c1200;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background: #f3d6a4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
margin: 0 0 12px;
|
|
||||||
color: #ff6b6b;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mild-warning {
|
|
||||||
margin: 0 0 12px;
|
|
||||||
color: white;
|
|
||||||
font-weight: 200;
|
|
||||||
}
|
|
||||||
2555
website/Cargo.lock
generated
Normal file
2555
website/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
website/Cargo.toml
Normal file
17
website/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "website"
|
||||||
|
version = "0.1.2"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rand = "0.8"
|
||||||
|
axum = { version = "0.7", features = ["multipart"] }
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "io-util"] }
|
||||||
|
tower-http = { version = "0.5", features = ["fs"] }
|
||||||
|
tera = "1.19.1"
|
||||||
|
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
axum-extra = { version = "0.9", features = ["cookie"] }
|
||||||
|
tokio-postgres = "0.7"
|
||||||
|
toml = "0.8"
|
||||||
|
tokio-util = { version = "0.7", features = ["io"] }
|
||||||
@ -1,4 +1,4 @@
|
|||||||
use rust_tripping::init_db;
|
use website::init_db;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
use rust_tripping::run_server;
|
use website::run_server;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
@ -8,6 +8,7 @@ pub struct GeneralServiceConfig {
|
|||||||
pub postgres_host: String,
|
pub postgres_host: String,
|
||||||
pub pg_database: String,
|
pub pg_database: String,
|
||||||
pub postgres_user: String,
|
pub postgres_user: String,
|
||||||
|
pub file_storage: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ConfigResult<T> = Result<T, Box<dyn std::error::Error>>;
|
pub type ConfigResult<T> = Result<T, Box<dyn std::error::Error>>;
|
||||||
@ -32,6 +33,14 @@ pub fn load_config(path: impl AsRef<Path>) -> ConfigResult<GeneralServiceConfig>
|
|||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.file_storage.trim().is_empty() {
|
||||||
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
"config file_storage is empty",
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,8 +1,7 @@
|
|||||||
use tokio_postgres::{Client, NoTls};
|
use tokio_postgres::{Client, NoTls};
|
||||||
|
|
||||||
const PERSON_SCHEMA_NAME: &str = "public";
|
|
||||||
|
|
||||||
use crate::config::GeneralServiceConfig;
|
use crate::config::GeneralServiceConfig;
|
||||||
|
use crate::web_file_uploads::render_upload_page;
|
||||||
|
|
||||||
pub type DbResult<T> = Result<T, Box<dyn std::error::Error>>;
|
pub type DbResult<T> = Result<T, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
@ -25,14 +24,23 @@ pub async fn connect_db(config: &GeneralServiceConfig) -> DbResult<Client> {
|
|||||||
pub async fn init_database(config: &GeneralServiceConfig) -> DbResult<()> {
|
pub async fn init_database(config: &GeneralServiceConfig) -> DbResult<()> {
|
||||||
let client = connect_db(config).await?;
|
let client = connect_db(config).await?;
|
||||||
|
|
||||||
let create_sql = format!(
|
let create_sql =
|
||||||
"CREATE TABLE IF NOT EXISTS \"{}\".person (\
|
"CREATE TABLE IF NOT EXISTS person (\
|
||||||
id SERIAL PRIMARY KEY,\
|
id SERIAL PRIMARY KEY,\
|
||||||
name TEXT NOT NULL,\
|
name TEXT NOT NULL,\
|
||||||
passcode TEXT NOT NULL\
|
passcode TEXT NOT NULL\
|
||||||
)",
|
)";
|
||||||
PERSON_SCHEMA_NAME
|
client.execute(create_sql, &[]).await?;
|
||||||
);
|
|
||||||
client.execute(create_sql.as_str(), &[]).await?;
|
let create_files_sql =
|
||||||
|
"CREATE TABLE IF NOT EXISTS stored_file (\
|
||||||
|
id SERIAL PRIMARY KEY,\
|
||||||
|
filename TEXT NOT NULL,\
|
||||||
|
size BIGINT NOT NULL\
|
||||||
|
)";
|
||||||
|
client.execute(create_files_sql, &[]).await?;
|
||||||
|
|
||||||
|
tokio::fs::create_dir_all(&config.file_storage).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -2,9 +2,11 @@ mod samplers;
|
|||||||
mod invoking_llms;
|
mod invoking_llms;
|
||||||
mod db;
|
mod db;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod web_file_uploads;
|
||||||
|
mod web_app_state;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Form, Query, State},
|
extract::{DefaultBodyLimit, Form, Query, State},
|
||||||
response::{Html, IntoResponse, Redirect, Response},
|
response::{Html, IntoResponse, Redirect, Response},
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Router,
|
Router,
|
||||||
@ -16,34 +18,20 @@ use std::net::SocketAddr;
|
|||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use tera::{Tera};
|
use tera::{Tera};
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::{ServeDir, ServeFile};
|
||||||
|
|
||||||
use samplers::random_junk::random_string;
|
use samplers::random_junk::random_string;
|
||||||
pub use config::{ConfigResult, GeneralServiceConfig, load_config, load_config_default};
|
use config::{ConfigResult, GeneralServiceConfig, load_config, load_config_default};
|
||||||
pub use db::{DbResult, connect_db, init_database};
|
use db::{DbResult, connect_db, init_database};
|
||||||
|
use web_file_uploads::{get_file, upload_get, upload_post};
|
||||||
|
use web_app_state::{AppState, AppStateInner, AuthenticatedUserId};
|
||||||
|
|
||||||
pub struct AppStateInner {
|
|
||||||
pub config: GeneralServiceConfig,
|
|
||||||
pub db: tokio_postgres::Client,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type AppState = std::sync::Arc<AppStateInner>;
|
pub async fn init_app_state() -> Result<AppState, Box<dyn std::error::Error>> {
|
||||||
|
|
||||||
pub async fn init_app_state() -> DbResult<AppState> {
|
|
||||||
let config = load_config_default()?;
|
let config = load_config_default()?;
|
||||||
let db = connect_db(&config).await?;
|
let db = connect_db(&config).await?;
|
||||||
Ok(std::sync::Arc::new(AppStateInner { config, db }))
|
let tera = Tera::new("frontend/pages/**/*.html")?;
|
||||||
}
|
Ok(std::sync::Arc::new(AppStateInner { config, db, tera }))
|
||||||
|
|
||||||
static TEMPLATES: OnceLock<Tera> = OnceLock::new();
|
|
||||||
|
|
||||||
fn templates() -> &'static Tera {
|
|
||||||
TEMPLATES.get_or_init(|| Tera::new("src/pages/**/*.html").expect("load templates"))
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AuthenticatedUserId {
|
|
||||||
id: i32,
|
|
||||||
name: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PasscodeAuthenticationResult{
|
enum PasscodeAuthenticationResult{
|
||||||
@ -59,14 +47,14 @@ enum WebsiteAuthenticationResult {
|
|||||||
async fn website_authentication_with_passcode(
|
async fn website_authentication_with_passcode(
|
||||||
state: &AppStateInner, passcode: &str) -> PasscodeAuthenticationResult {
|
state: &AppStateInner, passcode: &str) -> PasscodeAuthenticationResult {
|
||||||
let row = state.db.query_opt(
|
let row = state.db.query_opt(
|
||||||
"SELECT id, name FROM public.person WHERE passcode = $1 LIMIT 1",
|
"SELECT id, name FROM person WHERE passcode = $1 LIMIT 1",
|
||||||
&[&passcode],
|
&[&passcode],
|
||||||
).await;
|
).await;
|
||||||
|
|
||||||
match row {
|
match row {
|
||||||
Ok(Some(row)) => PasscodeAuthenticationResult::Ok(AuthenticatedUserId {
|
Ok(Some(row)) => PasscodeAuthenticationResult::Ok(AuthenticatedUserId {
|
||||||
id: row.get(0),
|
id: row.get::<_, i32>(0),
|
||||||
name: row.get(1),
|
name: row.get::<_, String>(1),
|
||||||
}),
|
}),
|
||||||
Ok(None) => PasscodeAuthenticationResult::WrongPassword,
|
Ok(None) => PasscodeAuthenticationResult::WrongPassword,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@ -91,15 +79,13 @@ async fn website_authentication_with_cookie(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn axum_handler_with_auth<T, H, Res>(
|
fn axum_handler_with_auth<H, Fut, Res, T>(
|
||||||
handler: H,
|
handler: H,
|
||||||
state: AppState,
|
state: AppState,
|
||||||
) -> impl Fn(CookieJar, T) -> std::pin::Pin<Box<dyn Future<Output = Response> + Send>> + Clone + Send + 'static
|
) -> impl Fn(CookieJar, T) -> std::pin::Pin<Box<dyn Future<Output = Response> + Send>> + Clone + Send + 'static
|
||||||
where
|
where
|
||||||
H: for<'a> Fn(T, &'a AppStateInner, AuthenticatedUserId) -> std::pin::Pin<Box<dyn Future<Output = Res> + Send + 'a>>
|
H: (Fn(T, AppState, AuthenticatedUserId) -> Fut) + Send + Clone + 'static,
|
||||||
+ Clone
|
Fut: Future<Output = Res> + Send + 'static,
|
||||||
+ Send
|
|
||||||
+ 'static,
|
|
||||||
Res: IntoResponse + 'static,
|
Res: IntoResponse + 'static,
|
||||||
T: Send + 'static,
|
T: Send + 'static,
|
||||||
{
|
{
|
||||||
@ -110,7 +96,7 @@ where
|
|||||||
let res = website_authentication_with_cookie(state.as_ref(), &jar).await;
|
let res = website_authentication_with_cookie(state.as_ref(), &jar).await;
|
||||||
match res {
|
match res {
|
||||||
WebsiteAuthenticationResult::SomeCookie(PasscodeAuthenticationResult::Ok(user)) => {
|
WebsiteAuthenticationResult::SomeCookie(PasscodeAuthenticationResult::Ok(user)) => {
|
||||||
handler(args, state.as_ref(), user).await.into_response()
|
handler(args, state, user).await.into_response()
|
||||||
}
|
}
|
||||||
WebsiteAuthenticationResult::NoCookie => {
|
WebsiteAuthenticationResult::NoCookie => {
|
||||||
Redirect::to("/login").into_response()
|
Redirect::to("/login").into_response()
|
||||||
@ -167,9 +153,7 @@ async fn login_get(
|
|||||||
ctx.insert("logged_in_cur_user", &cur_user.name);
|
ctx.insert("logged_in_cur_user", &cur_user.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = templates()
|
let body = state.tera.render("login.html", &ctx).expect("render index");
|
||||||
.render("login.html", &ctx)
|
|
||||||
.expect("render index");
|
|
||||||
(jar, Html(body))
|
(jar, Html(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,41 +189,59 @@ async fn login_post(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn index(
|
|
||||||
|
async fn index(
|
||||||
_: (),
|
_: (),
|
||||||
_state: &AppStateInner,
|
state: AppState,
|
||||||
_user: AuthenticatedUserId,
|
user: AuthenticatedUserId,
|
||||||
) -> std::pin::Pin<Box<dyn Future<Output = Html<String>> + Send + '_>> {
|
) -> impl IntoResponse {
|
||||||
Box::pin(async move {
|
let mut ctx = tera::Context::new();
|
||||||
let body = templates()
|
ctx.insert("logged_in_cur_user", &user.name);
|
||||||
.render("index.html", &tera::Context::new())
|
let body = state.tera
|
||||||
.expect("render index");
|
.render("index.html", &ctx)
|
||||||
Html(body)
|
.expect("render index");
|
||||||
})
|
Html(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn welcome(
|
async fn welcome(
|
||||||
_: (),
|
_: (),
|
||||||
_state: &AppStateInner,
|
_state: AppState,
|
||||||
_user: AuthenticatedUserId,
|
_user: AuthenticatedUserId,
|
||||||
) -> std::pin::Pin<Box<dyn Future<Output = Html<String>> + Send + '_>> {
|
) -> impl IntoResponse {
|
||||||
Box::pin(async move {
|
let body = _state.tera
|
||||||
let body = templates()
|
.render("welcome.html", &tera::Context::new())
|
||||||
.render("welcome.html", &tera::Context::new())
|
.expect("render welcome");
|
||||||
.expect("render welcome");
|
Html(body)
|
||||||
Html(body)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn board(
|
||||||
|
_: (),
|
||||||
|
_state: AppState,
|
||||||
|
_user: AuthenticatedUserId,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let body = _state.tera
|
||||||
|
.render("board.html", &tera::Context::new())
|
||||||
|
.expect("render welcome");
|
||||||
|
Html(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
pub async fn run_server() -> Result<(), Box<dyn std::error::Error>> {
|
pub async fn run_server() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let state = init_app_state().await.expect("lol");
|
let state = init_app_state().await.expect("lol");
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/login", get(login_get))
|
.route("/login", get(login_get))
|
||||||
.route("/login", post(login_post))
|
.route("/login", post(login_post))
|
||||||
.route("/welcome", get(axum_handler_with_auth(welcome, state.clone())))
|
.route("/upload", get(axum_handler_with_auth(upload_get, state.clone())))
|
||||||
.route("/", get(axum_handler_with_auth(index, state.clone())))
|
.route("/upload", post(axum_handler_with_auth(upload_post, state.clone())))
|
||||||
.nest_service("/static", ServeDir::new("src/static"))
|
.route("/get/:id", get(axum_handler_with_auth(get_file, state.clone())))
|
||||||
|
.route("/welcome", get(axum_handler_with_auth(welcome, state.clone())), )
|
||||||
|
.route("/board", get(axum_handler_with_auth(board, state.clone())), )
|
||||||
|
.route("/", get(axum_handler_with_auth(index, state.clone())), )
|
||||||
|
.nest_service("/static", ServeDir::new("frontend/static"))
|
||||||
|
.nest_service("/pkg/frontend.js", ServeFile::new("target_pkg/frontend.js"))
|
||||||
|
.nest_service("/pkg/frontend_bg.wasm", ServeFile::new("target_pkg/frontend_bg.wasm"))
|
||||||
|
.layer(DefaultBodyLimit::disable())
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
let addr: SocketAddr = "127.0.0.1:3000".parse().expect("valid socket addr");
|
let addr: SocketAddr = "127.0.0.1:3000".parse().expect("valid socket addr");
|
||||||
15
website/src/web_app_state.rs
Normal file
15
website/src/web_app_state.rs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
use tera::Tera;
|
||||||
|
use crate::GeneralServiceConfig;
|
||||||
|
|
||||||
|
pub struct AppStateInner {
|
||||||
|
pub config: GeneralServiceConfig,
|
||||||
|
pub db: tokio_postgres::Client,
|
||||||
|
pub tera: Tera,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type AppState = std::sync::Arc<AppStateInner>;
|
||||||
|
|
||||||
|
pub struct AuthenticatedUserId {
|
||||||
|
pub id: i32,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
260
website/src/web_file_uploads.rs
Normal file
260
website/src/web_file_uploads.rs
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
extract::{Multipart, Path, State},
|
||||||
|
http::{header, HeaderValue, StatusCode},
|
||||||
|
response::{Html, IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use tokio_util::io::ReaderStream;
|
||||||
|
|
||||||
|
pub const MAX_TOTAL_STORAGE_BYTES: i64 = 10 * 1024 * 1024 * 1024;
|
||||||
|
pub const MAX_FILE_SIZE_BYTES: i64 = 10 * 1024 * 1024 * 1024;
|
||||||
|
|
||||||
|
use crate::web_app_state::{AppState, AppStateInner, AuthenticatedUserId};
|
||||||
|
|
||||||
|
pub struct UploadSuccess {
|
||||||
|
pub(crate) id: i32,
|
||||||
|
pub(crate) filename: String,
|
||||||
|
pub(crate) size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_upload_page(state: &AppStateInner, error: Option<String>, success: Option<UploadSuccess>) -> Html<String> {
|
||||||
|
let mut ctx = tera::Context::new();
|
||||||
|
if let Some(error) = error {
|
||||||
|
ctx.insert("error", &error);
|
||||||
|
}
|
||||||
|
if let Some(success) = success {
|
||||||
|
ctx.insert("success_id", &success.id);
|
||||||
|
ctx.insert("success_filename", &success.filename);
|
||||||
|
ctx.insert("success_size", &success.size);
|
||||||
|
}
|
||||||
|
let body = state.tera
|
||||||
|
.render("upload.html", &ctx)
|
||||||
|
.expect("render upload");
|
||||||
|
Html(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn upload_get(
|
||||||
|
_: (),
|
||||||
|
_state: AppState,
|
||||||
|
_user: AuthenticatedUserId,) -> Html<String> {
|
||||||
|
render_upload_page(&_state, None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn upload_post(
|
||||||
|
(headers, mut multipart): (axum::http::HeaderMap, Multipart),
|
||||||
|
_state: AppState,
|
||||||
|
_user: AuthenticatedUserId,
|
||||||
|
) -> Html<String> {
|
||||||
|
let content_length: u64 = match headers
|
||||||
|
.get(header::CONTENT_LENGTH)
|
||||||
|
.and_then(|value| value.to_str().ok())
|
||||||
|
.and_then(|value| value.parse::<u64>().ok())
|
||||||
|
{
|
||||||
|
Some(value) => value,
|
||||||
|
_ => return render_upload_page(&_state,
|
||||||
|
Some("Missing or invalid Content-Length".to_string()),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (content_length > MAX_FILE_SIZE_BYTES as u64){
|
||||||
|
return render_upload_page(&_state, Some("File size is too big".to_string()), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let old_total_size: i64 = match _state .db.query_one(
|
||||||
|
"SELECT COALESCE(SUM(size), 0)::BIGINT FROM public.stored_file",
|
||||||
|
&[],
|
||||||
|
).await {
|
||||||
|
Ok(row) => row.get::<_, i64>(0),
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Database error: {err}");
|
||||||
|
return render_upload_page(&_state, Some("Server error".to_string()), None, );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(0 <= old_total_size);
|
||||||
|
assert!(old_total_size <= MAX_TOTAL_STORAGE_BYTES);
|
||||||
|
|
||||||
|
let total_with_new: u64 = old_total_size as u64 + content_length;
|
||||||
|
|
||||||
|
if total_with_new > MAX_TOTAL_STORAGE_BYTES as u64 {
|
||||||
|
return render_upload_page(&_state,
|
||||||
|
Some("Storage limit exceeded (10 GiB)".to_string()), None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut field = loop {
|
||||||
|
match multipart.next_field().await {
|
||||||
|
Ok(Some(field)) => {
|
||||||
|
if field.name() == Some("file") {
|
||||||
|
break field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(err) =>
|
||||||
|
return render_upload_page(&_state,
|
||||||
|
Some(format!("Upload error: {err}")), None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let filename = field
|
||||||
|
.file_name()
|
||||||
|
.map(|name| name.to_string())
|
||||||
|
.unwrap_or_else(|| "upload".to_string());
|
||||||
|
|
||||||
|
let row = match _state.db.query_one(
|
||||||
|
"INSERT INTO public.stored_file (filename, size) VALUES ($1, 0) RETURNING id",
|
||||||
|
&[&filename],
|
||||||
|
).await
|
||||||
|
{
|
||||||
|
Ok(row) => row,
|
||||||
|
Err(err) => {
|
||||||
|
return render_upload_page(&_state,
|
||||||
|
Some(format!("Database error: {err}")),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let id: i32 = row.get::<_, i32>(0);
|
||||||
|
|
||||||
|
let path = PathBuf::from(&_state.config.file_storage).join(id.to_string());
|
||||||
|
let mut file = match tokio::fs::File::create(&path).await {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(err) => {
|
||||||
|
let _ = _state.db.execute(
|
||||||
|
"DELETE FROM public.stored_file WHERE id = $1", &[&id]
|
||||||
|
).await;
|
||||||
|
eprintln!("Cannot create file: {err}");
|
||||||
|
return render_upload_page(&_state,
|
||||||
|
Some("Filesystem error".to_string()), None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = file.set_len(content_length).await {
|
||||||
|
let _ = tokio::fs::remove_file(&path).await;
|
||||||
|
let _ = _state.db.execute(
|
||||||
|
"DELETE FROM public.stored_file WHERE id = $1", &[&id]
|
||||||
|
).await;
|
||||||
|
eprintln!("Cannot preallocate file: {err}");
|
||||||
|
return render_upload_page(&_state,
|
||||||
|
Some("Filesystem error".to_string()), None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut written: u64 = 0;
|
||||||
|
loop {
|
||||||
|
match field.chunk().await {
|
||||||
|
Ok(Some(chunk)) => {
|
||||||
|
let chunk_len = chunk.len() as u64;
|
||||||
|
if written + chunk_len > content_length {
|
||||||
|
let _ = tokio::fs::remove_file(&path).await;
|
||||||
|
let _ = _state.db.execute(
|
||||||
|
"DELETE FROM public.stored_file WHERE id = $1", &[&id]
|
||||||
|
).await;
|
||||||
|
return render_upload_page(&_state,
|
||||||
|
Some("Your browser is bad".to_string()), None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
written += chunk_len;
|
||||||
|
{
|
||||||
|
let percent = 100. * (written as f32) / (content_length as f32);
|
||||||
|
println!("DEBUG: uploaded {percent}, cur chunk is {chunk_len}")
|
||||||
|
}
|
||||||
|
if let Err(err) = file.write_all(&chunk).await {
|
||||||
|
let _ = tokio::fs::remove_file(&path).await;
|
||||||
|
let _ = _state.db.execute(
|
||||||
|
"DELETE FROM public.stored_file WHERE id = $1", &[&id]
|
||||||
|
).await;
|
||||||
|
eprintln!("Can't write chunk to file: {err}");
|
||||||
|
return render_upload_page(&_state,
|
||||||
|
Some("Filesystem error".to_string()), None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => break,
|
||||||
|
Err(err) => {
|
||||||
|
let _ = tokio::fs::remove_file(&path).await;
|
||||||
|
let _ = _state.db.execute(
|
||||||
|
"DELETE FROM public.stored_file WHERE id = $1", &[&id]
|
||||||
|
).await;
|
||||||
|
return render_upload_page(&_state,
|
||||||
|
Some(format!("Upload error: {err}")), None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(err) = _state.db.execute(
|
||||||
|
"UPDATE public.stored_file SET size = $1 WHERE id = $2",
|
||||||
|
&[&(written as i64), &id],
|
||||||
|
).await {
|
||||||
|
let _ = tokio::fs::remove_file(&path).await;
|
||||||
|
let _ = _state.db.execute(
|
||||||
|
"DELETE FROM public.stored_file WHERE id = $1", &[&id]
|
||||||
|
).await;
|
||||||
|
|
||||||
|
return render_upload_page(&_state,
|
||||||
|
Some("Server error".to_string()), None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_upload_page(&_state,
|
||||||
|
None, Some(UploadSuccess {
|
||||||
|
id, filename, size: written, })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_file(
|
||||||
|
Path(id): Path<i32>,
|
||||||
|
state: AppState,
|
||||||
|
_user: AuthenticatedUserId,
|
||||||
|
) -> Response {
|
||||||
|
let row = match state
|
||||||
|
.db
|
||||||
|
.query_opt(
|
||||||
|
"SELECT filename, size FROM public.stored_file WHERE id = $1",
|
||||||
|
&[&id],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Some(row)) => row,
|
||||||
|
Ok(None) => return StatusCode::NOT_FOUND.into_response(),
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("file lookup error: {err}");
|
||||||
|
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let filename: String = row.get(0);
|
||||||
|
let size: i64 = row.get(1);
|
||||||
|
|
||||||
|
let path = PathBuf::from(&state.config.file_storage).join(id.to_string());
|
||||||
|
let file = match tokio::fs::File::open(&path).await {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("file open error for {path:?}: {err}");
|
||||||
|
return StatusCode::NOT_FOUND.into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let stream = ReaderStream::new(file);
|
||||||
|
let body = Body::from_stream(stream);
|
||||||
|
let mut response = Response::new(body);
|
||||||
|
if let Ok(value) = HeaderValue::from_str(&size.to_string()) {
|
||||||
|
response.headers_mut().insert(header::CONTENT_LENGTH, value);
|
||||||
|
}
|
||||||
|
response
|
||||||
|
.headers_mut()
|
||||||
|
.insert(header::CONTENT_TYPE, HeaderValue::from_static("application/octet-stream"));
|
||||||
|
response.headers_mut().insert(
|
||||||
|
header::CONTENT_DISPOSITION,
|
||||||
|
HeaderValue::from_str(&format!("attachment; filename=\"{}\"", filename)).unwrap_or_else(
|
||||||
|
|_| HeaderValue::from_static("attachment"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
response
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user