diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ddfdc4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +blog/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cbd8642 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Dominic Grimm + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/backend/.dockerignore b/backend/.dockerignore index 97c2bc9..889a42e 100644 --- a/backend/.dockerignore +++ b/backend/.dockerignore @@ -6,3 +6,4 @@ Dockerfile vendor/ example/ static/ +LICENSE diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 0d8aa51..bdbdcdf 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -30,7 +30,7 @@ dependencies = [ "actix-service", "actix-utils", "ahash", - "base64", + "base64 0.21.0", "bitflags", "brotli", "bytes", @@ -43,14 +43,14 @@ dependencies = [ "http", "httparse", "httpdate", - "itoa", + "itoa 1.0.5", "language-tags", "local-channel", "mime", "percent-encoding", "pin-project-lite", "rand", - "sha1", + "sha1 0.10.5", "smallvec", "tokio", "tokio-util", @@ -155,7 +155,7 @@ dependencies = [ "futures-core", "futures-util", "http", - "itoa", + "itoa 1.0.5", "language-tags", "log", "mime", @@ -322,6 +322,17 @@ dependencies = [ "toml", ] +[[package]] +name = "async-trait" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -339,13 +350,14 @@ dependencies = [ "askama_actix", "chrono", "clap", + "comrak", "diesel", "env_logger", "envconfig", "fronma", "lazy_static", "log", - "pulldown-cmark", + "r2d2_redis", "scan_dir", "serde", "serde_yaml 0.9.17", @@ -368,12 +380,42 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -491,6 +533,7 @@ dependencies = [ "once_cell", "strsim", "termcolor", + "terminal_size", ] [[package]] @@ -525,6 +568,38 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "comrak" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784836d0812dade01579cc0cc9b1684847044e716fd7aa6bffbc172e42199500" +dependencies = [ + "clap", + "emojis", + "entities", + "memchr", + "once_cell", + "pest", + "pest_derive", + "regex", + "shell-words", + "slug", + "syntect", + "typed-arena", + "unicode_categories", + "xdg", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -633,6 +708,12 @@ dependencies = [ "syn", ] +[[package]] +name = "deunicode" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690" + [[package]] name = "diesel" version = "2.0.3" @@ -643,7 +724,7 @@ dependencies = [ "byteorder", "chrono", "diesel_derives", - "itoa", + "itoa 1.0.5", "pq-sys", "r2d2", ] @@ -670,6 +751,41 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + +[[package]] +name = "emojis" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fe60b864b6544ad211d4053ced474a9b9d2c8d66b77f01d6c6bcfed10c6bf0" +dependencies = [ + "phf", +] + [[package]] name = "encoding_rs" version = "0.8.32" @@ -679,6 +795,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "entities" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" + [[package]] name = "env_logger" version = "0.10.0" @@ -733,6 +855,16 @@ dependencies = [ "libc", ] +[[package]] +name = "fancy-regex" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6b8560a05112eb52f04b00e5d3790c0dd75d9d980eb8a122fb23b92a623ccf" +dependencies = [ + "bit-set", + "regex", +] + [[package]] name = "flate2" version = "1.0.25" @@ -886,7 +1018,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", - "itoa", + "itoa 1.0.5", ] [[package]] @@ -979,6 +1111,12 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + [[package]] name = "itoa" version = "1.0.5" @@ -1021,6 +1159,15 @@ version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +[[package]] +name = "line-wrap" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" +dependencies = [ + "safemem", +] + [[package]] name = "link-cplusplus" version = "1.0.8" @@ -1182,6 +1329,28 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +[[package]] +name = "onig" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" +dependencies = [ + "bitflags", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "os_str_bytes" version = "6.4.1" @@ -1238,6 +1407,68 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +[[package]] +name = "pest" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028accff104c4e513bad663bbcd2ad7cfd5304144404c31ed0a77ac103d00660" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac3922aac69a40733080f53c1ce7f91dcf57e1a5f6c52f421fadec7fbdc4b69" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06646e185566b5961b4058dd107e0a7f56e77c3f484549fb119867773c0f202" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f60b2ba541577e2a0c307c8f39d1439108120eb7903adeb6497fa880c59616" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -1256,6 +1487,20 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +[[package]] +name = "plist" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5329b8f106a176ab0dce4aae5da86bfcb139bb74fb00882859e03745011f3635" +dependencies = [ + "base64 0.13.1", + "indexmap", + "line-wrap", + "quick-xml", + "serde", + "time 0.3.17", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1304,23 +1549,21 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "pulldown-cmark" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63" -dependencies = [ - "bitflags", - "memchr", - "unicase", -] - [[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-xml" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.23" @@ -1341,6 +1584,16 @@ dependencies = [ "scheduled-thread-pool", ] +[[package]] +name = "r2d2_redis" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "182473b876b0b93e353682ec58e207dd1cb4a62278bbe0045fe52b86b74363bb" +dependencies = [ + "r2d2", + "redis", +] + [[package]] name = "rand" version = "0.8.5" @@ -1371,6 +1624,21 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redis" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4f0ceb2ec0dd769483ecd283f6615aa83dcd0be556d5294c6e659caefe7cc54" +dependencies = [ + "async-trait", + "combine", + "dtoa", + "itoa 0.4.8", + "percent-encoding", + "sha1 0.6.1", + "url", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -1380,6 +1648,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + [[package]] name = "regex" version = "1.7.1" @@ -1432,6 +1711,21 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[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 = "scan_dir" version = "0.3.3" @@ -1494,7 +1788,7 @@ version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7434af0dc1cbd59268aa98b4c22c131c0584d2232f6fb166efb993e2832e896a" dependencies = [ - "itoa", + "itoa 1.0.5", "ryu", "serde", ] @@ -1506,7 +1800,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa", + "itoa 1.0.5", "ryu", "serde", ] @@ -1530,12 +1824,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fb06d4b6cdaef0e0c51fa881acb721bed3c924cfaa71d9c94a3b771dfdf6567" dependencies = [ "indexmap", - "itoa", + "itoa 1.0.5", "ryu", "serde", "unsafe-libyaml", ] +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + [[package]] name = "sha1" version = "0.10.5" @@ -1547,6 +1850,29 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -1556,6 +1882,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + [[package]] name = "slab" version = "0.4.7" @@ -1565,6 +1897,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slug" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373" +dependencies = [ + "deunicode", +] + [[package]] name = "smallvec" version = "1.10.0" @@ -1609,6 +1950,30 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syntect" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c454c27d9d7d9a84c7803aaa3c50cd088d2906fe3c6e42da3209aa623576a8" +dependencies = [ + "bincode", + "bitflags", + "fancy-regex", + "flate2", + "fnv", + "lazy_static", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "walkdir", + "yaml-rust", +] + [[package]] name = "termcolor" version = "1.2.0" @@ -1618,6 +1983,36 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb20089a8ba2b69debd491f8d2d023761cbf196e999218c591fa1e7e15a21907" +dependencies = [ + "rustix", + "windows-sys 0.42.0", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tikv-jemalloc-sys" version = "0.5.3+5.3.0-patched" @@ -1655,7 +2050,7 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" dependencies = [ - "itoa", + "itoa 1.0.5", "serde", "time-core", "time-macros", @@ -1753,12 +2148,24 @@ dependencies = [ "once_cell", ] +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + [[package]] name = "typenum" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +[[package]] +name = "ucd-trie" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" + [[package]] name = "unicase" version = "2.6.0" @@ -1795,6 +2202,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "unsafe-libyaml" version = "0.2.5" @@ -1824,6 +2237,17 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -2002,6 +2426,15 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +[[package]] +name = "xdg" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6" +dependencies = [ + "dirs", +] + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 871a94c..9fd6ffd 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -3,6 +3,7 @@ name = "backend" version = "0.1.0" edition = "2021" authors = ["Dominic Grimm "] +publish = false [[bin]] name = "backend" @@ -11,11 +12,11 @@ build = "build.rs" [[bin]] name = "blogctl" -# [profile.release] -# codegen-units = 1 -# lto = "fat" -# strip = true -# panic = "abort" +[profile.release] +codegen-units = 1 +lto = "fat" +strip = true +panic = "abort" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -27,13 +28,14 @@ askama = "0.11.1" askama_actix = "0.13.0" chrono = { version = "0.4.23", features = ["serde"] } clap = { version = "4.1.4", features = ["derive"] } +comrak = { version = "0.16.0", features = ["shortcodes"] } diesel = { version = "2.0.2", features = ["i-implement-a-third-party-backend-and-opt-into-breaking-changes", "postgres", "chrono", "r2d2"] } env_logger = "0.10.0" envconfig = "0.10.0" fronma = "0.1.1" lazy_static = "1.4.0" log = "0.4.17" -pulldown-cmark = { version = "0.9.2", default-features = false, features = ["simd"] } +r2d2_redis = "0.14.0" scan_dir = "0.3.3" serde = { version = "1.0.152", features = ["derive"] } serde_yaml = "0.9.17" diff --git a/backend/Dockerfile b/backend/Dockerfile index 6cff241..75619c8 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -10,6 +10,11 @@ WORKDIR /usr/src/static COPY --from=css /usr/src/scss/dist ./css RUN minify . -r -o . +FROM tdewolff/minify:latest as templates +WORKDIR /usr/src/templates +COPY ./templates . +RUN minify . -r -o . + FROM docker.io/lukemathwalker/cargo-chef:latest-rust-1.67.0 as chef FROM chef as diesel @@ -28,11 +33,16 @@ RUN cargo chef cook --recipe-path recipe.json RUN rm -rf ./src COPY ./build.rs . COPY --from=static /usr/src/static ./static -COPY ./templates ./templates +COPY --from=templates /usr/src/templates ./templates COPY ./src ./src RUN cargo build FROM docker.io/debian:bullseye-slim as runner +LABEL maintainer="Dominic Grimm " \ + org.opencontainers.image.description="Personal blog backend" \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.source="https://git.dergrimm.net/dergrimm/blog" \ + org.opencontainers.image.url="https://git.dergrimm.net/dergrimm/blog" RUN apt update RUN apt install -y libpq5 RUN apt install -y ca-certificates @@ -40,12 +50,10 @@ RUN apt-get clean RUN apt-get autoremove -y RUN rm -rf /var/lib/{apt,dpkg,cache,log}/ WORKDIR /usr/local/bin -ENV RUST_BACKTRACE=full COPY --from=diesel /usr/local/cargo/bin/diesel . WORKDIR /usr/src/backend COPY ./run.sh . RUN chmod +x ./run.sh COPY ./migrations ./migrations COPY --from=builder /usr/src/backend/target/debug/backend /usr/src/backend/target/debug/blogctl ./bin/ -EXPOSE 80 ENTRYPOINT [ "./run.sh" ] diff --git a/backend/scss/styles.scss b/backend/scss/styles.scss index e09b872..6a2541b 100644 --- a/backend/scss/styles.scss +++ b/backend/scss/styles.scss @@ -1,42 +1,88 @@ html, body { - width: 100vw; - min-height: 100vh; -} - -* { margin: 0; padding: 0; + width: 100vw; + min-height: 100vh; + font-family: "JetBrains Mono", monospace; } +// * { +// font-family: "JetBrains Mono", monospace; +// } + +h1, +h2, +h3 { + text-align: center; +} + +h2 { + text-decoration: underline; +} + +h3 { + font-style: italic; +} + +h4, +h5, +h6 { + font-weight: bold; +} + +a { + text-decoration: none; +} + +hr { + border: none; + border-top: medium solid black; +} + +ul.dashed { + list-style-type: none; + + li:before { + content: "-"; + position: absolute; + // margin-left: -20px; + margin-left: -1rem; + } +} + +table.border-rows { + border-collapse: collapse; + + tr:first-child, + tr:not(:last-child) { + border-bottom: thin solid black; + } + + th, + td { + padding: 1em; + } + + th:not(:last-child), + td:not(:last-child) { + // border: thin solid black; + border-right: thin solid black; + } +} + #wrapper { - width: 75%; margin: 0 auto; + overflow: auto; + min-height: 100vh; + display: flex; + flex-direction: column; } #navbar { display: flex; - margin-bottom: 1%; - - ul { - list-style-type: none; - margin: 0; - padding: 0; - overflow: hidden; - } - - li { - float: left; - } - - a { - display: block; - text-align: center; - padding: 1em; - text-decoration: none; - } } #navbar-brand { @@ -48,18 +94,138 @@ body { } } +.navbar-link a, +#navbar-links ul li a { + display: block; + text-align: center; +} + #navbar-links { width: 50%; ul { + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; float: right; + + li { + float: left; + + &:not(:first-of-type) a { + padding-left: 1rem; + } + } } } -#content { - padding: 3%; +#main-wrapper { + flex: 1; + padding: 1.5% 3%; +} + +#footer { + font-size: small; + text-align: center; +} + +#status-code { + text-align: center; +} + +#status-code-message { + font-size: small; +} + +#post-index { + li:not(:last-child) { + margin-bottom: 1rem; + } } .double-border { border: 5px double black; } + +.section-margin { + margin-top: 1%; +} + +.section-light-padding { + padding: 1%; +} + +.breadcrumb { + padding: 1% 0; + + ul { + display: flex; + flex-wrap: wrap; + list-style: none; + margin: 0; + padding: 0; + } + + li:not(:last-child)::after { + display: inline-block; + margin: 0 0.5rem; + content: ">"; + } +} + +.wysiwyg { + hr { + width: 50%; + } + + ul { + @extend ul.dashed; + } + + table { + @extend table.border-rows; + + margin: 0 auto; + } + + p > code, + li > code, + dd > code, + td > code { + word-wrap: break-word; + box-decoration-break: clone; + padding: 0.1rem 0.3rem 0.2rem; + border: thin solid black; + } + + pre { + padding: 0 1rem; + border: thin solid black; + overflow-x: scroll; + } +} + +@media only screen and (max-width: 600px) { + #wrapper { + width: 100%; + } +} + +@media only screen and (min-width: 600px) { + #wrapper { + width: 75%; + } +} + +@media only screen and (min-width: 768px) { +} + +@media only screen and (min-width: 992px) { +} + +@media only screen and (min-width: 1200px) { + #wrapper { + width: 50%; + } +} diff --git a/backend/src/bin/backend.rs b/backend/src/bin/backend.rs index 539d84b..5aa2ac8 100644 --- a/backend/src/bin/backend.rs +++ b/backend/src/bin/backend.rs @@ -1,4 +1,3 @@ -use diesel::QueryDsl; #[cfg(not(target_env = "msvc"))] use tikv_jemallocator::Jemalloc; @@ -6,104 +5,23 @@ use tikv_jemallocator::Jemalloc; #[global_allocator] static GLOBAL: Jemalloc = Jemalloc; -use actix_web::{get, http::StatusCode, middleware, web, App, HttpResponse, HttpServer}; -use actix_web_static_files::ResourceFiles; -use askama_actix::{Template, TemplateToResponse}; -use diesel::prelude::*; - -use backend::*; - -include!(concat!(env!("OUT_DIR"), "/generated.rs")); - -mod filters { - pub fn cmark(s: T) -> ::askama::Result { - let s = s.to_string(); - - let options = pulldown_cmark::Options::empty(); - let parser = pulldown_cmark::Parser::new_ext(&s, options); - - let mut html_output = String::with_capacity(s.len() * 3 / 2); - pulldown_cmark::html::push_html(&mut html_output, parser); - - Ok(html_output) - } -} - -#[derive(Template)] -#[template(path = "status_code.html")] -struct StatusCodeTemplate { - status_code: StatusCode, - message: Option, -} - -#[derive(Template)] -#[template(path = "posts/{slug}.html")] -struct PostBySlugTemplate { - post: db::models::Post, -} - -async fn not_found() -> HttpResponse { - StatusCodeTemplate { - status_code: StatusCode::NOT_FOUND, - message: None, - } - .to_response() -} - -#[get("/posts/{slug}")] -async fn post_by_slug(db_pool: web::Data, path: web::Path) -> HttpResponse { - let slug = path.into_inner(); - - let conn = &mut match db_pool.get() { - Ok(x) => x, - Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)), - }; - - let post = match db::schema::posts::table - .filter(db::schema::posts::slug.eq(&slug)) - .filter(db::schema::posts::active) - .first::(conn) - .optional() - { - Ok(x) => x, - Err(e) => { - dbg!(&e); - return HttpResponse::InternalServerError().body(format!("{:?}", e)); - } - }; - - match post { - Some(x) => PostBySlugTemplate { post: x }.to_response(), - None => { - let mut resp = StatusCodeTemplate { - status_code: StatusCode::NOT_FOUND, - message: None, - } - .to_response(); - *resp.status_mut() = StatusCode::NOT_FOUND; - - resp - } - } -} +use actix_web::{middleware, App, HttpServer}; #[actix_web::main] async fn main() -> std::io::Result<()> { env_logger::init(); - log::info!("Listening to requests at http://0.0.0.0:80"); + log::info!( + "Listening to requests at {}", + backend::config::CONFIG.bind_url + ); HttpServer::new(move || { - let generated = generate(); - App::new() .wrap(middleware::Compress::default()) .wrap(middleware::Logger::default()) - .app_data(web::Data::new(db::pool().unwrap())) - .service(post_by_slug) - .service(ResourceFiles::new("/static", generated)) - .default_service(web::route().to(not_found)) + .configure(backend::web::init) }) - .bind("0.0.0.0:80") + .bind(&backend::config::CONFIG.bind_url) .unwrap() .run() .await diff --git a/backend/src/bin/blogctl.rs b/backend/src/bin/blogctl.rs index 5a7f298..fc78ab3 100644 --- a/backend/src/bin/blogctl.rs +++ b/backend/src/bin/blogctl.rs @@ -9,6 +9,7 @@ use anyhow::{bail, Result}; use chrono::prelude::*; use clap::{Parser, Subcommand}; use diesel::prelude::*; +use r2d2_redis::redis; use scan_dir::ScanDir; use serde::{Deserialize, Serialize}; use std::fs; @@ -27,6 +28,9 @@ struct Cli { enum Commands { #[clap(about = "Imports new posts")] Import, + + #[clap(about = "Clears redis cache")] + Clear, } #[derive(Deserialize, Serialize, Debug)] @@ -50,88 +54,137 @@ struct Post { fn main() -> Result<()> { match Cli::parse().commands { Commands::Import => { - let conn = &mut db::establish_connection()?; + let db_conn = &mut db::establish_connection()?; + let redis_conn = &mut cache::establish_connection()?; - for post in ScanDir::dirs().read("/blog", |iter| { - iter.map(|(entry, _)| { - let path = entry.path().join("post.md"); - let src = fs::read_to_string(&path)?; - let frontmatter = match fronma::parser::parse::(&src) { - Ok(x) => x, - Err(x) => bail!("Error parsing frontmatter: {:?}", x), - }; + let posts = ScanDir::dirs() + .read("/blog", |iter| { + iter.map(|(entry, _)| { + let path = entry.path().join("post.md"); + let src = fs::read_to_string(&path)?; + let frontmatter = match fronma::parser::parse::(&src) { + Ok(x) => x, + Err(x) => bail!("Error parsing frontmatter: {:?}", x), + }; - Ok(Post { - path, - frontmatter: frontmatter.headers, - content: frontmatter.body.to_string(), - }) - }) - .collect::>>() - })?? { - let content = post.content.trim(); - - if let Some(id) = post.frontmatter.id { - diesel::update(db::schema::posts::table) - .filter(db::schema::posts::id.eq(id)) - .set(db::models::UpdatePost { - name: Some(&post.frontmatter.name), - slug: Some(&post.frontmatter.slug), - description: Some(&post.frontmatter.description), - content: Some(content), - published_at: Some(post.frontmatter.published_at), - edited_at: Some(post.frontmatter.edited_at), - active: Some(post.frontmatter.active), + Ok(Post { + path, + frontmatter: frontmatter.headers, + content: frontmatter.body.to_string(), }) - .execute(conn)?; - } else { - let id = if let Some(id) = db::schema::posts::table - .select(db::schema::posts::id) - .filter(db::schema::posts::slug.eq(&post.frontmatter.slug)) - .first::(conn) - .optional()? - { + }) + .collect::>>() + })?? + .into_iter() + .map(|post| -> Result<_> { + let trimmed = PostFrontmatter { + id: post.frontmatter.id, + name: post.frontmatter.name.trim().to_string(), + slug: post.frontmatter.slug.trim().to_string(), + description: post.frontmatter.description.trim().to_string(), + published_at: post.frontmatter.published_at, + edited_at: post.frontmatter.edited_at, + active: post.frontmatter.active, + }; + let content = post.content.trim(); + + if let Some(id) = trimmed.id { diesel::update(db::schema::posts::table) .filter(db::schema::posts::id.eq(id)) .set(db::models::UpdatePost { - name: Some(&post.frontmatter.name), - slug: None, - description: Some(&post.frontmatter.description), + name: Some(&trimmed.name), + slug: Some(&trimmed.slug), + description: Some(&trimmed.description), content: Some(content), - published_at: Some(post.frontmatter.published_at), - edited_at: Some(post.frontmatter.edited_at), - active: Some(post.frontmatter.active), + published_at: Some(trimmed.published_at), + edited_at: Some(trimmed.edited_at), + active: Some(trimmed.active), }) - .execute(conn)?; + .execute(db_conn)?; - id + Ok(id) } else { - diesel::insert_into(db::schema::posts::table) - .values(db::models::NewPost { - name: &post.frontmatter.name, - slug: &post.frontmatter.slug, - description: &post.frontmatter.description, - content: content, - published_at: post.frontmatter.published_at, - edited_at: post.frontmatter.edited_at, - active: post.frontmatter.active, - }) - .returning(db::schema::posts::id) - .get_result::(conn)? - }; + let id = if let Some(id) = db::schema::posts::table + .select(db::schema::posts::id) + .filter(db::schema::posts::slug.eq(&trimmed.slug)) + .first::(db_conn) + .optional()? + { + diesel::update(db::schema::posts::table) + .filter(db::schema::posts::id.eq(id)) + .set(db::models::UpdatePost { + name: Some(&trimmed.name), + slug: None, + description: Some(&trimmed.description), + content: Some(content), + published_at: Some(trimmed.published_at), + edited_at: Some(trimmed.edited_at), + active: Some(trimmed.active), + }) + .execute(db_conn)?; - fs::write( - post.path, - format!( - "---\n{}---\n{}", - serde_yaml::to_string(&PostFrontmatter { - id: Some(id), - ..post.frontmatter - })?, - post.content - ), - )?; - } + id + } else { + diesel::insert_into(db::schema::posts::table) + .values(db::models::NewPost { + name: &trimmed.name, + slug: &trimmed.slug, + description: &trimmed.description, + content: content, + published_at: trimmed.published_at, + edited_at: trimmed.edited_at, + active: trimmed.active, + }) + .returning(db::schema::posts::id) + .get_result::(db_conn)? + }; + + fs::write( + post.path, + format!( + "---\n{}---\n\n{}\n", + serde_yaml::to_string(&PostFrontmatter { + id: Some(id), + ..trimmed + })?, + content + ), + )?; + + Ok(id) + } + }) + .collect::>>()?; + + let ids = db::schema::posts::table + .select(db::schema::posts::id) + .load::(db_conn)?; + + diesel::delete( + db::schema::posts::table + .filter(diesel::dsl::not(db::schema::posts::id.eq_any(posts))), + ) + .execute(db_conn)?; + + for id in ids { + redis::cmd("DEL") + .arg(cache::keys::post_content(id)) + .query::<()>(redis_conn)?; + } + + Ok(()) + } + Commands::Clear => { + let db_conn = &mut db::establish_connection()?; + let redis_conn = &mut cache::establish_connection()?; + + for id in db::schema::posts::table + .select(db::schema::posts::id) + .load::(db_conn)? + { + redis::cmd("DEL") + .arg(cache::keys::post_content(id)) + .query::<()>(redis_conn)?; } Ok(()) diff --git a/backend/src/cache.rs b/backend/src/cache.rs new file mode 100644 index 0000000..5fe7070 --- /dev/null +++ b/backend/src/cache.rs @@ -0,0 +1,28 @@ +use anyhow::Result; +use lazy_static::lazy_static; +use r2d2_redis::{r2d2, redis, RedisConnectionManager}; + +use crate::config; + +pub type RedisPool = r2d2::Pool; +pub type ConnectionPool = r2d2::PooledConnection; + +pub fn establish_connection() -> Result { + Ok(redis::Client::open(config::CONFIG.redis_url.as_str())?.get_connection()?) +} + +pub fn pool() -> Result { + Ok(r2d2::Pool::builder().build(RedisConnectionManager::new( + config::CONFIG.redis_url.as_str(), + )?)?) +} + +lazy_static! { + pub static ref POOL: RedisPool = pool().unwrap(); +} + +pub mod keys { + pub fn post_content(id: i32) -> String { + format!("post_content:{}", id) + } +} diff --git a/backend/src/config.rs b/backend/src/config.rs index 632be35..282d108 100644 --- a/backend/src/config.rs +++ b/backend/src/config.rs @@ -1,11 +1,19 @@ - use envconfig::Envconfig; use lazy_static::lazy_static; #[derive(Envconfig, Debug)] pub struct Config { + #[envconfig(from = "BACKEND_BIND_URL")] + pub bind_url: String, + #[envconfig(from = "BACKEND_DB_URL")] pub db_url: String, + + #[envconfig(from = "BACKEND_REDIS_URL")] + pub redis_url: String, + + #[envconfig(from = "BACKEND_CACHE_POST_CONTENT_TTL")] + pub cache_post_content_ttl: usize, } lazy_static! { diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 7cb3263..5fee41d 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -1,2 +1,5 @@ +pub mod cache; pub mod config; pub mod db; +pub mod markdown; +pub mod web; diff --git a/backend/src/markdown.rs b/backend/src/markdown.rs new file mode 100644 index 0000000..298df01 --- /dev/null +++ b/backend/src/markdown.rs @@ -0,0 +1,28 @@ +pub fn to_html(src: &str) -> String { + let adapter = comrak::plugins::syntect::SyntectAdapter::new("InspiredGitHub"); + let options = comrak::ComrakOptions { + extension: comrak::ComrakExtensionOptions { + strikethrough: true, + tagfilter: true, + table: true, + autolink: true, + tasklist: true, + superscript: true, + header_ids: Some("__blog-content_".to_string()), + footnotes: true, + description_lists: true, + front_matter_delimiter: None, + shortcodes: true, + }, + parse: comrak::ComrakParseOptions { + smart: true, + ..comrak::ComrakParseOptions::default() + }, + ..comrak::ComrakOptions::default() + }; + let mut plugins = comrak::ComrakPlugins::default(); + + plugins.render.codefence_syntax_highlighter = Some(&adapter); + + comrak::markdown_to_html_with_plugins(src, &options, &plugins) +} diff --git a/backend/src/web/mod.rs b/backend/src/web/mod.rs new file mode 100644 index 0000000..0b092f5 --- /dev/null +++ b/backend/src/web/mod.rs @@ -0,0 +1,176 @@ +use actix_web::{get, http, web, HttpResponse}; +use actix_web_static_files::ResourceFiles; +use askama_actix::TemplateToResponse; +use diesel::prelude::*; +use r2d2_redis::redis; +use std::ops::DerefMut; + +use crate::{cache, config, db, markdown}; + +pub mod templates; + +pub mod static_dir { + include!(concat!(env!("OUT_DIR"), "/generated.rs")); +} + +async fn not_found() -> HttpResponse { + let mut resp = templates::StatusCode { + status_code: http::StatusCode::NOT_FOUND, + message: Some("maybe try a correct url?".to_string()), + } + .to_response(); + *resp.status_mut() = http::StatusCode::NOT_FOUND; + + resp +} + +#[get("/posts")] +async fn posts(db_pool: web::Data) -> HttpResponse { + let db_conn = &mut match db_pool.get() { + Ok(x) => x, + Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)), + }; + + let posts = match db::schema::posts::table + .filter(db::schema::posts::active) + .order(db::schema::posts::published_at.desc()) + .load::(db_conn) + { + Ok(x) => x, + Err(e) => { + return HttpResponse::InternalServerError().body(format!("{:?}", e)); + } + }; + + templates::Posts { posts }.to_response() +} + +#[get("/")] +async fn index() -> HttpResponse { + templates::Index.to_response() +} + +#[get("/about")] +async fn about() -> HttpResponse { + templates::About.to_response() +} + +#[get("/posts/{slug}")] +async fn post_by_slug( + db_pool: web::Data, + redis_pool: web::Data, + path: web::Path, +) -> HttpResponse { + let slug = path.into_inner(); + + let db_conn = &mut match db_pool.get() { + Ok(x) => x, + Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)), + }; + let redis_conn = &mut match redis_pool.get() { + Ok(x) => x, + Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)), + }; + + let post_stripped: Option<(i32, String)> = match db::schema::posts::table + .select((db::schema::posts::id, db::schema::posts::name)) + .filter(db::schema::posts::slug.eq(&slug)) + .filter(db::schema::posts::active) + .get_result::<(i32, String)>(db_conn) + .optional() + { + Ok(x) => x, + Err(e) => { + return HttpResponse::InternalServerError().body(format!("{:?}", e)); + } + }; + + match post_stripped { + Some(stripped) => { + let (stripped_id, stripped_name) = stripped; + + let key = cache::keys::post_content(stripped_id); + + match match redis::cmd("GET") + .arg(&key) + .query::>(redis_conn.deref_mut()) + { + Ok(x) => x, + Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)), + } { + Some(s) => templates::PostBySlug { + name: stripped_name, + slug, + content: s, + } + .to_response(), + None => { + let post = match db::schema::posts::table + .filter(db::schema::posts::id.eq(stripped_id)) + .first::(db_conn) + { + Ok(x) => x, + Err(e) => { + return HttpResponse::InternalServerError().body(format!("{:?}", e)); + } + }; + + let html = markdown::to_html(&post.content); + + match redis::cmd("SET") + .arg(&key) + .arg(&html) + .query::>(redis_conn.deref_mut()) + { + Ok(x) => x, + Err(e) => { + return HttpResponse::InternalServerError().body(format!("{:?}", e)) + } + }; + if let Err(e) = redis::cmd("EXPIRE") + .arg(key) + .arg(config::CONFIG.cache_post_content_ttl) + .query::<()>(redis_conn.deref_mut()) + { + return HttpResponse::InternalServerError().body(format!("{:?}", e)); + } + + templates::PostBySlug { + name: post.name, + slug: post.slug, + content: html, + } + .to_response() + } + } + } + None => { + let mut resp = templates::StatusCode { + status_code: http::StatusCode::NOT_FOUND, + message: Some("this post does not exists... yet".to_string()), + } + .to_response(); + *resp.status_mut() = http::StatusCode::NOT_FOUND; + + resp + } + } +} + +fn setup_routes(cfg: &mut web::ServiceConfig) { + let generated = static_dir::generate(); + + cfg.service(index) + .service(about) + .service(posts) + .service(ResourceFiles::new("/static", generated)) + .service(post_by_slug) + .default_service(web::route().to(not_found)); +} + +pub fn init(cfg: &mut web::ServiceConfig) { + cfg.app_data(web::Data::new(db::pool().unwrap())) + .app_data(web::Data::new(cache::pool().unwrap())); + + setup_routes(cfg); +} diff --git a/backend/src/web/templates.rs b/backend/src/web/templates.rs new file mode 100644 index 0000000..2abe2c2 --- /dev/null +++ b/backend/src/web/templates.rs @@ -0,0 +1,33 @@ +use actix_web::http; +use askama_actix::Template; + +use crate::db; + +#[derive(Template)] +#[template(path = "status_code.html")] +pub struct StatusCode { + pub status_code: http::StatusCode, + pub message: Option, +} + +#[derive(Template)] +#[template(path = "web/index.html")] +pub struct Index; + +#[derive(Template)] +#[template(path = "web/about.html")] +pub struct About; + +#[derive(Template)] +#[template(path = "web/posts/index.html")] +pub struct Posts { + pub posts: Vec, +} + +#[derive(Template)] +#[template(path = "web/posts/{slug}.html")] +pub struct PostBySlug { + pub name: String, + pub slug: String, + pub content: String, +} diff --git a/backend/templates/base.html b/backend/templates/base.html index c238438..30fb7cf 100644 --- a/backend/templates/base.html +++ b/backend/templates/base.html @@ -7,7 +7,7 @@ {% block title %}{% endblock %} | dergrimm's blog - + @@ -15,19 +15,39 @@
- +
+ +
-
- {% block content %}{% endblock %} +
+ + +
+ +
+ {% block content %}{% endblock %} +
+ +
diff --git a/backend/templates/posts/{slug}.html b/backend/templates/posts/{slug}.html deleted file mode 100644 index e43530a..0000000 --- a/backend/templates/posts/{slug}.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "base.html" %} - -{% block title %}{{ post.name }}{% endblock %} - -{% block head %}{% endblock %} - -{% block content %} -
- {{ post.content|cmark|safe }} -
-{% endblock %} diff --git a/backend/templates/status_code.html b/backend/templates/status_code.html index d208f86..9d81d00 100644 --- a/backend/templates/status_code.html +++ b/backend/templates/status_code.html @@ -1,14 +1,19 @@ {% extends "base.html" %} -{% block title %}{{ status_code }}{% endblock %} +{% block title %}{{ status_code|lower }}{% endblock %} {% block head %}{% endblock %} +{% block breadcrumb %}{% endblock %} + {% block content %} -

{{ status_code }}!

- {% match message %} - {% when Some with (x) %} -

{{ x }}

- {% when None %} - {% endmatch %} +
+

{{ status_code }}!

+ {% match message %} + {% when Some with (x) %} +

{{ x }}

+ {% when None %} + {% endmatch %} +

:(

+
{% endblock %} diff --git a/backend/templates/web/about.html b/backend/templates/web/about.html new file mode 100644 index 0000000..d06dd2e --- /dev/null +++ b/backend/templates/web/about.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block title %}index{% endblock %} + +{% block head %}{% endblock %} + +{% block breadcrumb %}{% endblock %} + +{% block content %} +

...fearlessly conquering the world of backend

+

About

+

Hey, I'm Dominic (aka. dergrimm) and am currently a student going to school in Germany.

+

+ You can contact me at: + dominic@dergrimm.net +

+{% endblock %} diff --git a/backend/templates/web/index.html b/backend/templates/web/index.html new file mode 100644 index 0000000..36f1e74 --- /dev/null +++ b/backend/templates/web/index.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} + +{% block title %}index{% endblock %} + +{% block head %}{% endblock %} + +{% block breadcrumb %}{% endblock %} + +{% block content %} +

This is my site

+

Hey, I'm Dominic (aka. dergrimm) and this is my blog!

+

I mostly plan to talk about modern tech and server side stuff like Rust, Crystal, C, databases and APIs.

+

Keep posted by following my feed.

+{% endblock %} diff --git a/backend/templates/web/posts/index.html b/backend/templates/web/posts/index.html new file mode 100644 index 0000000..3a6a9c1 --- /dev/null +++ b/backend/templates/web/posts/index.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block title %}posts{% endblock %} + +{% block head %}{% endblock %} + +{% block breadcrumb %} +
  • posts
  • +{% endblock %} + +{% block content %} +
      + {% for post in posts %} +
    • + + {{ post.name }} + ({{ post.published_at }}{% match post.edited_at %}{% when Some with (x) %} -> {{ x }}{% when None %}{% endmatch %}) + +
      + {{ post.description }} +
    • + {% endfor %} +
    +{% endblock %} diff --git a/backend/templates/web/posts/{slug}.html b/backend/templates/web/posts/{slug}.html new file mode 100644 index 0000000..1f1754d --- /dev/null +++ b/backend/templates/web/posts/{slug}.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block title %}{{ name }}{% endblock %} + +{% block head %}{% endblock %} + +{% block breadcrumb %} +
  • posts
  • +
  • {{ slug }}
  • +{% endblock %} + +{% block content %} +
    + {{content|safe }} +
    +{% endblock %} diff --git a/blog/hello_world/post.md b/blog/hello_world/post.md index 3fc95f6..093cb6e 100644 --- a/blog/hello_world/post.md +++ b/blog/hello_world/post.md @@ -4,7 +4,7 @@ name: Hello world! slug: hello_world description: Hello world to the internet. Set up my first blog! published_at: 2023-02-06 -edited_at: null +edited_at: 2023-02-09 active: true --- diff --git a/docker-compose.yml b/docker-compose.yml index 6f22cf8..5fd4c0c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: "3" services: db: - image: docker.io/postgres:alpine + image: docker.io/postgres:15-alpine restart: always environment: POSTGRES_USER: ${POSTGRES_USER} @@ -11,27 +11,45 @@ services: - db:/var/lib/postgresql/data adminer: - image: docker.io/adminer:standalone + image: docker.io/adminer:4-standalone restart: always ports: - 8080:8080 depends_on: - db - backend: - image: git.dergrimm.net/dergrimm/blog_backend:latest + redis: + image: docker.io/redis:7-alpine + restart: always + + redis-commander: + image: rediscommander/redis-commander:latest + restart: always + environment: + REDIS_HOSTS: local:redis:6379 + ports: + - 8081:8081 + depends_on: + - redis + + blog: + image: git.dergrimm.net/dergrimm/blog:latest build: context: ./backend restart: always command: worker environment: + BACKEND_BIND_URL: 0.0.0.0:80 BACKEND_DB_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_USER} + BACKEND_REDIS_URL: redis://redis + BACKEND_CACHE_POST_CONTENT_TTL: 3600 volumes: - ./blog:/blog ports: - 80:80 depends_on: - db + - redis volumes: db: