add: full initial

This commit is contained in:
trisua 2025-08-21 00:30:58 -04:00
parent f5c663495d
commit d06bc5e726
29 changed files with 592 additions and 1928 deletions

2
.gitignore vendored
View file

@ -1 +1,3 @@
target/ target/
app.toml
/buckets

647
Cargo.lock generated
View file

@ -146,29 +146,6 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "axum-extra"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d"
dependencies = [
"axum",
"axum-core",
"bytes",
"cookie",
"futures-util",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"serde",
"tower",
"tower-layer",
"tower-service",
]
[[package]] [[package]]
name = "axum-macros" name = "axum-macros"
version = "0.5.0" version = "0.5.0"
@ -257,13 +234,31 @@ dependencies = [
] ]
[[package]] [[package]]
name = "bstr" name = "buckets"
version = "1.12.0" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [ dependencies = [
"memchr", "axum",
"buckets-core",
"dotenv",
"pathbufd",
"tetratto-core",
"tokio",
"tower-http",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "buckets-core"
version = "1.0.1"
dependencies = [
"oiseau",
"pathbufd",
"serde", "serde",
"serde_json",
"tetratto-core",
"tetratto-shared",
"toml",
] ]
[[package]] [[package]]
@ -325,28 +320,6 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "chrono-tz"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb"
dependencies = [
"chrono",
"chrono-tz-build",
"phf 0.11.3",
]
[[package]]
name = "chrono-tz-build"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1"
dependencies = [
"parse-zoneinfo",
"phf 0.11.3",
"phf_codegen",
]
[[package]] [[package]]
name = "combine" name = "combine"
version = "4.6.7" version = "4.6.7"
@ -367,17 +340,6 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@ -412,31 +374,6 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.6" version = "0.1.6"
@ -476,21 +413,6 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
name = "deranged"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
dependencies = [
"powerfmt",
]
[[package]]
name = "deunicode"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04"
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -534,12 +456,6 @@ dependencies = [
"dtoa", "dtoa",
] ]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]] [[package]]
name = "emojis" name = "emojis"
version = "0.7.2" version = "0.7.2"
@ -571,7 +487,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -760,41 +676,11 @@ version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "glob"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "globset"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5"
dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "globwalk"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
dependencies = [
"bitflags 2.9.2",
"ignore",
"walkdir",
]
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.4.11" version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
dependencies = [ dependencies = [
"atomic-waker", "atomic-waker",
"bytes", "bytes",
@ -893,15 +779,6 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "humansize"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
dependencies = [
"libm",
]
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.6.0" version = "1.6.0"
@ -1112,22 +989,6 @@ dependencies = [
"icu_properties", "icu_properties",
] ]
[[package]]
name = "ignore"
version = "0.4.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b"
dependencies = [
"crossbeam-deque",
"globset",
"log",
"memchr",
"regex-automata 0.4.9",
"same-file",
"walkdir",
"winapi-util",
]
[[package]] [[package]]
name = "image" name = "image"
version = "0.25.6" version = "0.25.6"
@ -1148,7 +1009,6 @@ checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
"serde",
] ]
[[package]] [[package]]
@ -1178,15 +1038,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.15" version = "1.0.15"
@ -1215,12 +1066,6 @@ version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
name = "libm"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.9.4" version = "0.9.4"
@ -1268,31 +1113,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "malachite"
version = "1.0.0"
dependencies = [
"axum",
"axum-extra",
"dotenv",
"glob",
"nanoneo",
"oiseau",
"pathbufd",
"regex",
"serde",
"serde_json",
"serde_valid",
"tera",
"tetratto-core",
"tetratto-shared",
"tokio",
"toml 0.9.5",
"tower-http",
"tracing",
"tracing-subscriber",
]
[[package]] [[package]]
name = "maplit" name = "maplit"
version = "1.0.2" version = "1.0.2"
@ -1389,12 +1209,6 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "nanoneo"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1495d19c5bed5372c613d7b4a38e8093b357f4405ce38ba1de2d6586e5c892"
[[package]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.14" version = "0.2.14"
@ -1438,12 +1252,6 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.46" version = "0.1.46"
@ -1562,15 +1370,6 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "parse-zoneinfo"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
dependencies = [
"regex",
]
[[package]] [[package]]
name = "paste" name = "paste"
version = "1.0.15" version = "1.0.15"
@ -1592,50 +1391,6 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pest"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
dependencies = [
"memchr",
"thiserror 2.0.12",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pest_meta"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5"
dependencies = [
"pest",
"sha2",
]
[[package]] [[package]]
name = "phf" name = "phf"
version = "0.11.3" version = "0.11.3"
@ -1775,12 +1530,6 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.21" version = "0.2.21"
@ -1796,27 +1545,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.95" version = "1.0.95"
@ -1883,8 +1611,6 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [ dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4", "rand_core 0.6.4",
] ]
@ -1894,20 +1620,10 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [ dependencies = [
"rand_chacha 0.9.0", "rand_chacha",
"rand_core 0.9.3", "rand_core 0.9.3",
] ]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
]
[[package]] [[package]]
name = "rand_chacha" name = "rand_chacha"
version = "0.9.0" version = "0.9.0"
@ -1923,9 +1639,6 @@ name = "rand_core"
version = "0.6.4" version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.16",
]
[[package]] [[package]]
name = "rand_core" name = "rand_core"
@ -2083,14 +1796,14 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.29" version = "0.23.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"rustls-pki-types", "rustls-pki-types",
@ -2131,15 +1844,6 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "schannel" name = "schannel"
version = "0.1.27" version = "0.1.27"
@ -2226,15 +1930,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "1.0.0" version = "1.0.0"
@ -2256,52 +1951,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_valid"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b615bed66931a7a9809b273937adc8a402d038b1e509d027fcaf62f084d33d1"
dependencies = [
"indexmap",
"itertools",
"num-traits",
"once_cell",
"paste",
"regex",
"serde",
"serde_json",
"serde_valid_derive",
"serde_valid_literal",
"thiserror 1.0.69",
"toml 0.8.23",
"unicode-segmentation",
]
[[package]]
name = "serde_valid_derive"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fa1a5a21ea5aab06d2e6a6b59837d450fb2be9695be97735a711edfbe79ea07"
dependencies = [
"itertools",
"paste",
"proc-macro-error2",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "serde_valid_literal"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd07331596ea967dccf9a35bde71ecd757490e09827b938a5c6226c648e3a25e"
dependencies = [
"paste",
"regex",
]
[[package]] [[package]]
name = "sha1" name = "sha1"
version = "0.10.6" version = "0.10.6"
@ -2363,16 +2012,6 @@ version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
[[package]]
name = "slug"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724"
dependencies = [
"deunicode",
"wasm-bindgen",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.15.1" version = "1.15.1"
@ -2450,12 +2089,6 @@ dependencies = [
"unicode-properties", "unicode-properties",
] ]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"
@ -2516,15 +2149,15 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.20.0" version = "3.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom 0.3.3", "getrandom 0.3.3",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -2538,33 +2171,11 @@ dependencies = [
"utf-8", "utf-8",
] ]
[[package]]
name = "tera"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee"
dependencies = [
"chrono",
"chrono-tz",
"globwalk",
"humansize",
"lazy_static",
"percent-encoding",
"pest",
"pest_derive",
"rand 0.8.5",
"regex",
"serde",
"serde_json",
"slug",
"unic-segment",
]
[[package]] [[package]]
name = "tetratto-core" name = "tetratto-core"
version = "15.0.1" version = "15.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7aeb9dcc5631ec6188bb9438dc97015c6662b6f59e650e5afa865775f170c9c" checksum = "605c03fac71468f57f9c47d9246300640f3f65ec9f19fb86799e10f632d3ea68"
dependencies = [ dependencies = [
"async-recursion", "async-recursion",
"base16ct", "base16ct",
@ -2582,7 +2193,7 @@ dependencies = [
"tetratto-l10n", "tetratto-l10n",
"tetratto-shared", "tetratto-shared",
"tokio", "tokio",
"toml 0.9.5", "toml",
"totp-rs", "totp-rs",
] ]
@ -2594,7 +2205,7 @@ checksum = "d96f5e41633c757e3519efb47c9b85d00d14322c1961360e126d0ecc0ea79b86"
dependencies = [ dependencies = [
"pathbufd", "pathbufd",
"serde", "serde",
"toml 0.9.5", "toml",
] ]
[[package]] [[package]]
@ -2615,33 +2226,13 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.12" version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [ dependencies = [
"thiserror-impl 2.0.12", "thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
] ]
[[package]] [[package]]
@ -2664,37 +2255,6 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "time"
version = "0.3.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
[[package]]
name = "time-macros"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.1" version = "0.8.1"
@ -2821,18 +2381,6 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned 0.6.9",
"toml_datetime 0.6.11",
"toml_edit",
]
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.9.5" version = "0.9.5"
@ -2841,22 +2389,13 @@ checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
"serde_spanned 1.0.0", "serde_spanned",
"toml_datetime 0.7.0", "toml_datetime",
"toml_parser", "toml_parser",
"toml_writer", "toml_writer",
"winnow", "winnow",
] ]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.7.0" version = "0.7.0"
@ -2866,20 +2405,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned 0.6.9",
"toml_datetime 0.6.11",
"toml_write",
"winnow",
]
[[package]] [[package]]
name = "toml_parser" name = "toml_parser"
version = "1.0.2" version = "1.0.2"
@ -2889,12 +2414,6 @@ dependencies = [
"winnow", "winnow",
] ]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]] [[package]]
name = "toml_writer" name = "toml_writer"
version = "1.0.2" version = "1.0.2"
@ -3055,7 +2574,7 @@ dependencies = [
"log", "log",
"rand 0.9.1", "rand 0.9.1",
"sha1", "sha1",
"thiserror 2.0.12", "thiserror",
"utf-8", "utf-8",
] ]
@ -3065,62 +2584,6 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]]
name = "ucd-trie"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "unic-char-property"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
dependencies = [
"unic-char-range",
]
[[package]]
name = "unic-char-range"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
[[package]]
name = "unic-common"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
[[package]]
name = "unic-segment"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23"
dependencies = [
"unic-ucd-segment",
]
[[package]]
name = "unic-ucd-segment"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700"
dependencies = [
"unic-char-property",
"unic-char-range",
"unic-ucd-version",
]
[[package]]
name = "unic-ucd-version"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
dependencies = [
"unic-common",
]
[[package]] [[package]]
name = "unicase" name = "unicase"
version = "2.8.1" version = "2.8.1"
@ -3154,12 +2617,6 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.2.1" version = "0.2.1"
@ -3230,16 +2687,6 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[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]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"
@ -3390,15 +2837,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"
@ -3628,9 +3066,6 @@ name = "winnow"
version = "0.7.12" version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "wit-bindgen-rt" name = "wit-bindgen-rt"

View file

@ -1,34 +1,19 @@
[package] [workspace]
name = "malachite" resolver = "2"
version = "1.0.0" members = ["crates/buckets", "crates/buckets-core"]
edition = "2024" package.authors = ["trisuaso"]
authors = ["trisuaso"] package.repository = "https://trisua.com/t/buckets"
repository = "https://trisua.com/t/malachite" package.license = "AGPL-3.0-or-later"
license = "AGPL-3.0-or-later" package.homepage = "https://tetratto.com"
homepage = "https://trisua.com"
[dependencies] [profile.dev]
tetratto-core = "15.0.1" incremental = true
tetratto-shared = "12.0.6"
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] } [profile.release]
pathbufd = "0.1.4" opt-level = 3
serde = { version = "1.0.219", features = ["derive"] } lto = true
tera = "1.20.0" codegen-units = 2
tracing = "0.1.41" # panic = "abort"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } panic = "unwind"
tower-http = { version = "0.6.6", features = [ strip = true
"trace", incremental = true
"fs",
"catch-panic",
"set-header",
] }
axum = { version = "0.8.4", features = ["macros", "ws"] }
axum-extra = { version = "0.10.1", features = ["cookie"] }
nanoneo = "0.2.0"
dotenv = "0.15.0"
glob = "0.3.2"
serde_json = "1.0.142"
toml = "0.9.4"
serde_valid = { version = "1.0.5", features = ["toml"] }
regex = "1.11.1"
oiseau = { version = "0.1.2", default-features = false, features = ["postgres", "redis",] }

View file

@ -1,3 +1,5 @@
# 🪨 malachite # 🪣 buckets
simple template for building backends with a structure similar to how the [tetratto](https://trisua.com/t/tetratto) repository is organized object-like storage http server
it's basically like regular file storage, but it connects to your database to store file metadata, buckets, ids, etc. (meaning files are stored in a jumbled mess on the file system, but are controlled by entries in the database!)

7
app/.gitignore vendored
View file

@ -1,7 +0,0 @@
docs/**/*
!docs/example.md
icons/
templates_build/
public/favicon.svg
.env
app.toml

View file

@ -1 +0,0 @@
# hi :)

View file

@ -1,208 +0,0 @@
// theme preference
function media_theme_pref() {
document.documentElement.removeAttribute("class");
if (
window.matchMedia("(prefers-color-scheme: dark)").matches &&
(!window.localStorage.getItem("malachite.app:theme") ||
window.localStorage.getItem("malachite.app:theme") === "Auto")
) {
document.documentElement.classList.add("dark");
document.getElementById("switch_light").classList.add("hidden");
document.getElementById("switch_dark").classList.remove("hidden");
} else if (
window.matchMedia("(prefers-color-scheme: light)").matches &&
(!window.localStorage.getItem("malachite.app:theme") ||
window.localStorage.getItem("malachite.app:theme") === "Auto")
) {
document.documentElement.classList.remove("dark");
document.getElementById("switch_light").classList.remove("hidden");
document.getElementById("switch_dark").classList.add("hidden");
} else if (window.localStorage.getItem("malachite.app:theme")) {
/* restore theme */
const current = window.localStorage.getItem("malachite.app:theme");
document.documentElement.className = current.toLowerCase();
if (current === "Light") {
document.getElementById("switch_light").classList.remove("hidden");
document.getElementById("switch_dark").classList.add("hidden");
} else {
document.getElementById("switch_light").classList.add("hidden");
document.getElementById("switch_dark").classList.remove("hidden");
}
}
}
globalThis.temporary_set_theme = (theme) => {
document.documentElement.className = theme.toLowerCase();
if (theme === "Light") {
document.getElementById("switch_light").classList.remove("hidden");
document.getElementById("switch_dark").classList.add("hidden");
} else {
document.getElementById("switch_light").classList.add("hidden");
document.getElementById("switch_dark").classList.remove("hidden");
}
};
globalThis.set_theme = (theme) => {
window.localStorage.setItem("malachite.app:theme", theme);
document.documentElement.className = theme;
media_theme_pref();
};
media_theme_pref();
// messages
function get_cookie(key) {
return (document.cookie.split(`${key}=`)[1] || "").split(";")[0];
}
function check_message() {
const element = document.getElementById("messages");
const message = get_cookie("App-Message");
const message_good = get_cookie("App-Message-Good") === "true";
if (message) {
element.style.marginBottom = "1rem";
element.style.paddingLeft = "1rem";
element.innerHTML = `<li class="${message_good ? "green" : "red"}">${message.replaceAll('"', "")}</li>`;
}
// clear cookies
for (cookie of document.cookie.split(";")) {
// biome-ignore lint/suspicious/noDocumentCookie: cookie store is barely supported
document.cookie = `${cookie.split("=")[0]}=; expires=${new Date(0).toUTCString()}; path=/`;
}
}
globalThis.show_message = (message, message_good = true) => {
const element = document.getElementById("messages");
element.style.marginBottom = "1rem";
element.style.paddingLeft = "1rem";
element.innerHTML = `<li class="${message_good ? "green" : "red"}">${message.replaceAll('"', "")}</li>`;
};
check_message();
// components
function close_dropdowns() {
for (const dropdown of Array.from(
document.querySelectorAll(".inner.open"),
)) {
dropdown.classList.remove("open");
}
}
globalThis.open_dropdown = (event) => {
event.stopImmediatePropagation();
let target = event.target;
while (!target.matches(".dropdown")) {
target = target.parentElement;
}
// close all others
close_dropdowns();
// open
setTimeout(() => {
for (const dropdown of Array.from(target.querySelectorAll(".inner"))) {
// check y
const box = target.getBoundingClientRect();
let parent = dropdown.parentElement;
while (!parent.matches("html, .window")) {
parent = parent.parentElement;
}
let parent_height = parent.getBoundingClientRect().y;
if (parent.nodeName === "HTML") {
parent_height = window.screen.height;
}
const scroll = window.scrollY;
const height = parent_height;
const y = box.y + scroll;
if (y > height - scroll - 375) {
dropdown.classList.add("top");
} else {
dropdown.classList.remove("top");
}
// open
dropdown.classList.add("open");
if (dropdown.classList.contains("open")) {
dropdown.removeAttribute("aria-hidden");
} else {
dropdown.setAttribute("aria-hidden", "true");
}
}
}, 5);
};
globalThis.init_dropdowns = (bind_to) => {
for (const dropdown of Array.from(document.querySelectorAll(".inner"))) {
dropdown.setAttribute("aria-hidden", "true");
}
bind_to.addEventListener("click", (event) => {
if (
event.target.matches(".dropdown") ||
event.target.matches("[exclude=dropdown]")
) {
return;
}
for (const dropdown of Array.from(
document.querySelectorAll(".inner.open"),
)) {
dropdown.classList.remove("open");
}
});
};
globalThis.hash_check = (hash) => {
if (hash.startsWith("#/")) {
for (const x of Array.from(document.querySelectorAll(".subpage"))) {
x.classList.add("hidden");
}
document.getElementById(hash).classList.remove("hidden");
}
};
window.addEventListener("hashchange", (_) => hash_check(window.location.hash));
setTimeout(() => {
// run initial hash check
hash_check(window.location.hash);
}, 150);
globalThis.submitter_load = (submitter) => {
return {
load() {
submitter.querySelector("[ui_ident=text]").classList.add("hidden");
submitter
.querySelector("[ui_ident=loader]")
.classList.remove("hidden");
submitter.setAttribute("disabled", "true");
},
failed() {
submitter
.querySelector("[ui_ident=text]")
.classList.remove("hidden");
submitter
.querySelector("[ui_ident=loader]")
.classList.add("hidden");
submitter.removeAttribute("disabled");
},
};
};

View file

@ -1 +0,0 @@
../../target/doc

View file

@ -1,705 +0,0 @@
:root {
color-scheme: light dark;
--color-super-lowered: oklch(87.1% 0.006 286.286);
--color-lowered: oklch(96.7% 0.001 286.375);
--color-surface: oklch(92.9% 0.013 255.508);
--color-raised: oklch(98.4% 0.003 247.858);
--color-super-raised: oklch(96.8% 0.007 247.896);
--color-text: hsl(0, 0%, 5%);
--color-link: #2949b2;
--color-shadow: rgba(0, 0, 0, 0.08);
--color-red: hsl(0, 84%, 40%);
--color-green: hsl(100, 84%, 20%);
--color-yellow: oklch(47% 0.157 37.304);
--color-purple: hsl(284, 84%, 20%);
--color-green-lowered: hsl(100, 84%, 15%);
--color-red-lowered: hsl(0, 84%, 35%);
--shadow-x-offset: 0;
--shadow-y-offset: 0.125rem;
--shadow-size: var(--pad-1);
--pad-1: 0.2rem;
--pad-2: 0.35rem;
--pad-3: 0.5rem;
--pad-4: 1rem;
--radius: 0.2rem;
--nav-height: 36px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-size: 16px;
}
.dark,
.dark * {
--color-super-lowered: var(--color-super-raised);
--color-lowered: var(--color-raised);
--color-surface: oklch(21% 0.006 285.885);
--color-raised: oklch(27.4% 0.006 286.033);
--color-super-raised: oklch(37% 0.013 285.805);
--color-text: hsl(0, 0%, 95%);
--color-link: #93c5fd;
--color-red: hsl(0, 94%, 82%);
--color-green: hsl(100, 94%, 82%);
--color-yellow: oklch(90.1% 0.076 70.697);
--color-purple: hsl(284, 94%, 82%);
}
html,
body {
line-height: 1.5;
letter-spacing: 0.15px;
font-family:
"Inter",
"Poppins",
"Roboto",
ui-sans-serif,
-apple-system,
BlinkMacSystemFont,
system-ui,
sans-serif,
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol",
"Noto Color Emoji";
color: var(--color-text);
background: var(--color-surface);
overflow: auto auto;
height: 100dvh;
scroll-behavior: smooth;
overflow-x: hidden;
}
main {
width: 80ch;
margin: var(--pad-4) auto;
padding: var(--pad-3) var(--pad-4);
}
article {
margin: var(--pad-2) 0;
height: calc(100dvh - var(--pad-4) - var(--nav-height) * 2);
}
.tab {
flex: 1 0 auto;
overflow: auto;
}
.tabs .tab {
height: 100%;
}
.fadein {
animation: fadein ease-in-out 1 0.5s forwards running;
}
nav {
height: var(--nav-height);
}
nav.sticky {
background: var(--color-raised);
position: sticky;
z-index: 2;
top: 0;
}
@media screen and (max-width: 900px) {
main,
article,
nav,
header,
footer {
width: 100%;
}
article {
margin-top: 0;
}
main {
padding: 0;
}
.flex_collapse_rev {
flex-direction: column-reverse !important;
}
}
.container {
margin: 10px auto 0;
width: 100%;
}
.content_container {
margin: 0 auto var(--pad-2);
width: 100%;
}
@media screen and (min-width: 500px) {
.content_container {
max-width: 540px;
}
}
@media (min-width: 768px) {
.content_container {
max-width: 720px;
}
}
@media (min-width: 900px) {
.content_container {
max-width: 960px;
}
@media (min-width: 1200px) {
article {
padding: 0;
}
.content_container {
max-width: 1100px;
}
}
}
video {
max-width: 100%;
border-radius: var(--radius);
}
/* card */
.card {
padding: var(--pad-4);
background: var(--color-raised);
color: var(--color-text);
}
.card_nest .card:nth-child(1) {
background: var(--color-super-raised);
padding: var(--pad-2) var(--pad-4);
}
/* button */
.button {
--h: 36px;
display: flex;
justify-content: center;
align-items: center;
gap: var(--pad-2);
padding: var(--pad-2) calc(var(--pad-3) * 1.5);
cursor: pointer;
background: var(--color-raised);
color: var(--color-text);
outline: none;
border: none;
width: max-content;
height: var(--h);
line-height: var(--h);
transition: background 0.15s;
text-decoration: none !important;
user-select: none;
appearance: none;
}
.button:disabled {
opacity: 50%;
cursor: not-allowed;
}
.button.small {
--h: 28px;
}
.button:not(:has(.button:hover)):not(.camo):hover {
background: var(--color-super-raised);
}
.button.camo {
background: transparent;
color: inherit;
}
.bar .button:not(.simple).camo:hover {
color: var(--color-link);
}
.button.simple {
--size: 18px;
font-weight: 600;
padding: var(--pad-2) !important;
border-radius: var(--radius);
width: var(--size);
height: var(--size);
aspect-ratio: 1 / 1;
font-size: 12px;
}
.button.surface {
background: var(--color-surface);
}
.button.surface.simple:is(.camo *) {
background: var(--color-super-raised);
}
.button.green:not(.dark *) {
background: var(--color-green);
color: white !important;
&:hover {
background: var(--color-green-lowered) !important;
}
}
.button.red:not(.dark *) {
background: var(--color-red);
color: white !important;
&:hover {
background: var(--color-red-lowered) !important;
}
}
/* dropdown */
.dropdown {
position: relative;
}
.dropdown .inner {
display: none;
flex-direction: column;
box-shadow: var(--shadow-x-offset) var(--shadow-y-offset) var(--shadow-size)
var(--color-shadow);
background: var(--color-raised);
color: inherit;
position: absolute;
z-index: 2;
top: 100%;
right: 0;
width: max-content;
max-width: 15rem;
}
.dropdown .inner.left {
right: unset;
left: 0;
}
.dropdown .inner.open {
display: flex;
}
.dropdown .inner .button,
.dropdown .inner .title {
padding: var(--pad-3) var(--pad-4);
justify-content: flex-start;
width: 100%;
}
.dropdown .inner .title {
font-weight: 600;
font-size: 14px;
}
.dropdown:has(.inner.open) .button:nth-child(1):not(.inner *) {
background: var(--color-raised);
}
.dropdown .inner.top {
top: unset;
bottom: 100%;
}
.dropdown .inner.left {
left: 0;
right: unset;
}
/* input */
input {
--h: 36px;
padding: var(--pad-2) calc(var(--pad-3) * 1.5);
background: var(--color-raised);
color: var(--color-text);
outline: none;
border: none;
width: max-content;
transition:
background 0.15s,
border 0.15s;
height: var(--h);
line-height: var(--h);
border-left: solid 0px transparent;
}
input:not([type="checkbox"]):focus {
outline: solid 2px var(--color-primary);
box-shadow: 0 0 0 4px oklch(87% 0.065 274.039 / 25%);
background: var(--color-super-raised);
}
input:user-invalid,
input[data-invalid] {
border-left: inset 5px var(--color-red);
}
input.surface {
background: var(--color-surface);
}
input[type="checkbox"] {
height: max-content;
}
/* typo */
p,
ul,
ol {
margin-bottom: var(--pad-4) !important;
&:last-child {
margin-bottom: 0 !important;
}
}
.post_right:not(.repost) {
max-width: calc(100% - 52px);
}
.rhs {
width: 100% !important;
}
.name {
max-width: 250px;
overflow: hidden;
/* overflow-wrap: break-word; */
overflow-wrap: anywhere;
text-overflow: ellipsis;
}
@media screen and (min-width: 901px) {
.name.shorter {
max-width: 200px;
}
.name.lg\:long {
max-width: unset;
}
.rhs {
width: calc(100% - 23rem) !important;
}
}
ul,
ol {
margin: var(--pad-2) 0 var(--pad-2) var(--pad-4);
}
pre {
padding: var(--pad-2) var(--pad-4);
border-left: solid 5px var(--color-primary);
background: var(--color-surface);
border-radius: var(--radius);
margin-bottom: var(--pad-4);
}
code {
padding: 0;
}
pre,
code {
font-family: "Jetbrains Mono", "Fire Code", monospace;
width: 100%;
max-width: 100%;
overflow: auto;
border-radius: var(--radius);
font-size: 0.8rem !important;
color: inherit;
}
code * {
font-size: 0.8rem !important;
}
code:not(pre *) {
padding: var(--pad-1) var(--pad-2);
background: oklch(98% 0.016 73.684 / 25%);
color: oklch(90.1% 0.076 70.697);
border-radius: var(--radius);
white-space: break-spaces;
}
code:not(pre *):not(.dark *) {
background: oklch(83.7% 0.128 66.29 / 25%);
color: oklch(47% 0.157 37.304);
}
svg.icon {
stroke: currentColor;
fill: currentColor;
width: 18px;
height: 1em;
}
svg.icon.filled {
fill: currentColor;
}
.no_fill svg.icon {
fill: transparent;
}
button svg {
pointer-events: none;
}
hr {
border-top: solid 1px var(--color-super-lowered) !important;
border-left: 0;
border-bottom: 0;
border-right: 0;
}
hr.margin,
.container hr {
margin: var(--pad-4) 0;
}
span.img_sizer {
display: inline-block;
}
p,
li,
span,
code {
max-width: 100%;
overflow-wrap: normal;
text-wrap: stable;
word-wrap: break-word;
}
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.75rem;
}
h3 {
font-size: 1.5rem;
}
h4 {
font-size: 1.25rem;
}
h5 {
font-size: var(--pad-4);
}
h6 {
font-size: var(--pad-3);
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: var(--pad-4) 0;
font-weight: 700;
width: -moz-max-content;
position: relative;
max-width: 100%;
}
h1 {
text-align: center;
margin: 2rem 0;
width: 100%;
}
a {
text-decoration: none;
color: var(--color-link);
}
.color_block a {
color: inherit;
}
a.flush {
color: inherit;
}
a:hover {
text-decoration: underline;
}
img {
display: inline;
max-width: 100%;
vertical-align: middle;
}
.img_sizer img {
width: 100%;
height: 100%;
}
.avatar {
--size: 18px;
width: var(--size);
height: var(--size);
aspect-ratio: 1 / 1;
}
blockquote {
padding-left: 1rem;
border-left: solid 5px var(--color-green);
color: var(--color-green);
opacity: 75%;
}
p,
span {
font-size: inherit;
}
/* extra */
@keyframes fadein {
from {
opacity: 0%;
}
to {
opacity: 100%;
}
}
.loader {
animation: spin linear infinite 2s forwards running;
display: flex;
justify-content: center;
align-items: center;
}
@keyframes spin {
from {
transform: rotateZ(0deg);
}
to {
transform: rotateZ(360deg);
}
}
.items-end {
align-items: flex-end;
}
/* table */
table {
width: 100%;
table-layout: auto;
margin: var(--pad-4) 0;
border-collapse: separate;
border-spacing: 0;
border: solid 1px var(--color-super-raised);
}
table td,
table th {
padding: var(--pad-2) var(--pad-4);
}
table tr:not(thead *):nth-child(odd) {
background: var(--color-super-raised);
}
table thead th {
text-align: left;
}
/* details */
details {
width: 100%;
margin: var(--pad-4) 0;
}
details summary {
background: var(--color-super-raised);
padding: var(--pad-2) var(--pad-4);
cursor: pointer;
}
details .content {
padding: var(--pad-4);
background: var(--color-surface);
}
/* dialog */
dialog {
background: var(--color-surface);
color: var(--color-text);
box-shadow: var(--shadow-x-offset) var(--shadow-y-offset) var(--shadow-size)
var(--color-shadow);
animation: fadein ease-in-out 1 0.25s forwards running;
max-width: 95%;
width: 30rem;
margin: auto;
padding: var(--pad-4);
border: 0;
}
dialog.inner {
display: flex;
flex-direction: column;
gap: var(--pad-2);
}
dialog::backdrop {
background: hsla(0, 0%, 0%, 25%);
backdrop-filter: blur(2px);
}
dialog:is(.dark *)::backdrop {
background: hsla(0, 0%, 100%, 15%);
}
/* menus */
menu {
display: flex;
}
menu .button {
justify-content: flex-start;
width: 100%;
}
menu .button.active {
background: var(--color-super-raised);
}
menu.col {
flex-direction: column;
width: 25rem;
max-width: 100%;
}

View file

@ -1,9 +0,0 @@
(text "{% extends \"root.lisp\" %} {% block head %}")
(title
(text "Error - {{ name }}"))
(link ("rel" "icon") ("href" "/public/favicon.svg"))
(text "{% endblock %} {% block body %}")
(div
("class" "card")
(p (text "{{ error }}")))
(text "{% endblock %}")

View file

@ -1,12 +0,0 @@
(text "{% extends \"root.lisp\" %} {% block head %}")
(title
(text "{{ name }}"))
(meta ("property" "og:title") ("content" "{{ name }}"))
(meta ("property" "twitter:title") ("content" "{{ name }}"))
(link ("rel" "icon") ("href" "/public/favicon.svg"))
(text "{% endblock %} {% block body %}")
(div
("class" "card")
(h1 (text "{{ name }}")))
(text "{% endblock %}")

View file

@ -1,75 +0,0 @@
(text "<!doctype html>")
(html
("lang" "en")
(head
(meta ("charset" "UTF-8"))
(meta ("name" "viewport") ("content" "width=device-width, initial-scale=1.0"))
(meta ("http-equiv" "X-UA-Compatible") ("content" "ie=edge"))
(link ("rel" "stylesheet") ("href" "https://repodelivery.trisua.com/tetratto/crates/app/src/public/css/utility.css"))
(link ("rel" "stylesheet") ("href" "/public/style.css?v={{ build_code }}"))
(style (text ":root { --color-primary: {{ theme_color }}; }"))
(meta ("name" "theme-color") ("content" "{{ theme_color }}"))
(meta ("property" "og:type") ("content" "website"))
(meta ("property" "og:site_name") ("content" "{{ name }}"))
(script ("src" "/public/app.js?v={{ build_code }}") ("defer"))
(text "{% block head %}{% endblock %}"))
(body
; nav
(nav
("class" "flex w_full justify_between gap_2 sticky")
(div
("class" "flex side")
(div
("class" "dropdown")
(button
("onclick" "open_dropdown(event)")
("exclude" "dropdown")
("class" "button camo fade")
(text "{{ icon \"menu\" }}"))
(div
("class" "inner left")
(a
("class" "button")
("href" "/")
(text "home"))
(a
("class" "button")
("href" "https://trisua.com/t/malachite")
(text "source"))
(text "{% block dropdown %}{% endblock %}")))
(a ("class" "button camo") ("href" "/") (b (text "{{ name }}"))))
(div
("class" "side flex")
(text "{% block nav_extras %}{% endblock %}")
; theme switches
(button
("class" "button camo fade")
("id" "switch_light")
("title" "Switch theme")
("onclick" "set_theme('Dark')")
(text "{{ icon \"sun\" }}"))
(button
("class" "button camo fade hidden")
("id" "switch_dark")
("title" "Switch theme")
("onclick" "set_theme('Light')")
(text "{{ icon \"moon\" }}"))))
; page
(article
("class" "content_container flex flex_col")
("id" "page")
(ul ("id" "messages"))
(text "{% block body %}{% endblock %}")
(div ("style" "min-height: 32px")))
(script (text "setTimeout(() => init_dropdowns(document.body), 150);"))))

View file

@ -0,0 +1,19 @@
[package]
name = "buckets-core"
description = "Buckets media upload types"
version = "1.0.1"
edition = "2024"
readme = "../../README.md"
authors.workspace = true
repository.workspace = true
license.workspace = true
homepage.workspace = true
[dependencies]
tetratto-core = "15.0.2"
tetratto-shared = "12.0.6"
pathbufd = "0.1.4"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.142"
toml = "0.9.4"
oiseau = { version = "0.1.2", default-features = false, features = ["postgres", "redis",] }

View file

@ -4,30 +4,16 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Config { pub struct Config {
/// The name of the site. Shown in the UI. /// The directory files are stored in (relative to cwd).
#[serde(default = "default_name")] #[serde(default = "default_directory")]
pub name: String, pub directory: String,
/// The (CSS) theme color of the site. Shown in the UI.
#[serde(default = "default_theme_color")]
pub theme_color: String,
/// Real IP header (for reverse proxy).
#[serde(default = "default_real_ip_header")]
pub real_ip_header: String,
/// Database configuration. /// Database configuration.
#[serde(default = "default_database")] #[serde(default = "default_database")]
pub database: DatabaseConfig, pub database: DatabaseConfig,
} }
fn default_name() -> String { fn default_directory() -> String {
"App".to_string() "buckets".to_string()
}
fn default_theme_color() -> String {
"#6ee7b7".to_string()
}
fn default_real_ip_header() -> String {
"CF-Connecting-IP".to_string()
} }
fn default_database() -> DatabaseConfig { fn default_database() -> DatabaseConfig {
@ -43,9 +29,7 @@ impl Configuration for Config {
impl Default for Config { impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {
name: default_name(), directory: default_directory(),
theme_color: default_theme_color(),
real_ip_header: default_real_ip_header(),
database: default_database(), database: default_database(),
} }
} }

View file

@ -1,11 +1,10 @@
mod sql; mod sql;
mod uploads;
use crate::config::Config; use crate::config::Config;
use oiseau::{execute, postgres::DataManager as OiseauManager, postgres::Result as PgResult}; use oiseau::{execute, postgres::DataManager as OiseauManager, postgres::Result as PgResult};
use tetratto_core::model::{Error, Result}; use tetratto_core::model::{Error, Result};
pub const NAME_REGEX: &str = r"[^\w_\-\.,!]+";
#[derive(Clone)] #[derive(Clone)]
pub struct DataManager(pub OiseauManager<Config>); pub struct DataManager(pub OiseauManager<Config>);
@ -22,7 +21,7 @@ impl DataManager {
Err(e) => return Err(Error::DatabaseConnection(e.to_string())), Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
}; };
// execute!(&conn, sql::CREATE_TABLE_ENTRIES).unwrap(); execute!(&conn, sql::CREATE_TABLE_UPLOADS).unwrap();
Ok(()) Ok(())
} }

View file

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS uploads (
id BIGINT NOT NULL PRIMARY KEY,
created BIGINT NOT NULL,
owner BIGINT NOT NULL,
bucket TEXT NOT NULL,
metadata TEXT NOT NULL
)

View file

@ -0,0 +1 @@
pub const CREATE_TABLE_UPLOADS: &str = include_str!("./create_uploads.sql");

View file

@ -0,0 +1,167 @@
use crate::{
DataManager,
model::{MediaUpload, UploadMetadata},
};
use oiseau::{PostgresRow, cache::Cache, execute, get, params, query_rows};
use tetratto_core::auto_method;
use tetratto_core::model::{Error, Result};
impl DataManager {
/// Get a [`MediaUpload`] from an SQL row.
pub(crate) fn get_upload_from_row(x: &PostgresRow) -> MediaUpload {
MediaUpload {
id: get!(x->0(i64)) as usize,
created: get!(x->1(i64)) as usize,
owner: get!(x->2(i64)) as usize,
bucket: get!(x->3(String)),
metadata: serde_json::from_str(&get!(x->4(String))).unwrap(),
}
}
auto_method!(get_upload_by_id(usize as i64)@get_upload_from_row -> "SELECT * FROM uploads WHERE id = $1" --name="upload" --returns=MediaUpload --cache-key-tmpl="atto.upload:{}");
/// Get all uploads (paginated).
///
/// # Arguments
/// * `batch` - the limit of items in each page
/// * `page` - the page number
pub async fn get_uploads(&self, batch: usize, page: usize) -> Result<Vec<MediaUpload>> {
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_rows!(
&conn,
"SELECT * FROM uploads ORDER BY created DESC LIMIT $1 OFFSET $2",
&[&(batch as i64), &((page * batch) as i64)],
|x| { Self::get_upload_from_row(x) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("upload".to_string()));
}
Ok(res.unwrap())
}
/// Get all uploads by their owner (paginated).
///
/// # Arguments
/// * `owner` - the ID of the owner of the upload
/// * `batch` - the limit of items in each page
/// * `page` - the page number
pub async fn get_uploads_by_owner(
&self,
owner: usize,
batch: usize,
page: usize,
) -> Result<Vec<MediaUpload>> {
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_rows!(
&conn,
"SELECT * FROM uploads WHERE owner = $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
&[&(owner as i64), &(batch as i64), &((page * batch) as i64)],
|x| { Self::get_upload_from_row(x) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("upload".to_string()));
}
Ok(res.unwrap())
}
/// Get all uploads by their owner.
///
/// # Arguments
/// * `owner` - the ID of the owner of the upload
pub async fn get_uploads_by_owner_all(&self, owner: usize) -> Result<Vec<MediaUpload>> {
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_rows!(
&conn,
"SELECT * FROM uploads WHERE owner = $1 ORDER BY created DESC",
&[&(owner as i64)],
|x| { Self::get_upload_from_row(x) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("upload".to_string()));
}
Ok(res.unwrap())
}
/// Create a new upload in the database.
///
/// # Arguments
/// * `data` - a mock [`MediaUpload`] object to insert
pub async fn create_upload(&self, data: MediaUpload) -> Result<MediaUpload> {
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
data.metadata.validate_kv()?;
let res = execute!(
&conn,
"INSERT INTO uploads VALUES ($1, $2, $3, $4, $5)",
params![
&(data.id as i64),
&(data.created as i64),
&(data.owner as i64),
&data.bucket,
&serde_json::to_string(&data.metadata).unwrap().as_str(),
]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
// return
Ok(data)
}
pub async fn delete_upload(&self, id: usize) -> Result<()> {
// if !user.permissions.check(FinePermission::MANAGE_UPLOADS) {
// return Err(Error::NotAllowed);
// }
// delete file
// it's most important that the file gets off the file system first, even
// if there's an issue in the database
//
// the actual file takes up much more space than the database entry.
let upload = self.get_upload_by_id(id).await?;
upload.remove(&self.0.0.directory)?;
// delete from database
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(&conn, "DELETE FROM uploads WHERE id = $1", &[&(id as i64)]);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
self.0.1.remove(format!("atto.upload:{}", id)).await;
// return
Ok(())
}
auto_method!(update_upload_metadata(UploadMetadata) -> "UPDATE uploads SET metadata = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.upload:{}");
}

View file

@ -0,0 +1,7 @@
pub mod model;
mod database;
pub use database::DataManager;
mod config;
pub use config::Config;

View file

@ -0,0 +1,122 @@
use pathbufd::PathBufD;
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fs::{exists, remove_file, write},
};
use tetratto_core::model::{Error, Result};
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
#[derive(Serialize, Deserialize, PartialEq, Eq)]
pub enum MediaType {
#[serde(alias = "image/webp")]
Webp,
#[serde(alias = "image/avif")]
Avif,
#[serde(alias = "image/png")]
Png,
#[serde(alias = "image/jpg")]
Jpg,
#[serde(alias = "image/gif")]
Gif,
#[serde(alias = "image/carpgraph")]
Carpgraph,
}
impl MediaType {
pub fn extension(&self) -> &str {
match self {
Self::Webp => "webp",
Self::Avif => "avif",
Self::Png => "png",
Self::Jpg => "jpg",
Self::Gif => "gif",
Self::Carpgraph => "carpgraph",
}
}
pub fn mime(&self) -> String {
format!("image/{}", self.extension())
}
}
#[derive(Serialize, Deserialize)]
pub struct UploadMetadata {
pub what: MediaType,
#[serde(default)]
pub alt: String,
#[serde(default)]
pub kv: HashMap<String, String>,
}
impl UploadMetadata {
pub fn validate_kv(&self) -> Result<()> {
for x in &self.kv {
if x.0.len() > 32 {
return Err(Error::DataTooLong("key".to_string()));
}
if x.1.len() > 128 {
return Err(Error::DataTooLong("value".to_string()));
}
}
Ok(())
}
}
#[derive(Serialize, Deserialize)]
pub struct MediaUpload {
pub id: usize,
pub created: usize,
pub owner: usize,
pub bucket: String,
pub metadata: UploadMetadata,
}
impl MediaUpload {
/// Create a new [`MediaUpload`].
pub fn new(what: MediaType, owner: usize, bucket: String) -> Self {
Self {
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
created: unix_epoch_timestamp(),
owner,
bucket,
metadata: UploadMetadata {
alt: String::new(),
what,
kv: HashMap::new(),
},
}
}
/// Get the path to the fs file for this upload.
pub fn path(&self, directory: &str) -> PathBufD {
PathBufD::current().extend(&[
directory,
&format!("{}.{}", self.id, self.metadata.what.extension()),
])
}
/// Write to this upload in the file system.
pub fn write(&self, directory: &str, bytes: &[u8]) -> Result<()> {
match write(self.path(directory), bytes) {
Ok(_) => Ok(()),
Err(e) => Err(Error::MiscError(e.to_string())),
}
}
/// Delete this upload in the file system.
pub fn remove(&self, directory: &str) -> Result<()> {
let path = self.path(directory);
if !exists(&path).unwrap() {
return Ok(());
}
match remove_file(path) {
Ok(_) => Ok(()),
Err(e) => Err(Error::MiscError(e.to_string())),
}
}
}

25
crates/buckets/Cargo.toml Normal file
View file

@ -0,0 +1,25 @@
[package]
name = "buckets"
description = "Buckets API service"
version = "1.0.0"
edition = "2024"
authors.workspace = true
repository.workspace = true
license.workspace = true
homepage.workspace = true
[dependencies]
tetratto-core = "15.0.2"
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
pathbufd = "0.1.4"
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tower-http = { version = "0.6.6", features = [
"trace",
"fs",
"catch-panic",
"set-header",
] }
axum = { version = "0.8.4", features = ["macros", "ws"] }
dotenv = "0.15.0"
buckets-core = { path = "../buckets-core" }

View file

@ -0,0 +1,74 @@
mod routes;
use axum::{Extension, Router};
use buckets_core::{Config, DataManager};
use std::{env::var, net::SocketAddr, sync::Arc};
use tokio::sync::RwLock;
use tower_http::{
catch_panic::CatchPanicLayer,
trace::{self, TraceLayer},
};
use tracing::{Level, info};
pub(crate) type State = Arc<RwLock<DataManager>>;
#[macro_export]
macro_rules! create_dir_if_not_exists {
($dir_path:expr) => {
if !std::fs::exists(&$dir_path).unwrap() {
std::fs::create_dir($dir_path).unwrap();
}
};
}
#[tokio::main(flavor = "multi_thread")]
async fn main() {
dotenv::dotenv().ok();
tracing_subscriber::fmt()
.with_target(false)
.compact()
.init();
let port = match var("PORT") {
Ok(port) => port.parse::<u16>().expect("port should be a u16"),
Err(_) => 8020,
};
// ...
let database = DataManager::new(Config::read())
.await
.expect("failed to connect to database");
database.init().await.expect("failed to init database");
create_dir_if_not_exists!(&database.0.0.directory);
// create app
let app = Router::new()
.merge(routes::routes())
.layer(Extension(Arc::new(RwLock::new(database))))
.layer(axum::extract::DefaultBodyLimit::max(
var("BODY_LIMIT")
.unwrap_or("8388608".to_string())
.parse::<usize>()
.unwrap(),
))
.layer(
TraceLayer::new_for_http()
.make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
.on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
)
.layer(CatchPanicLayer::new());
// ...
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port))
.await
.unwrap();
info!("🪣 buckets.");
info!("listening on http://0.0.0.0:{}", port);
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.unwrap();
}

View file

@ -0,0 +1,94 @@
use crate::State;
use axum::{
Extension, Json, Router, body::Body, extract::Path, response::IntoResponse, routing::get,
};
use buckets_core::model::MediaType;
use pathbufd::PathBufD;
use std::{
fs::{File, exists},
io::Read,
};
use tetratto_core::model::{ApiReturn, Error, carp::CarpGraph};
pub fn routes() -> Router {
Router::new()
.route("/{bucket}/{id}", get(get_request))
.route("/{bucket}/{id}/json", get(get_json_request))
}
pub fn read_image(path: PathBufD) -> Vec<u8> {
let mut bytes = Vec::new();
for byte in File::open(path).unwrap().bytes() {
bytes.push(byte.unwrap())
}
bytes
}
// api
pub async fn get_request(
Path((bucket, id)): Path<(String, usize)>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = &(data.read().await);
let upload = match data.get_upload_by_id(id).await {
Ok(u) => u,
Err(e) => {
return Err(Json(e.into()));
}
};
if !upload.bucket.is_empty() && upload.bucket != bucket {
return Err(Json(ApiReturn {
ok: false,
message: Error::MiscError("Bucket mismatch".to_string()).to_string(),
payload: (),
}));
}
// ...
let path = upload.path(&data.0.0.directory);
if !exists(&path).unwrap() {
return Err(Json(Error::GeneralNotFound("file".to_string()).into()));
}
let bytes = read_image(path);
if upload.metadata.what == MediaType::Carpgraph {
// conver to svg and return
return Ok((
[("Content-Type", "image/svg+xml".to_string())],
Body::from(CarpGraph::from_bytes(bytes).to_svg()),
));
}
Ok((
[("Content-Type", upload.metadata.what.mime())],
Body::from(bytes),
))
}
pub async fn get_json_request(
Path((bucket, id)): Path<(String, usize)>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = &(data.read().await);
let upload = match data.get_upload_by_id(id).await {
Ok(u) => u,
Err(e) => return Json(e.into()),
};
if !upload.bucket.is_empty() && upload.bucket != bucket {
return Json(Error::MiscError("Bucket mismatch".to_string()).into());
}
Json(ApiReturn {
ok: true,
message: "Success".to_string(),
payload: Some(upload),
})
}

View file

@ -4,3 +4,6 @@ doc:
build: build:
just doc just doc
cargo build -r cargo build -r
publish-core:
cargo publish --allow-dirty --package buckets-core

View file

@ -1 +0,0 @@
// pub const CREATE_TABLE_ENTRIES: &str = include_str!("./create_entries.sql");

View file

@ -1,136 +0,0 @@
#![doc = include_str!("../README.md")]
mod config;
mod database;
mod markdown;
mod model;
mod routes;
use crate::database::DataManager;
use axum::{Extension, Router};
use config::Config;
use nanoneo::core::element::Render;
use std::{collections::HashMap, env::var, net::SocketAddr, process::exit, sync::Arc};
use tera::{Tera, Value};
use tetratto_core::html;
use tetratto_shared::hash::salt;
use tokio::sync::RwLock;
use tower_http::{
catch_panic::CatchPanicLayer,
trace::{self, TraceLayer},
};
use tracing::{Level, info};
pub(crate) type InnerState = (DataManager, Tera, String);
pub(crate) type State = Arc<RwLock<InnerState>>;
fn render_markdown(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
Ok(markdown::render_markdown(value.as_str().unwrap())
.replace("\\@", "@")
.replace("%5C@", "@")
.into())
}
fn remove_script_tags(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
Ok(value
.as_str()
.unwrap()
.replace("</script>", "&lt;/script&gt;")
.into())
}
#[macro_export]
macro_rules! create_dir_if_not_exists {
($dir_path:expr) => {
if !std::fs::exists(&$dir_path).unwrap() {
std::fs::create_dir($dir_path).unwrap();
}
};
}
#[tokio::main(flavor = "multi_thread")]
async fn main() {
dotenv::dotenv().ok();
tracing_subscriber::fmt()
.with_target(false)
.compact()
.init();
let port = match var("PORT") {
Ok(port) => port.parse::<u16>().expect("port should be a u16"),
Err(_) => 8020,
};
// ...
let database = DataManager::new(Config::read())
.await
.expect("failed to connect to database");
database.init().await.expect("failed to init database");
// build lisp
create_dir_if_not_exists!("./templates_build");
create_dir_if_not_exists!("./icons");
for x in glob::glob("./templates_src/**/*").expect("failed to read pattern") {
match x {
Ok(x) => std::fs::write(
x.to_str()
.unwrap()
.replace("templates_src/", "templates_build/"),
html::pull_icons(
nanoneo::parse(&std::fs::read_to_string(x).expect("failed to read template"))
.render(&mut HashMap::new()),
"./icons",
)
.await,
)
.expect("failed to write template"),
Err(e) => panic!("{e}"),
}
}
// create docs dir
create_dir_if_not_exists!("./docs");
// ...
let mut tera = match Tera::new(&format!("./templates_build/**/*")) {
Ok(t) => t,
Err(e) => {
println!("{e}");
exit(1);
}
};
tera.register_filter("markdown", render_markdown);
tera.register_filter("remove_script_tags", remove_script_tags);
// create app
let app = Router::new()
.merge(routes::routes())
.layer(Extension(Arc::new(RwLock::new((database, tera, salt())))))
.layer(axum::extract::DefaultBodyLimit::max(
var("BODY_LIMIT")
.unwrap_or("8388608".to_string())
.parse::<usize>()
.unwrap(),
))
.layer(
TraceLayer::new_for_http()
.make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
.on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
)
.layer(CatchPanicLayer::new());
// ...
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port))
.await
.unwrap();
info!("🪨 malachite.");
info!("listening on http://0.0.0.0:{}", port);
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.unwrap();
}

View file

@ -1,18 +0,0 @@
use std::collections::HashSet;
pub fn render_markdown(input: &str) -> String {
let html = tetratto_shared::markdown::render_markdown_dirty(input);
let mut allowed_attributes = HashSet::new();
allowed_attributes.insert("id");
allowed_attributes.insert("class");
allowed_attributes.insert("ref");
allowed_attributes.insert("aria-label");
allowed_attributes.insert("lang");
allowed_attributes.insert("title");
allowed_attributes.insert("align");
allowed_attributes.insert("src");
allowed_attributes.insert("style");
tetratto_shared::markdown::clean_html(html, allowed_attributes)
}

View file

@ -1 +0,0 @@
//! Base types matching SQL table structures.

View file

@ -1,88 +0,0 @@
use crate::{State, config::Config};
use axum::{
Extension, Router,
extract::Path,
response::{Html, IntoResponse},
routing::{get, get_service},
};
use pathbufd::PathBufD;
use tera::Context;
use tetratto_core::model::Error;
pub fn routes() -> Router {
Router::new()
.nest_service(
"/public",
get_service(tower_http::services::ServeDir::new("./public")),
)
.fallback(not_found_request)
.route("/docs/{name}", get(view_doc_request))
// pages
.route("/", get(index_request))
// api
// ...
}
fn default_context(config: &Config, build_code: &str) -> Context {
let mut ctx = Context::new();
ctx.insert("name", &config.name);
ctx.insert("theme_color", &config.theme_color);
ctx.insert("build_code", &build_code);
ctx
}
// pages
async fn not_found_request(Extension(data): Extension<State>) -> impl IntoResponse {
let (ref data, ref tera, ref build_code) = *data.read().await;
let mut ctx = default_context(&data.0.0, &build_code);
ctx.insert(
"error",
&Error::GeneralNotFound("page".to_string()).to_string(),
);
return Html(tera.render("error.lisp", &ctx).unwrap());
}
async fn index_request(Extension(data): Extension<State>) -> impl IntoResponse {
let (ref data, ref tera, ref build_code) = *data.read().await;
Html(
tera.render("index.lisp", &default_context(&data.0.0, &build_code))
.unwrap(),
)
}
async fn view_doc_request(
Extension(data): Extension<State>,
Path(name): Path<String>,
) -> impl IntoResponse {
let (ref data, ref tera, ref build_code) = *data.read().await;
let path = PathBufD::current().extend(&["docs", &format!("{name}.md")]);
if !std::fs::exists(&path).unwrap_or(false) {
let mut ctx = default_context(&data.0.0, &build_code);
ctx.insert(
"error",
&Error::GeneralNotFound("entry".to_string()).to_string(),
);
return Html(tera.render("error.lisp", &ctx).unwrap());
}
let text = match std::fs::read_to_string(&path) {
Ok(t) => t,
Err(e) => {
let mut ctx = default_context(&data.0.0, &build_code);
ctx.insert("error", &Error::MiscError(e.to_string()).to_string());
return Html(tera.render("error.lisp", &ctx).unwrap());
}
};
let mut ctx = default_context(&data.0.0, &build_code);
ctx.insert("text", &text);
ctx.insert("file_name", &name);
return Html(tera.render("doc.lisp", &ctx).unwrap());
}
// api
// ...