From 331423d3f6ae97b366d70a9b54aeafe4173de0fa Mon Sep 17 00:00:00 2001 From: Honbra Date: Sun, 9 Jun 2024 18:29:07 +0200 Subject: [PATCH] We have a web UI now, I guess --- .prettierrc | 3 + ...81bf7eef0678276d1e9674398d99684f818ca.json | 15 - ...2074ca5461affae6b7f4daa07ba6618f96aa.json} | 16 +- ...0651897914b080ec395db706782f4fbe3042b.json | 16 - ...1dd8ce5ad8839398e382af103962f206317fc.json | 22 - ...6e798eb6d3f63f248a950db817600f272715.json} | 10 +- ...b4ca049d36d8370861ba4be25372e542a0ba1.json | 15 - ...bc7ae38772b677e4d99c60c2f6d616078f94e.json | 16 + ...69747a88683fab15feb466c848c2f2ba65c50.json | 22 - Cargo.lock | 303 +++++-- Cargo.toml | 25 +- flake.lock | 18 +- flake.nix | 1 + .../20240416191149_create-link-file.down.sql | 3 +- .../20240416191149_create-link-file.up.sql | 11 +- src/app/api/files.rs | 252 ------ src/app/api/mod.rs | 5 +- src/app/mod.rs | 13 +- src/app/pages/admin/mod.rs | 27 + src/app/pages/admin/pastes.rs | 113 +++ src/app/pages/mod.rs | 78 ++ src/app/pages/pastes_public.rs | 42 + src/app/root.rs | 38 +- src/error.rs | 14 +- src/style.scss | 50 ++ static/script.js | 8 + static/style.css | 795 ++++++++++++++++++ tailwind.config.js | 34 + 28 files changed, 1471 insertions(+), 494 deletions(-) create mode 100644 .prettierrc delete mode 100644 .sqlx/query-0687afdf61aef5edee2b530e67d81bf7eef0678276d1e9674398d99684f818ca.json rename .sqlx/{query-3a21749fe8df1fd7eb2e77ac89b19217f0c99925b33b928f223c33f0d8cfd551.json => query-218cab8328020896b78a81be345a2074ca5461affae6b7f4daa07ba6618f96aa.json} (50%) delete mode 100644 .sqlx/query-25c1819af558e744e238802f3e30651897914b080ec395db706782f4fbe3042b.json delete mode 100644 .sqlx/query-44d061458ca74c102b74d132c8f1dd8ce5ad8839398e382af103962f206317fc.json rename .sqlx/{query-7a41221f1c34f6ac44691f3e29bb48a7bf59a667f99462c823137a04b5d80ea3.json => query-b5a5561c8b6458f14c64e5df09ce6e798eb6d3f63f248a950db817600f272715.json} (53%) delete mode 100644 .sqlx/query-bdb8d776b5cbf82b35b2d24a3fdb4ca049d36d8370861ba4be25372e542a0ba1.json create mode 100644 .sqlx/query-c9694938fa65a37d27cbbe251e7bc7ae38772b677e4d99c60c2f6d616078f94e.json delete mode 100644 .sqlx/query-ed6ee326516d37d078ce80b39d769747a88683fab15feb466c848c2f2ba65c50.json delete mode 100644 src/app/api/files.rs create mode 100644 src/app/pages/admin/mod.rs create mode 100644 src/app/pages/admin/pastes.rs create mode 100644 src/app/pages/mod.rs create mode 100644 src/app/pages/pastes_public.rs create mode 100644 src/style.scss create mode 100644 static/script.js create mode 100644 static/style.css create mode 100644 tailwind.config.js diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..0a02bce --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "tabWidth": 4 +} diff --git a/.sqlx/query-0687afdf61aef5edee2b530e67d81bf7eef0678276d1e9674398d99684f818ca.json b/.sqlx/query-0687afdf61aef5edee2b530e67d81bf7eef0678276d1e9674398d99684f818ca.json deleted file mode 100644 index 7a64f17..0000000 --- a/.sqlx/query-0687afdf61aef5edee2b530e67d81bf7eef0678276d1e9674398d99684f818ca.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-3a21749fe8df1fd7eb2e77ac89b19217f0c99925b33b928f223c33f0d8cfd551.json b/.sqlx/query-218cab8328020896b78a81be345a2074ca5461affae6b7f4daa07ba6618f96aa.json similarity index 50% rename from .sqlx/query-3a21749fe8df1fd7eb2e77ac89b19217f0c99925b33b928f223c33f0d8cfd551.json rename to .sqlx/query-218cab8328020896b78a81be345a2074ca5461affae6b7f4daa07ba6618f96aa.json index 02b3740..78b779d 100644 --- a/.sqlx/query-3a21749fe8df1fd7eb2e77ac89b19217f0c99925b33b928f223c33f0d8cfd551.json +++ b/.sqlx/query-218cab8328020896b78a81be345a2074ca5461affae6b7f4daa07ba6618f96aa.json @@ -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" } diff --git a/.sqlx/query-25c1819af558e744e238802f3e30651897914b080ec395db706782f4fbe3042b.json b/.sqlx/query-25c1819af558e744e238802f3e30651897914b080ec395db706782f4fbe3042b.json deleted file mode 100644 index 4425075..0000000 --- a/.sqlx/query-25c1819af558e744e238802f3e30651897914b080ec395db706782f4fbe3042b.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-44d061458ca74c102b74d132c8f1dd8ce5ad8839398e382af103962f206317fc.json b/.sqlx/query-44d061458ca74c102b74d132c8f1dd8ce5ad8839398e382af103962f206317fc.json deleted file mode 100644 index 2037b55..0000000 --- a/.sqlx/query-44d061458ca74c102b74d132c8f1dd8ce5ad8839398e382af103962f206317fc.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-7a41221f1c34f6ac44691f3e29bb48a7bf59a667f99462c823137a04b5d80ea3.json b/.sqlx/query-b5a5561c8b6458f14c64e5df09ce6e798eb6d3f63f248a950db817600f272715.json similarity index 53% rename from .sqlx/query-7a41221f1c34f6ac44691f3e29bb48a7bf59a667f99462c823137a04b5d80ea3.json rename to .sqlx/query-b5a5561c8b6458f14c64e5df09ce6e798eb6d3f63f248a950db817600f272715.json index 89d45bd..82888bd 100644 --- a/.sqlx/query-7a41221f1c34f6ac44691f3e29bb48a7bf59a667f99462c823137a04b5d80ea3.json +++ b/.sqlx/query-b5a5561c8b6458f14c64e5df09ce6e798eb6d3f63f248a950db817600f272715.json @@ -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" } diff --git a/.sqlx/query-bdb8d776b5cbf82b35b2d24a3fdb4ca049d36d8370861ba4be25372e542a0ba1.json b/.sqlx/query-bdb8d776b5cbf82b35b2d24a3fdb4ca049d36d8370861ba4be25372e542a0ba1.json deleted file mode 100644 index 1ec0cb5..0000000 --- a/.sqlx/query-bdb8d776b5cbf82b35b2d24a3fdb4ca049d36d8370861ba4be25372e542a0ba1.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-c9694938fa65a37d27cbbe251e7bc7ae38772b677e4d99c60c2f6d616078f94e.json b/.sqlx/query-c9694938fa65a37d27cbbe251e7bc7ae38772b677e4d99c60c2f6d616078f94e.json new file mode 100644 index 0000000..7f5bc35 --- /dev/null +++ b/.sqlx/query-c9694938fa65a37d27cbbe251e7bc7ae38772b677e4d99c60c2f6d616078f94e.json @@ -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" +} diff --git a/.sqlx/query-ed6ee326516d37d078ce80b39d769747a88683fab15feb466c848c2f2ba65c50.json b/.sqlx/query-ed6ee326516d37d078ce80b39d769747a88683fab15feb466c848c2f2ba65c50.json deleted file mode 100644 index 5592394..0000000 --- a/.sqlx/query-ed6ee326516d37d078ce80b39d769747a88683fab15feb466c848c2f2ba65c50.json +++ /dev/null @@ -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" -} diff --git a/Cargo.lock b/Cargo.lock index a6cbd3d..8bb577e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 9177815..30ab0d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/flake.lock b/flake.lock index ab63de2..5041ff9 100644 --- a/flake.lock +++ b/flake.lock @@ -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": { diff --git a/flake.nix b/flake.nix index 1a05d00..ebeb760 100644 --- a/flake.nix +++ b/flake.nix @@ -44,6 +44,7 @@ postgresql sqlfluff sqlx-cli + tailwindcss ]; }; } diff --git a/migrations/20240416191149_create-link-file.down.sql b/migrations/20240416191149_create-link-file.down.sql index ccb05ce..4fe8d86 100644 --- a/migrations/20240416191149_create-link-file.down.sql +++ b/migrations/20240416191149_create-link-file.down.sql @@ -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; diff --git a/migrations/20240416191149_create-link-file.up.sql b/migrations/20240416191149_create-link-file.up.sql index d386000..886ba49 100644 --- a/migrations/20240416191149_create-link-file.up.sql +++ b/migrations/20240416191149_create-link-file.up.sql @@ -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 ); diff --git a/src/app/api/files.rs b/src/app/api/files.rs deleted file mode 100644 index 023dc12..0000000 --- a/src/app/api/files.rs +++ /dev/null @@ -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 { - 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, -} - -#[derive(Serialize)] -struct NewFile { - id: Ulid, - hash: String, - mime: String, - key: Option, -} - -#[derive(Deserialize)] -struct UploadFileOptions { - #[serde(default)] - create_key: bool, -} - -#[instrument(skip_all)] -async fn upload_file( - State(SharedState { db, config }): State, - Query(UploadFileOptions { create_key }): Query, - TypedHeader(content_type): TypedHeader, - body: Body, -) -> Result, 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::::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, - Path(id): Path, -) -> Result, 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, - Path(file_id): Path, -) -> Result { - 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, - Path(file_id): Path, -) -> Result<(StatusCode, Json), 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, - Path((file_id, key_id)): Path<(Ulid, Ulid)>, -) -> Result { - 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)), - } -} diff --git a/src/app/api/mod.rs b/src/app/api/mod.rs index e38bc37..00fd65c 100644 --- a/src/app/api/mod.rs +++ b/src/app/api/mod.rs @@ -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 { - Router::new() - .merge(files::resource()) - .merge(links::resource()) + Router::new().merge(links::resource()) } diff --git a/src/app/mod.rs b/src/app/mod.rs index e8f7b32..80b50f6 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -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: Arc, } pub async fn build_app(config: Config) -> eyre::Result { @@ -27,11 +29,16 @@ pub async fn build_app(config: Config) -> eyre::Result { ) .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() diff --git a/src/app/pages/admin/mod.rs b/src/app/pages/admin/mod.rs new file mode 100644 index 0000000..cd46007 --- /dev/null +++ b/src/app/pages/admin/mod.rs @@ -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 { + 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" } } + }, + ) +} diff --git a/src/app/pages/admin/pastes.rs b/src/app/pages/admin/pastes.rs new file mode 100644 index 0000000..5055d64 --- /dev/null +++ b/src/app/pages/admin/pastes.rs @@ -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 { + 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, +) -> Result { + 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, + content: Option, +} + +async fn show_create_paste( + Query(CreatePasteFieldsOptional { title, content }): Query, +) -> 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, + Form(CreatePasteFields { title, content }): Form, +) -> Result { + 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)), + } +} diff --git a/src/app/pages/mod.rs b/src/app/pages/mod.rs new file mode 100644 index 0000000..1452eb8 --- /dev/null +++ b/src/app/pages/mod.rs @@ -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 { + 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>, href: impl Into>) -> 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 = t.into(); + let iso = dt.to_rfc3339(); + html! { + time datetime=(iso) { (iso) } + } +} diff --git a/src/app/pages/pastes_public.rs b/src/app/pages/pastes_public.rs new file mode 100644 index 0000000..b863ac2 --- /dev/null +++ b/src/app/pages/pastes_public.rs @@ -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 { + Router::new().route("/:id", get(show_paste)) +} + +const BC_PASTES_PUBLIC: Breadcrumb = Breadcrumb::new_static("Pastes", "/p"); + +async fn show_paste( + Path(id): Path, + State(SharedState { db, .. }): State, +) -> Result { + 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)), + } +} diff --git a/src/app/root.rs b/src/app/root.rs index 1e3e74f..8c6e345 100644 --- a/src/app/root.rs +++ b/src/app/root.rs @@ -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 { - Router::new() - .route("/:slug", get(redirect_link)) - .route("/f/:key", get(download_file)) + Router::new().route("/:slug", get(redirect_link)) } async fn redirect_link( @@ -35,27 +25,3 @@ async fn redirect_link( None => Err(AppError::LinkNotFoundSlug(slug)), } } - -async fn download_file( - State(SharedState { db, config }): State, - Path(key): Path, - request: Request, -) -> Result>, AppError> { - let file = query!( - "SELECT hash, mime FROM file_key JOIN file ON file_id = file.id WHERE file_key.id = $1", - Uuid::from(key), - ) - .fetch_optional(&db) - .await? - .ok_or(AppError::FileKeyNotFound(key))?; - let mime: Option = file.mime.parse().ok(); - let path = config.file_store_dir.join(hex::encode(file.hash)); - let mut sf = match mime { - Some(mime) => ServeFile::new_with_mime(path, &mime), - None => ServeFile::new(path), - }; - match sf.try_call(request).await { - Ok(response) => Ok(response.map(|body| body.map_err(Into::into).boxed_unsync())), - Err(err) => Err(AppError::Io(err)), - } -} diff --git a/src/error.rs b/src/error.rs index a971ac2..dc245dd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,3 @@ -use std::path::PathBuf; - use axum::{body::Body, response::IntoResponse}; use http::StatusCode; use tracing::{error, field}; @@ -13,12 +11,8 @@ pub enum AppError { LinkNotFoundId(Ulid), #[error("link not found ({0})")] LinkNotFoundSlug(String), - #[error("file not found ({0})")] - FileNotFound(Ulid), - #[error("file key not found ({0})")] - FileKeyNotFound(Ulid), - #[error("file is missing ({0})")] - FileMissing(PathBuf), + #[error("paste not found ({0})")] + PasteNotFound(Ulid), #[error("database returned an impossible number of affected rows ({0})")] ImpossibleAffectedRows(u64), #[error("ulid conflict")] @@ -39,9 +33,7 @@ impl IntoResponse for AppError { Self::LinkNotFoundId(_) | Self::LinkNotFoundSlug(_) => { (StatusCode::NOT_FOUND, "Link not found") } - Self::FileNotFound(_) => (StatusCode::NOT_FOUND, "File not found"), - Self::FileKeyNotFound(_) => (StatusCode::NOT_FOUND, "File key not found"), - Self::FileMissing(_) => (StatusCode::INTERNAL_SERVER_ERROR, "File is missing"), + Self::PasteNotFound(_) => (StatusCode::NOT_FOUND, "Paste not found"), Self::ImpossibleAffectedRows(_) => ( StatusCode::INTERNAL_SERVER_ERROR, "Database returned an impossible number of affected rows", diff --git a/src/style.scss b/src/style.scss new file mode 100644 index 0000000..1c26c0b --- /dev/null +++ b/src/style.scss @@ -0,0 +1,50 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + @apply flex flex-col gap-8 px-4 py-16 mx-auto max-w-4xl bg-slate-900 text-fg-base max-lg:py-8; +} + +header { + div { + @apply flex gap-2 mb-1 text-lg text-fg-deemphasized; + } + + h1 { + @apply text-5xl font-extrabold text-fg-headings; + } +} + +footer { + @apply text-center text-fg-deemphasized; +} + +a.tl { + @apply underline hover:no-underline hover:text-white focus-visible:no-underline focus-visible:text-white; +} + +table { + @apply table-auto border-collapse border border-bd-base; + + thead tr th { + @apply border border-bd-base px-2 py-1 text-left; + } + + tbody tr td { + @apply border border-bd-base px-2 py-1; + } +} + +form fieldset { + @apply flex flex-col p-4 pt-2 gap-4 border border-bd-base rounded-xl; + + input[type="text"], + textarea { + @apply px-3 py-2 rounded-lg bg-transparent border border-bd-base hover:border-bd-highlighted focus-visible:border-bd-highlighted; + } + + input[type="submit"] { + @apply ml-auto px-3 py-2 rounded-lg cursor-pointer border border-bd-base hover:border-bd-highlighted focus-visible:border-bd-highlighted; + } +} diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..1d19834 --- /dev/null +++ b/static/script.js @@ -0,0 +1,8 @@ +document + .querySelectorAll("time[datetime]") + .forEach( + (e) => + (e.innerText = new Date( + e.getAttribute("datetime") + ).toLocaleString()) + ); diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..4d6ded7 --- /dev/null +++ b/static/style.css @@ -0,0 +1,795 @@ +/* +! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS +*/ + +html, +:host { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: sans-serif; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ + -webkit-tap-highlight-color: transparent; + /* 7 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font-family by default. +2. Use the user's configured `mono` font-feature-settings by default. +3. Use the user's configured `mono` font-variation-settings by default. +4. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: monospace; + /* 1 */ + font-feature-settings: normal; + /* 2 */ + font-variation-settings: normal; + /* 3 */ + font-size: 1em; + /* 4 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + letter-spacing: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +input:where([type='button']), +input:where([type='reset']), +input:where([type='submit']) { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +.static { + position: static; +} + +.flex { + display: flex; +} + +.table { + display: table; +} + +.min-h-32 { + min-height: 8rem; +} + +.w-full { + width: 100%; +} + +.gap-2 { + gap: 0.5rem; +} + +.rounded-xl { + border-radius: 0.75rem; +} + +.border { + border-width: 1px; +} + +.border-bd-base { + --tw-border-opacity: 1; + border-color: rgb(51 65 85 / var(--tw-border-opacity)); +} + +.p-4 { + padding: 1rem; +} + +body { + margin-left: auto; + margin-right: auto; + display: flex; + max-width: 56rem; + flex-direction: column; + gap: 2rem; + --tw-bg-opacity: 1; + background-color: rgb(15 23 42 / var(--tw-bg-opacity)); + padding-left: 1rem; + padding-right: 1rem; + padding-top: 4rem; + padding-bottom: 4rem; + --tw-text-opacity: 1; + color: rgb(203 213 225 / var(--tw-text-opacity)); +} + +@media not all and (min-width: 1024px) { + body { + padding-top: 2rem; + padding-bottom: 2rem; + } +} + +header { + div { + margin-bottom: 0.25rem; + } + div { + display: flex; + } + div { + gap: 0.5rem; + } + div { + font-size: 1.125rem; + line-height: 1.75rem; + } + div { + --tw-text-opacity: 1; + color: rgb(148 163 184 / var(--tw-text-opacity)); + } + h1 { + font-size: 3rem; + line-height: 1; + } + h1 { + font-weight: 800; + } + h1 { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); + } +} + +footer { + text-align: center; + --tw-text-opacity: 1; + color: rgb(148 163 184 / var(--tw-text-opacity)); +} + +a.tl { + text-decoration-line: underline; +} + +a.tl:hover { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); + text-decoration-line: none; +} + +a.tl:focus-visible { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); + text-decoration-line: none; +} + +table { + table-layout: auto; + border-collapse: collapse; + border-width: 1px; + --tw-border-opacity: 1; + border-color: rgb(51 65 85 / var(--tw-border-opacity)); + thead tr th { + border-width: 1px; + } + thead tr th { + --tw-border-opacity: 1; + border-color: rgb(51 65 85 / var(--tw-border-opacity)); + } + thead tr th { + padding-left: 0.5rem; + padding-right: 0.5rem; + } + thead tr th { + padding-top: 0.25rem; + padding-bottom: 0.25rem; + } + thead tr th { + text-align: left; + } + tbody tr td { + border-width: 1px; + } + tbody tr td { + --tw-border-opacity: 1; + border-color: rgb(51 65 85 / var(--tw-border-opacity)); + } + tbody tr td { + padding-left: 0.5rem; + padding-right: 0.5rem; + } + tbody tr td { + padding-top: 0.25rem; + padding-bottom: 0.25rem; + } +} + +form fieldset { + display: flex; + flex-direction: column; + gap: 1rem; + border-radius: 0.75rem; + border-width: 1px; + --tw-border-opacity: 1; + border-color: rgb(51 65 85 / var(--tw-border-opacity)); + padding: 1rem; + padding-top: 0.5rem; + input[type="text"], + textarea { + border-radius: 0.5rem; + } + input[type="text"], + textarea { + border-width: 1px; + } + input[type="text"], + textarea { + --tw-border-opacity: 1; + border-color: rgb(51 65 85 / var(--tw-border-opacity)); + } + input[type="text"], + textarea { + background-color: transparent; + } + input[type="text"], + textarea { + padding-left: 0.75rem; + padding-right: 0.75rem; + } + input[type="text"], + textarea { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + } + input[type="text"]:hover, + textarea:hover { + --tw-border-opacity: 1; + border-color: rgb(100 116 139 / var(--tw-border-opacity)); + } + input[type="text"]:focus-visible, + textarea:focus-visible { + --tw-border-opacity: 1; + border-color: rgb(100 116 139 / var(--tw-border-opacity)); + } + input[type="submit"] { + margin-left: auto; + } + input[type="submit"] { + cursor: pointer; + } + input[type="submit"] { + border-radius: 0.5rem; + } + input[type="submit"] { + border-width: 1px; + } + input[type="submit"] { + --tw-border-opacity: 1; + border-color: rgb(51 65 85 / var(--tw-border-opacity)); + } + input[type="submit"] { + padding-left: 0.75rem; + padding-right: 0.75rem; + } + input[type="submit"] { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + } + input[type="submit"]:hover { + --tw-border-opacity: 1; + border-color: rgb(100 116 139 / var(--tw-border-opacity)); + } + input[type="submit"]:focus-visible { + --tw-border-opacity: 1; + border-color: rgb(100 116 139 / var(--tw-border-opacity)); + } +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..f33e954 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,34 @@ +/** @type {import("tailwindcss/colors")} */ +const colors = require("tailwindcss/colors"); + +/** @type {import("tailwindcss").Config} */ +module.exports = { + content: ["./src/app/pages/**/*.rs"], + theme: { + fontFamily: { + sans: ["sans-serif"], + mono: ["monospace"], + }, + extend: { + colors: { + fg: { + base: colors.slate[300], + deemphasized: colors.slate[400], + headings: colors.white, + }, + bg: { + base: colors.slate[900], + raised: { + // backgrounds + 1: colors.slate[800], + 2: colors.slate[700], + }, + }, + bd: { + base: colors.slate[700], + highlighted: colors.slate[500], + }, + }, + }, + }, +};