1
0
Fork 0

We have a web UI now, I guess

This commit is contained in:
Honbra 2024-06-09 18:29:07 +02:00
parent 75b87e7bac
commit 331423d3f6
Signed by: honbra
GPG key ID: B61CC9ADABE2D952
28 changed files with 1471 additions and 494 deletions

3
.prettierrc Normal file
View file

@ -0,0 +1,3 @@
{
"tabWidth": 4
}

View file

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO file_key (id, file_id) VALUES ($1, $2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid"
]
},
"nullable": []
},
"hash": "0687afdf61aef5edee2b530e67d81bf7eef0678276d1e9674398d99684f818ca"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, hash, mime FROM file WHERE id = $1",
"query": "SELECT id, title FROM paste",
"describe": {
"columns": [
{
@ -10,25 +10,17 @@
},
{
"ordinal": 1,
"name": "hash",
"type_info": "Bytea"
},
{
"ordinal": 2,
"name": "mime",
"name": "title",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
"Left": []
},
"nullable": [
false,
false,
false
]
},
"hash": "3a21749fe8df1fd7eb2e77ac89b19217f0c99925b33b928f223c33f0d8cfd551"
"hash": "218cab8328020896b78a81be345a2074ca5461affae6b7f4daa07ba6618f96aa"
}

View file

@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO file (id, hash, mime) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Bytea",
"Text"
]
},
"nullable": []
},
"hash": "25c1819af558e744e238802f3e30651897914b080ec395db706782f4fbe3042b"
}

View file

@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id FROM file_key WHERE file_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "44d061458ca74c102b74d132c8f1dd8ce5ad8839398e382af103962f206317fc"
}

View file

@ -1,16 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "SELECT hash, mime FROM file_key JOIN file ON file_id = file.id WHERE file_key.id = $1",
"query": "SELECT title, content FROM paste WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "hash",
"type_info": "Bytea"
"name": "title",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "mime",
"name": "content",
"type_info": "Text"
}
],
@ -24,5 +24,5 @@
false
]
},
"hash": "7a41221f1c34f6ac44691f3e29bb48a7bf59a667f99462c823137a04b5d80ea3"
"hash": "b5a5561c8b6458f14c64e5df09ce6e798eb6d3f63f248a950db817600f272715"
}

View file

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM file_key WHERE id = $1 AND file_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid"
]
},
"nullable": []
},
"hash": "bdb8d776b5cbf82b35b2d24a3fdb4ca049d36d8370861ba4be25372e542a0ba1"
}

View file

@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO paste (id, title, content) VALUES ($1, $2, $3)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "c9694938fa65a37d27cbbe251e7bc7ae38772b677e4d99c60c2f6d616078f94e"
}

View file

@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM file WHERE id = $1 RETURNING hash",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "hash",
"type_info": "Bytea"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "ed6ee326516d37d078ce80b39d769747a88683fab15feb466c848c2f2ba65c50"
}

303
Cargo.lock generated
View file

@ -30,12 +30,42 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
[[package]]
name = "alloc-stdlib"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "allocator-api2"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "async-trait"
version = "0.1.80"
@ -137,7 +167,6 @@ dependencies = [
"axum-core",
"bytes",
"futures-util",
"headers",
"http",
"http-body",
"http-body-util",
@ -215,6 +244,27 @@ dependencies = [
"generic-array",
]
[[package]]
name = "brotli"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19483b140a7ac7174d34b5a581b406c64f84da5409d3e09cf4fff604f9270e67"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor",
]
[[package]]
name = "brotli-decompressor"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
@ -251,12 +301,30 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"windows-targets 0.52.5",
]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "cpufeatures"
version = "0.2.12"
@ -281,6 +349,15 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.11"
@ -395,9 +472,9 @@ checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
[[package]]
name = "figment"
version = "0.10.18"
version = "0.10.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d032832d74006f99547004d49410a4b4218e4c33382d56ca3ff89df74f86b953"
checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
dependencies = [
"atomic",
"pear",
@ -413,6 +490,16 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"
[[package]]
name = "flate2"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "flume"
version = "0.11.0"
@ -559,30 +646,6 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "headers"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9"
dependencies = [
"base64",
"bytes",
"headers-core",
"http",
"httpdate",
"mime",
"sha1",
]
[[package]]
name = "headers-core"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
dependencies = [
"http",
]
[[package]]
name = "heck"
version = "0.4.1"
@ -718,6 +781,29 @@ dependencies = [
"tokio",
]
[[package]]
name = "iana-time-zone"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "idna"
version = "0.5.0"
@ -834,6 +920,30 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "maud"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df518b75016b4289cdddffa1b01f2122f4a49802c93191f3133f6dc2472ebcaa"
dependencies = [
"axum-core",
"http",
"itoa",
"maud_macros",
]
[[package]]
name = "maud_macros"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa453238ec218da0af6b11fc5978d3b5c3a45ed97b722391a2a11f3306274e18"
dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.63",
]
[[package]]
name = "md-5"
version = "0.10.6"
@ -850,6 +960,37 @@ version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
[[package]]
name = "memory-serve"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d1f8b697424035812d76b87ce97cc06bf144eb569838ebc459f0edd6b473699"
dependencies = [
"axum",
"brotli",
"flate2",
"memory-serve-macros",
"sha256",
"tracing",
]
[[package]]
name = "memory-serve-macros"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9049d464b282ee8d87ed38a9144ad263a791e4451c1eb96456da2c00845337f8"
dependencies = [
"brotli",
"mime_guess",
"proc-macro2",
"quote",
"sha256",
"syn 2.0.63",
"tracing",
"tracing-subscriber",
"walkdir",
]
[[package]]
name = "mime"
version = "0.3.17"
@ -898,21 +1039,16 @@ version = "0.1.0"
dependencies = [
"axum",
"axum-extra",
"bytes",
"chrono",
"eyre",
"figment",
"futures-util",
"headers",
"hex",
"http",
"http-body-util",
"mime",
"maud",
"memory-serve",
"serde",
"sha2",
"sqlx",
"thiserror",
"tokio",
"tokio-util",
"tower-http",
"tracing",
"tracing-subscriber",
@ -1151,6 +1287,29 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.82"
@ -1281,6 +1440,15 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
@ -1289,18 +1457,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.202"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.202"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [
"proc-macro2",
"quote",
@ -1371,6 +1539,19 @@ dependencies = [
"digest",
]
[[package]]
name = "sha256"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18278f6a914fa3070aa316493f7d2ddfb9ac86ebc06fa3b83bffda487e9065b0"
dependencies = [
"async-trait",
"bytes",
"hex",
"sha2",
"tokio",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
@ -1714,18 +1895,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.60"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.60"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",
@ -1759,9 +1940,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.37.0"
version = "1.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
dependencies = [
"backtrace",
"bytes",
@ -1776,9 +1957,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [
"proc-macro2",
"quote",
@ -2066,6 +2247,16 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -2168,12 +2359,30 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.52.5",
]
[[package]]
name = "windows-sys"
version = "0.48.0"

View file

@ -4,29 +4,24 @@ version = "0.1.0"
edition = "2021"
[dependencies]
axum = { version = "0.7.5", default-features = false, features = ["http1", "json", "macros", "matched-path", "tokio", "tower-log", "tracing", "query"] }
axum-extra = { version = "0.9.3", features = ["async-read-body", "typed-header"] }
bytes = "1.6.0"
axum = { version = "0.7.5", default-features = false, features = ["http1", "json", "macros", "matched-path", "tokio", "tower-log", "tracing", "query", "form"] }
axum-extra = { version = "0.9.3", features = ["async-read-body"] }
chrono = { version = "0.4.38", default-features = false, features = ["alloc", "clock"] }
eyre = "0.6.12"
figment = { version = "0.10.15", features = ["env", "toml"] }
futures-util = { version = "0.3.30", default-features = false }
headers = "0.4.0"
hex = "0.4.3"
figment = { version = "0.10.19", features = ["env", "toml"] }
http = "1.1.0"
http-body-util = "0.1.1"
mime = "0.3.17"
serde = { version = "1.0.197", features = ["derive"] }
sha2 = "0.10.8"
maud = { version = "0.26.0", features = ["axum"] }
memory-serve = "0.4.5"
serde = { version = "1.0.203", features = ["derive"] }
sqlx = { version = "0.7.4", features = ["runtime-tokio", "postgres", "uuid"] }
thiserror = "1.0.58"
tokio = { version = "1.37.0", features = ["rt-multi-thread", "macros", "fs", "io-std"] }
tokio-util = { version = "0.7.10", features = ["io"] }
thiserror = "1.0.61"
tokio = { version = "1.38.0", features = ["rt-multi-thread", "macros", "fs", "io-std"] }
tower-http = { version = "0.5.2", features = ["trace", "fs"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
ulid = { version = "1.1.2", features = ["uuid", "serde"] }
url = { version = "2.5.0", features = ["serde"] }
uuid = "1.7.0"
uuid = "1.8.0"
[profile.release]
strip = true

View file

@ -8,11 +8,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1712384501,
"narHash": "sha256-AZmYmEnc1ZkSlxUJVUtGh9VFAqWPr+xtNIiBqD2eKfc=",
"lastModified": 1717741716,
"narHash": "sha256-v71DDu2gb02iBIAhUwu5mQZNtYD45QcbEqahqsOCCyU=",
"owner": "nix-community",
"repo": "fenix",
"rev": "99c6241db5ca5363c05c8f4acbdf3a4e8fc42844",
"rev": "05f2a8ae62c45637b20d162fa2dd450b79e71c27",
"type": "github"
},
"original": {
@ -41,11 +41,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1712439257,
"narHash": "sha256-aSpiNepFOMk9932HOax0XwNxbA38GOUVOiXfUVPOrck=",
"lastModified": 1717602782,
"narHash": "sha256-pL9jeus5QpX5R+9rsp3hhZ+uplVHscNJh8n8VpqscM0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ff0dbd94265ac470dda06a657d5fe49de93b4599",
"rev": "e8057b67ebf307f01bdcc8fba94d94f75039d1f6",
"type": "github"
},
"original": {
@ -65,11 +65,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1712156296,
"narHash": "sha256-St7ZQrkrr5lmQX9wC1ZJAFxL8W7alswnyZk9d1se3Us=",
"lastModified": 1717583671,
"narHash": "sha256-+lRAmz92CNUxorqWusgJbL9VE1eKCnQQojglRemzwkw=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "8e581ac348e223488622f4d3003cb2bd412bf27e",
"rev": "48bbdd6a74f3176987d5c809894ac33957000d19",
"type": "github"
},
"original": {

View file

@ -44,6 +44,7 @@
postgresql
sqlfluff
sqlx-cli
tailwindcss
];
};
}

View file

@ -1,3 +1,2 @@
DROP TABLE IF EXISTS file_key;
DROP TABLE IF EXISTS file;
DROP TABLE IF EXISTS paste;
DROP TABLE IF EXISTS link;

View file

@ -4,13 +4,8 @@ CREATE TABLE IF NOT EXISTS link (
destination TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS file (
CREATE TABLE IF NOT EXISTS paste (
id UUID PRIMARY KEY,
hash BYTEA UNIQUE NOT NULL,
mime TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS file_key (
id UUID PRIMARY KEY,
file_id UUID REFERENCES file (id) ON DELETE CASCADE NOT NULL
title TEXT NOT NULL,
content TEXT NOT NULL
);

View file

@ -1,252 +0,0 @@
use std::path::PathBuf;
use axum::{
body::Body,
extract::{Path, Query, State},
routing::{delete, get, post},
Json, Router,
};
use axum_extra::TypedHeader;
use futures_util::TryStreamExt;
use headers::ContentType;
use http::StatusCode;
use mime::Mime;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use sqlx::query;
use tokio::{fs, io};
use tokio_util::io::StreamReader;
use tracing::{error, field, info, instrument};
use ulid::Ulid;
use uuid::Uuid;
use crate::{app::SharedState, error::AppError};
pub fn resource() -> Router<SharedState> {
Router::new()
.route("/files", post(upload_file))
.route("/files/:file_id", get(get_file_info))
.route("/files/:file_id", delete(delete_file))
.route("/files/:file_id/keys/", post(create_file_key))
.route("/files/:file_id/keys/:key_id", delete(delete_file_key))
}
#[derive(Serialize)]
struct File {
id: Ulid,
hash: String,
mime: String,
keys: Vec<Ulid>,
}
#[derive(Serialize)]
struct NewFile {
id: Ulid,
hash: String,
mime: String,
key: Option<Ulid>,
}
#[derive(Deserialize)]
struct UploadFileOptions {
#[serde(default)]
create_key: bool,
}
#[instrument(skip_all)]
async fn upload_file(
State(SharedState { db, config }): State<SharedState>,
Query(UploadFileOptions { create_key }): Query<UploadFileOptions>,
TypedHeader(content_type): TypedHeader<ContentType>,
body: Body,
) -> Result<Json<NewFile>, AppError> {
let id = Ulid::new();
let path_temp = config.file_temp_dir.join(id.to_string());
let mut hasher = Sha256::new();
{
let mut file_temp = fs::File::create(&path_temp).await?;
let better_body = body
.into_data_stream()
.inspect_ok(|b| hasher.update(b))
.map_err(|err| io::Error::new(io::ErrorKind::Other, err));
let mut reader = StreamReader::new(better_body);
if let Err(err) = io::copy(&mut reader, &mut file_temp).await {
error!(
err = field::display(&err),
file_path = field::debug(&path_temp),
"failed to copy file, removing",
);
drop(file_temp);
if let Err(err) = fs::remove_file(path_temp).await {
error!(
err = field::display(err),
"failed to remove failed upload file",
);
}
return Err(err.into());
}
}
let hash = hasher.finalize();
let hash_hex = hex::encode(hash);
let path_hash = PathBuf::from("files").join(&hash_hex);
if fs::try_exists(&path_hash).await? {
info!(hash = hash_hex, "file already exists");
if let Err(err) = fs::remove_file(&path_temp).await {
error!(err = field::display(&err), "failed to remove temp file");
}
} else if let Err(err) = fs::rename(&path_temp, &path_hash).await {
error!(err = field::display(&err), "failed to move finished file");
if let Err(err) = fs::remove_file(&path_temp).await {
error!(
err = field::display(&err),
"failed to remove file after failed move",
);
}
return Err(err.into());
}
let mime = Into::<Mime>::into(content_type);
let mime_str = mime.to_string();
let mut tx = db.begin().await?;
match query!(
"INSERT INTO file (id, hash, mime) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
Uuid::from(id),
&hash[..],
mime_str,
)
.execute(&mut *tx)
.await?
.rows_affected()
{
0 | 1 => {}
rows => return Err(AppError::ImpossibleAffectedRows(rows)),
}
// `ON CONFLICT DO NOTHING RETURNING id` only works when there *isn't* a
// conflict
let id = query!("SELECT id FROM file WHERE hash = $1", &hash[..])
.fetch_one(&mut *tx)
.await?
.id
.into();
let mut key_opt = None;
if create_key {
let key = Ulid::new();
key_opt = Some(key);
match query!(
"INSERT INTO file_key (id, file_id) VALUES ($1, $2)",
Uuid::from(key),
Uuid::from(id),
)
.execute(&mut *tx)
.await?
.rows_affected()
{
1 => {}
0 => return Err(AppError::UlidConflict(key)),
rows => return Err(AppError::ImpossibleAffectedRows(rows)),
}
}
tx.commit().await?;
Ok(Json(NewFile {
id,
hash: hash_hex,
mime: mime_str,
key: key_opt,
}))
}
async fn get_file_info(
State(SharedState { db, .. }): State<SharedState>,
Path(id): Path<Ulid>,
) -> Result<Json<File>, AppError> {
let (file, keys) = tokio::try_join!(
query!(
"SELECT id, hash, mime FROM file WHERE id = $1",
Uuid::from(id),
)
.fetch_optional(&db),
query!("SELECT id FROM file_key WHERE file_id = $1", Uuid::from(id)).fetch_all(&db),
)?;
match file {
Some(r) => Ok(Json(File {
id,
hash: hex::encode(r.hash),
mime: r.mime,
keys: keys.into_iter().map(|r| r.id.into()).collect(),
})),
None => Err(AppError::FileNotFound(id)),
}
}
#[instrument(skip_all)]
async fn delete_file(
State(SharedState { db, config }): State<SharedState>,
Path(file_id): Path<Ulid>,
) -> Result<StatusCode, AppError> {
let file_hash = query!(
"DELETE FROM file WHERE id = $1 RETURNING hash",
Uuid::from(file_id)
)
.fetch_optional(&db)
.await?
.ok_or(AppError::FileNotFound(file_id))?
.hash;
let file_path = config.file_store_dir.join(hex::encode(file_hash));
if let Err(err) = fs::remove_file(file_path).await {
error!(err = field::display(err), "failed to remove file");
}
Ok(StatusCode::NO_CONTENT)
}
async fn create_file_key(
State(SharedState { db, .. }): State<SharedState>,
Path(file_id): Path<Ulid>,
) -> Result<(StatusCode, Json<Ulid>), AppError> {
let key_id = Ulid::new();
match query!(
"INSERT INTO file_key (id, file_id) VALUES ($1, $2)",
Uuid::from(key_id),
Uuid::from(file_id),
)
.execute(&db)
.await?
.rows_affected()
{
1 => Ok((StatusCode::CREATED, Json(key_id))),
rows => Err(AppError::ImpossibleAffectedRows(rows)),
}
}
async fn delete_file_key(
State(SharedState { db, .. }): State<SharedState>,
Path((file_id, key_id)): Path<(Ulid, Ulid)>,
) -> Result<StatusCode, AppError> {
match query!(
"DELETE FROM file_key WHERE id = $1 AND file_id = $2",
Uuid::from(key_id),
Uuid::from(file_id),
)
.execute(&db)
.await?
.rows_affected()
{
1 => Ok(StatusCode::NO_CONTENT),
0 => Err(AppError::FileKeyNotFound(key_id)),
rows => Err(AppError::ImpossibleAffectedRows(rows)),
}
}

View file

@ -1,4 +1,3 @@
mod files;
mod links;
use axum::Router;
@ -6,7 +5,5 @@ use axum::Router;
use super::SharedState;
pub fn router() -> Router<SharedState> {
Router::new()
.merge(files::resource())
.merge(links::resource())
Router::new().merge(links::resource())
}

View file

@ -1,10 +1,12 @@
mod api;
mod pages;
mod root;
use std::sync::Arc;
use axum::{body::Body, Router};
use http::Request;
use memory_serve::{load_assets, MemoryServe};
use sqlx::{postgres::PgConnectOptions, PgPool};
use tower_http::trace::{DefaultOnRequest, DefaultOnResponse, TraceLayer};
use tracing::{field, span, Level};
@ -14,7 +16,7 @@ use crate::config::Config;
#[derive(Clone)]
struct SharedState {
db: PgPool,
config: Arc<Config>,
_config: Arc<Config>,
}
pub async fn build_app(config: Config) -> eyre::Result<Router> {
@ -27,11 +29,16 @@ pub async fn build_app(config: Config) -> eyre::Result<Router> {
)
.await?;
Ok(root::router()
let memory_router = MemoryServe::new(load_assets!("./static/")).into_router();
Ok(Router::new()
.merge(memory_router)
.merge(pages::router())
.nest("/api", api::router())
.merge(root::router())
.with_state(SharedState {
db,
config: Arc::new(config),
_config: Arc::new(config),
})
.layer(
TraceLayer::new_for_http()

View file

@ -0,0 +1,27 @@
mod pastes;
use axum::{routing::get, Router};
use maud::{html, Markup};
use super::{page, Breadcrumb, BC_INDEX};
use crate::app::SharedState;
pub(super) fn router() -> Router<SharedState> {
Router::new()
.route("/", get(show_admin_page()))
.nest("/pastes", pastes::router())
}
const BC_ADMIN: Breadcrumb = Breadcrumb::new_static("Admin", "/admin");
fn show_admin_page() -> Markup {
page(
"Admin",
&[BC_INDEX],
html! {
"meow :3"
p { a.tl href="/admin/pastes" { "Pastes" } }
p { a.tl href="/admin/pastes/new" { "Create Paste" } }
},
)
}

View file

@ -0,0 +1,113 @@
use axum::{
extract::{Query, State},
response::Redirect,
routing::get,
Form, Router,
};
use maud::{html, Markup};
use serde::Deserialize;
use sqlx::query;
use ulid::Ulid;
use uuid::Uuid;
use super::BC_ADMIN;
use crate::{
app::{
pages::{dt_iso, page, Breadcrumb, BC_INDEX},
SharedState,
},
error::AppError,
};
pub(super) fn router() -> Router<SharedState> {
Router::new()
.route("/", get(show_pastes).post(create_paste))
.route("/new", get(show_create_paste))
}
const BC_PASTES: Breadcrumb = Breadcrumb::new_static("Pastes", "/admin/pastes");
async fn show_pastes(
State(SharedState { db, .. }): State<SharedState>,
) -> Result<Markup, AppError> {
Ok(page(
"Pastes",
&[BC_INDEX, BC_ADMIN],
html! {
table class="w-full" {
thead {
tr {
th { "Name" }
th { "Created at" }
th { "Actions" }
}
}
tbody {
@for p in query!("SELECT id, title FROM paste").fetch_all(&db).await? {
tr {
td { a.tl href=(format!("/p/{}", Ulid::from(p.id))) { (p.title)} }
td { (dt_iso(Ulid::from(p.id).datetime())) }
td class="w-min-content" {
div class="flex gap-2" {
a.tl href="#" { "Edit" }
a.tl href="#" { "Delete" }
}
}
}
}
}
}
},
))
}
#[derive(Deserialize)]
struct CreatePasteFieldsOptional {
title: Option<String>,
content: Option<String>,
}
async fn show_create_paste(
Query(CreatePasteFieldsOptional { title, content }): Query<CreatePasteFieldsOptional>,
) -> Markup {
page(
"Create Paste",
&[BC_INDEX, BC_ADMIN, BC_PASTES],
html! {
form method="post" action="/admin/pastes" {
fieldset {
legend { "Paste" }
input class="w-full" type="text" name="title" placeholder="Paste title" required value=(title.unwrap_or_default());
textarea class="w-full min-h-32" name="content" placeholder="Paste content" required { (content.unwrap_or_default()) }
input type="submit" value="Create";
}
}
},
)
}
#[derive(Deserialize)]
struct CreatePasteFields {
title: String,
content: String,
}
async fn create_paste(
State(SharedState { db, .. }): State<SharedState>,
Form(CreatePasteFields { title, content }): Form<CreatePasteFields>,
) -> Result<Redirect, AppError> {
let id = Ulid::new();
match query!(
"INSERT INTO paste (id, title, content) VALUES ($1, $2, $3)",
Uuid::from(id),
title,
content,
)
.execute(&db)
.await?
.rows_affected()
{
1 => Ok(Redirect::temporary(&format!("/p/{id}"))),
r => Err(AppError::ImpossibleAffectedRows(r)),
}
}

78
src/app/pages/mod.rs Normal file
View file

@ -0,0 +1,78 @@
mod admin;
mod pastes_public;
use std::{borrow::Cow, time::SystemTime};
use axum::Router;
use chrono::{DateTime, Utc};
use maud::{html, Markup, DOCTYPE};
use super::SharedState;
pub fn router() -> Router<SharedState> {
Router::new()
.nest("/p", pastes_public::router())
.nest("/admin", admin::router())
}
const BC_INDEX: Breadcrumb = Breadcrumb::new_static("NCPN", "/");
struct Breadcrumb<'a> {
title: Cow<'a, str>,
href: Cow<'a, str>,
}
impl<'a> Breadcrumb<'a> {
fn new(title: impl Into<Cow<'a, str>>, href: impl Into<Cow<'a, str>>) -> Self {
Breadcrumb {
title: title.into(),
href: href.into(),
}
}
}
impl Breadcrumb<'static> {
const fn new_static(title: &'static str, href: &'static str) -> Self {
Breadcrumb {
title: Cow::Borrowed(title),
href: Cow::Borrowed(href),
}
}
}
fn page(title: &str, breadcrumbs: &[Breadcrumb], content: Markup) -> Markup {
html! {
(DOCTYPE)
html {
head {
meta charset="utf-8";
title { (format!("{title} | NCPN")) }
link rel="stylesheet" href="/style.css";
script src="/script.js" defer {}
}
body {
header {
div {
@for b in breadcrumbs {
a.tl href=(b.href) { (b.title) }
span { "/" }
}
}
h1 { (title) }
}
(content)
footer {
"© 2024 mrrp meow :3"
}
}
}
}
}
fn dt_iso(t: SystemTime) -> Markup {
let dt: DateTime<Utc> = t.into();
let iso = dt.to_rfc3339();
html! {
time datetime=(iso) { (iso) }
}
}

View file

@ -0,0 +1,42 @@
// 01HZT59RTH4R6P1TYE6NMAFYEP 018FF454-E351-260D-60EB-CE3568A7F9D6
use axum::{
extract::{Path, State},
routing::get,
Router,
};
use maud::{html, Markup};
use sqlx::query;
use ulid::Ulid;
use uuid::Uuid;
use super::{page, Breadcrumb, BC_INDEX};
use crate::{app::SharedState, error::AppError};
pub(super) fn router() -> Router<SharedState> {
Router::new().route("/:id", get(show_paste))
}
const BC_PASTES_PUBLIC: Breadcrumb = Breadcrumb::new_static("Pastes", "/p");
async fn show_paste(
Path(id): Path<Ulid>,
State(SharedState { db, .. }): State<SharedState>,
) -> Result<Markup, AppError> {
match query!(
"SELECT title, content FROM paste WHERE id = $1",
Uuid::from(id)
)
.fetch_optional(&db)
.await?
{
Some(r) => Ok(page(
&r.title,
&[BC_INDEX, BC_PASTES_PUBLIC],
html! {
pre class="p-4 border border-bd-base rounded-xl" { (r.content) }
},
)),
None => Err(AppError::PasteNotFound(id)),
}
}

View file

@ -1,26 +1,16 @@
use axum::{
body::Body,
extract::{Path, State},
response::Redirect,
routing::get,
BoxError, Router,
Router,
};
use bytes::Bytes;
use http::{Request, Response};
use http_body_util::{combinators::UnsyncBoxBody, BodyExt};
use mime::Mime;
use sqlx::query;
use tower_http::services::ServeFile;
use ulid::Ulid;
use uuid::Uuid;
use super::SharedState;
use crate::error::AppError;
pub fn router() -> Router<SharedState> {
Router::new()
.route("/:slug", get(redirect_link))
.route("/f/:key", get(download_file))
Router::new().route("/:slug", get(redirect_link))
}