This commit is contained in:
Dominic Grimm 2023-02-11 12:48:39 +01:00
parent 584c07ff23
commit 53e144d9a7
No known key found for this signature in database
GPG key ID: 6F294212DEAAC530
24 changed files with 1236 additions and 254 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
blog/

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Dominic Grimm <dominic@dergrimm.net>
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.

View file

@ -6,3 +6,4 @@ Dockerfile
vendor/ vendor/
example/ example/
static/ static/
LICENSE

477
backend/Cargo.lock generated
View file

@ -30,7 +30,7 @@ dependencies = [
"actix-service", "actix-service",
"actix-utils", "actix-utils",
"ahash", "ahash",
"base64", "base64 0.21.0",
"bitflags", "bitflags",
"brotli", "brotli",
"bytes", "bytes",
@ -43,14 +43,14 @@ dependencies = [
"http", "http",
"httparse", "httparse",
"httpdate", "httpdate",
"itoa", "itoa 1.0.5",
"language-tags", "language-tags",
"local-channel", "local-channel",
"mime", "mime",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rand", "rand",
"sha1", "sha1 0.10.5",
"smallvec", "smallvec",
"tokio", "tokio",
"tokio-util", "tokio-util",
@ -155,7 +155,7 @@ dependencies = [
"futures-core", "futures-core",
"futures-util", "futures-util",
"http", "http",
"itoa", "itoa 1.0.5",
"language-tags", "language-tags",
"log", "log",
"mime", "mime",
@ -322,6 +322,17 @@ dependencies = [
"toml", "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]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.1.0"
@ -339,13 +350,14 @@ dependencies = [
"askama_actix", "askama_actix",
"chrono", "chrono",
"clap", "clap",
"comrak",
"diesel", "diesel",
"env_logger", "env_logger",
"envconfig", "envconfig",
"fronma", "fronma",
"lazy_static", "lazy_static",
"log", "log",
"pulldown-cmark", "r2d2_redis",
"scan_dir", "scan_dir",
"serde", "serde",
"serde_yaml 0.9.17", "serde_yaml 0.9.17",
@ -368,12 +380,42 @@ dependencies = [
"rustc-demangle", "rustc-demangle",
] ]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.21.0" version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" 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]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -491,6 +533,7 @@ dependencies = [
"once_cell", "once_cell",
"strsim", "strsim",
"termcolor", "termcolor",
"terminal_size",
] ]
[[package]] [[package]]
@ -525,6 +568,38 @@ dependencies = [
"unicode-width", "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]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.4.0" version = "0.4.0"
@ -633,6 +708,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "deunicode"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690"
[[package]] [[package]]
name = "diesel" name = "diesel"
version = "2.0.3" version = "2.0.3"
@ -643,7 +724,7 @@ dependencies = [
"byteorder", "byteorder",
"chrono", "chrono",
"diesel_derives", "diesel_derives",
"itoa", "itoa 1.0.5",
"pq-sys", "pq-sys",
"r2d2", "r2d2",
] ]
@ -670,6 +751,41 @@ dependencies = [
"crypto-common", "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]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.32" version = "0.8.32"
@ -679,6 +795,12 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "entities"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
[[package]] [[package]]
name = "env_logger" name = "env_logger"
version = "0.10.0" version = "0.10.0"
@ -733,6 +855,16 @@ dependencies = [
"libc", "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]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.25" version = "1.0.25"
@ -886,7 +1018,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
"itoa", "itoa 1.0.5",
] ]
[[package]] [[package]]
@ -979,6 +1111,12 @@ dependencies = [
"windows-sys 0.45.0", "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]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.5" version = "1.0.5"
@ -1021,6 +1159,15 @@ version = "0.2.139"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" 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]] [[package]]
name = "link-cplusplus" name = "link-cplusplus"
version = "1.0.8" version = "1.0.8"
@ -1182,6 +1329,28 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" 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]] [[package]]
name = "os_str_bytes" name = "os_str_bytes"
version = "6.4.1" version = "6.4.1"
@ -1238,6 +1407,68 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 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]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.9" version = "0.2.9"
@ -1256,6 +1487,20 @@ version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 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]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.17"
@ -1304,23 +1549,21 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "quick-error" name = "quick-error"
version = "1.2.3" version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 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]] [[package]]
name = "quote" name = "quote"
version = "1.0.23" version = "1.0.23"
@ -1341,6 +1584,16 @@ dependencies = [
"scheduled-thread-pool", "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]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -1371,6 +1624,21 @@ dependencies = [
"getrandom", "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]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.16" version = "0.2.16"
@ -1380,6 +1648,17 @@ dependencies = [
"bitflags", "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]] [[package]]
name = "regex" name = "regex"
version = "1.7.1" version = "1.7.1"
@ -1432,6 +1711,21 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" 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]] [[package]]
name = "scan_dir" name = "scan_dir"
version = "0.3.3" version = "0.3.3"
@ -1494,7 +1788,7 @@ version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7434af0dc1cbd59268aa98b4c22c131c0584d2232f6fb166efb993e2832e896a" checksum = "7434af0dc1cbd59268aa98b4c22c131c0584d2232f6fb166efb993e2832e896a"
dependencies = [ dependencies = [
"itoa", "itoa 1.0.5",
"ryu", "ryu",
"serde", "serde",
] ]
@ -1506,7 +1800,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"itoa", "itoa 1.0.5",
"ryu", "ryu",
"serde", "serde",
] ]
@ -1530,12 +1824,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fb06d4b6cdaef0e0c51fa881acb721bed3c924cfaa71d9c94a3b771dfdf6567" checksum = "8fb06d4b6cdaef0e0c51fa881acb721bed3c924cfaa71d9c94a3b771dfdf6567"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"itoa", "itoa 1.0.5",
"ryu", "ryu",
"serde", "serde",
"unsafe-libyaml", "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]] [[package]]
name = "sha1" name = "sha1"
version = "0.10.5" version = "0.10.5"
@ -1547,6 +1850,29 @@ dependencies = [
"digest", "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]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.0" version = "1.4.0"
@ -1556,6 +1882,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "siphasher"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.7" version = "0.4.7"
@ -1565,6 +1897,15 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "slug"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373"
dependencies = [
"deunicode",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.10.0" version = "1.10.0"
@ -1609,6 +1950,30 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "termcolor" name = "termcolor"
version = "1.2.0" version = "1.2.0"
@ -1618,6 +1983,36 @@ dependencies = [
"winapi-util", "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]] [[package]]
name = "tikv-jemalloc-sys" name = "tikv-jemalloc-sys"
version = "0.5.3+5.3.0-patched" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
dependencies = [ dependencies = [
"itoa", "itoa 1.0.5",
"serde", "serde",
"time-core", "time-core",
"time-macros", "time-macros",
@ -1753,12 +2148,24 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "typed-arena"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.16.0" version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "ucd-trie"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
[[package]] [[package]]
name = "unicase" name = "unicase"
version = "2.6.0" version = "2.6.0"
@ -1795,6 +2202,12 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "unicode_categories"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]] [[package]]
name = "unsafe-libyaml" name = "unsafe-libyaml"
version = "0.2.5" version = "0.2.5"
@ -1824,6 +2237,17 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 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]] [[package]]
name = "wasi" name = "wasi"
version = "0.10.0+wasi-snapshot-preview1" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "xdg"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6"
dependencies = [
"dirs",
]
[[package]] [[package]]
name = "yaml-rust" name = "yaml-rust"
version = "0.4.5" version = "0.4.5"

View file

@ -3,6 +3,7 @@ name = "backend"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
authors = ["Dominic Grimm <dominic@dergrimm.net>"] authors = ["Dominic Grimm <dominic@dergrimm.net>"]
publish = false
[[bin]] [[bin]]
name = "backend" name = "backend"
@ -11,11 +12,11 @@ build = "build.rs"
[[bin]] [[bin]]
name = "blogctl" name = "blogctl"
# [profile.release] [profile.release]
# codegen-units = 1 codegen-units = 1
# lto = "fat" lto = "fat"
# strip = true strip = true
# panic = "abort" panic = "abort"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # 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" askama_actix = "0.13.0"
chrono = { version = "0.4.23", features = ["serde"] } chrono = { version = "0.4.23", features = ["serde"] }
clap = { version = "4.1.4", features = ["derive"] } 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"] } 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" env_logger = "0.10.0"
envconfig = "0.10.0" envconfig = "0.10.0"
fronma = "0.1.1" fronma = "0.1.1"
lazy_static = "1.4.0" lazy_static = "1.4.0"
log = "0.4.17" 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" scan_dir = "0.3.3"
serde = { version = "1.0.152", features = ["derive"] } serde = { version = "1.0.152", features = ["derive"] }
serde_yaml = "0.9.17" serde_yaml = "0.9.17"

View file

@ -10,6 +10,11 @@ WORKDIR /usr/src/static
COPY --from=css /usr/src/scss/dist ./css COPY --from=css /usr/src/scss/dist ./css
RUN minify . -r -o . 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 docker.io/lukemathwalker/cargo-chef:latest-rust-1.67.0 as chef
FROM chef as diesel FROM chef as diesel
@ -28,11 +33,16 @@ RUN cargo chef cook --recipe-path recipe.json
RUN rm -rf ./src RUN rm -rf ./src
COPY ./build.rs . COPY ./build.rs .
COPY --from=static /usr/src/static ./static COPY --from=static /usr/src/static ./static
COPY ./templates ./templates COPY --from=templates /usr/src/templates ./templates
COPY ./src ./src COPY ./src ./src
RUN cargo build RUN cargo build
FROM docker.io/debian:bullseye-slim as runner FROM docker.io/debian:bullseye-slim as runner
LABEL maintainer="Dominic Grimm <dominic@dergrimm.net>" \
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 update
RUN apt install -y libpq5 RUN apt install -y libpq5
RUN apt install -y ca-certificates RUN apt install -y ca-certificates
@ -40,12 +50,10 @@ RUN apt-get clean
RUN apt-get autoremove -y RUN apt-get autoremove -y
RUN rm -rf /var/lib/{apt,dpkg,cache,log}/ RUN rm -rf /var/lib/{apt,dpkg,cache,log}/
WORKDIR /usr/local/bin WORKDIR /usr/local/bin
ENV RUST_BACKTRACE=full
COPY --from=diesel /usr/local/cargo/bin/diesel . COPY --from=diesel /usr/local/cargo/bin/diesel .
WORKDIR /usr/src/backend WORKDIR /usr/src/backend
COPY ./run.sh . COPY ./run.sh .
RUN chmod +x ./run.sh RUN chmod +x ./run.sh
COPY ./migrations ./migrations COPY ./migrations ./migrations
COPY --from=builder /usr/src/backend/target/debug/backend /usr/src/backend/target/debug/blogctl ./bin/ COPY --from=builder /usr/src/backend/target/debug/backend /usr/src/backend/target/debug/blogctl ./bin/
EXPOSE 80
ENTRYPOINT [ "./run.sh" ] ENTRYPOINT [ "./run.sh" ]

View file

@ -1,42 +1,88 @@
html, html,
body { body {
width: 100vw;
min-height: 100vh;
}
* {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100vw;
min-height: 100vh;
font-family: "JetBrains Mono", monospace; 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 { #wrapper {
width: 75%;
margin: 0 auto; margin: 0 auto;
overflow: auto;
min-height: 100vh;
display: flex;
flex-direction: column;
} }
#navbar { #navbar {
display: flex; 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 { #navbar-brand {
@ -48,18 +94,138 @@ body {
} }
} }
.navbar-link a,
#navbar-links ul li a {
display: block;
text-align: center;
}
#navbar-links { #navbar-links {
width: 50%; width: 50%;
ul { ul {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
float: right; float: right;
li {
float: left;
&:not(:first-of-type) a {
padding-left: 1rem;
}
}
} }
} }
#content { #main-wrapper {
padding: 3%; 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 { .double-border {
border: 5px double black; 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%;
}
}

View file

@ -1,4 +1,3 @@
use diesel::QueryDsl;
#[cfg(not(target_env = "msvc"))] #[cfg(not(target_env = "msvc"))]
use tikv_jemallocator::Jemalloc; use tikv_jemallocator::Jemalloc;
@ -6,104 +5,23 @@ use tikv_jemallocator::Jemalloc;
#[global_allocator] #[global_allocator]
static GLOBAL: Jemalloc = Jemalloc; static GLOBAL: Jemalloc = Jemalloc;
use actix_web::{get, http::StatusCode, middleware, web, App, HttpResponse, HttpServer}; use actix_web::{middleware, App, 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<T: std::fmt::Display>(s: T) -> ::askama::Result<String> {
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<String>,
}
#[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<db::DbPool>, path: web::Path<String>) -> 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::<db::models::Post>(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
}
}
}
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
env_logger::init(); 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 || { HttpServer::new(move || {
let generated = generate();
App::new() App::new()
.wrap(middleware::Compress::default()) .wrap(middleware::Compress::default())
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
.app_data(web::Data::new(db::pool().unwrap())) .configure(backend::web::init)
.service(post_by_slug)
.service(ResourceFiles::new("/static", generated))
.default_service(web::route().to(not_found))
}) })
.bind("0.0.0.0:80") .bind(&backend::config::CONFIG.bind_url)
.unwrap() .unwrap()
.run() .run()
.await .await

View file

@ -9,6 +9,7 @@ use anyhow::{bail, Result};
use chrono::prelude::*; use chrono::prelude::*;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use diesel::prelude::*; use diesel::prelude::*;
use r2d2_redis::redis;
use scan_dir::ScanDir; use scan_dir::ScanDir;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fs; use std::fs;
@ -27,6 +28,9 @@ struct Cli {
enum Commands { enum Commands {
#[clap(about = "Imports new posts")] #[clap(about = "Imports new posts")]
Import, Import,
#[clap(about = "Clears redis cache")]
Clear,
} }
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
@ -50,88 +54,137 @@ struct Post {
fn main() -> Result<()> { fn main() -> Result<()> {
match Cli::parse().commands { match Cli::parse().commands {
Commands::Import => { 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| { let posts = ScanDir::dirs()
iter.map(|(entry, _)| { .read("/blog", |iter| {
let path = entry.path().join("post.md"); iter.map(|(entry, _)| {
let src = fs::read_to_string(&path)?; let path = entry.path().join("post.md");
let frontmatter = match fronma::parser::parse::<PostFrontmatter>(&src) { let src = fs::read_to_string(&path)?;
Ok(x) => x, let frontmatter = match fronma::parser::parse::<PostFrontmatter>(&src) {
Err(x) => bail!("Error parsing frontmatter: {:?}", x), Ok(x) => x,
}; Err(x) => bail!("Error parsing frontmatter: {:?}", x),
};
Ok(Post { Ok(Post {
path, path,
frontmatter: frontmatter.headers, frontmatter: frontmatter.headers,
content: frontmatter.body.to_string(), content: frontmatter.body.to_string(),
})
})
.collect::<Result<Vec<_>>>()
})?? {
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),
}) })
.execute(conn)?; })
} else { .collect::<Result<Vec<_>>>()
let id = if let Some(id) = db::schema::posts::table })??
.select(db::schema::posts::id) .into_iter()
.filter(db::schema::posts::slug.eq(&post.frontmatter.slug)) .map(|post| -> Result<_> {
.first::<i32>(conn) let trimmed = PostFrontmatter {
.optional()? 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) diesel::update(db::schema::posts::table)
.filter(db::schema::posts::id.eq(id)) .filter(db::schema::posts::id.eq(id))
.set(db::models::UpdatePost { .set(db::models::UpdatePost {
name: Some(&post.frontmatter.name), name: Some(&trimmed.name),
slug: None, slug: Some(&trimmed.slug),
description: Some(&post.frontmatter.description), description: Some(&trimmed.description),
content: Some(content), content: Some(content),
published_at: Some(post.frontmatter.published_at), published_at: Some(trimmed.published_at),
edited_at: Some(post.frontmatter.edited_at), edited_at: Some(trimmed.edited_at),
active: Some(post.frontmatter.active), active: Some(trimmed.active),
}) })
.execute(conn)?; .execute(db_conn)?;
id Ok(id)
} else { } else {
diesel::insert_into(db::schema::posts::table) let id = if let Some(id) = db::schema::posts::table
.values(db::models::NewPost { .select(db::schema::posts::id)
name: &post.frontmatter.name, .filter(db::schema::posts::slug.eq(&trimmed.slug))
slug: &post.frontmatter.slug, .first::<i32>(db_conn)
description: &post.frontmatter.description, .optional()?
content: content, {
published_at: post.frontmatter.published_at, diesel::update(db::schema::posts::table)
edited_at: post.frontmatter.edited_at, .filter(db::schema::posts::id.eq(id))
active: post.frontmatter.active, .set(db::models::UpdatePost {
}) name: Some(&trimmed.name),
.returning(db::schema::posts::id) slug: None,
.get_result::<i32>(conn)? 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( id
post.path, } else {
format!( diesel::insert_into(db::schema::posts::table)
"---\n{}---\n{}", .values(db::models::NewPost {
serde_yaml::to_string(&PostFrontmatter { name: &trimmed.name,
id: Some(id), slug: &trimmed.slug,
..post.frontmatter description: &trimmed.description,
})?, content: content,
post.content published_at: trimmed.published_at,
), edited_at: trimmed.edited_at,
)?; active: trimmed.active,
} })
.returning(db::schema::posts::id)
.get_result::<i32>(db_conn)?
};
fs::write(
post.path,
format!(
"---\n{}---\n\n{}\n",
serde_yaml::to_string(&PostFrontmatter {
id: Some(id),
..trimmed
})?,
content
),
)?;
Ok(id)
}
})
.collect::<Result<Vec<_>>>()?;
let ids = db::schema::posts::table
.select(db::schema::posts::id)
.load::<i32>(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::<i32>(db_conn)?
{
redis::cmd("DEL")
.arg(cache::keys::post_content(id))
.query::<()>(redis_conn)?;
} }
Ok(()) Ok(())

28
backend/src/cache.rs Normal file
View file

@ -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<RedisConnectionManager>;
pub type ConnectionPool = r2d2::PooledConnection<RedisConnectionManager>;
pub fn establish_connection() -> Result<redis::Connection> {
Ok(redis::Client::open(config::CONFIG.redis_url.as_str())?.get_connection()?)
}
pub fn pool() -> Result<RedisPool> {
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)
}
}

View file

@ -1,11 +1,19 @@
use envconfig::Envconfig; use envconfig::Envconfig;
use lazy_static::lazy_static; use lazy_static::lazy_static;
#[derive(Envconfig, Debug)] #[derive(Envconfig, Debug)]
pub struct Config { pub struct Config {
#[envconfig(from = "BACKEND_BIND_URL")]
pub bind_url: String,
#[envconfig(from = "BACKEND_DB_URL")] #[envconfig(from = "BACKEND_DB_URL")]
pub db_url: String, 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! { lazy_static! {

View file

@ -1,2 +1,5 @@
pub mod cache;
pub mod config; pub mod config;
pub mod db; pub mod db;
pub mod markdown;
pub mod web;

28
backend/src/markdown.rs Normal file
View file

@ -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)
}

176
backend/src/web/mod.rs Normal file
View file

@ -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<db::DbPool>) -> 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::models::Post>(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<db::DbPool>,
redis_pool: web::Data<cache::RedisPool>,
path: web::Path<String>,
) -> 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::<Option<String>>(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::models::Post>(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::<Option<String>>(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);
}

View file

@ -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<String>,
}
#[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<db::models::Post>,
}
#[derive(Template)]
#[template(path = "web/posts/{slug}.html")]
pub struct PostBySlug {
pub name: String,
pub slug: String,
pub content: String,
}

View file

@ -7,7 +7,7 @@
<title>{% block title %}{% endblock %} | dergrimm's blog</title> <title>{% block title %}{% endblock %} | dergrimm's blog</title>
<link rel="preconnect" href="https://fonts.bunny.net" /> <link rel="preconnect" href="https://fonts.bunny.net" />
<link href="https://fonts.bunny.net/css?family=jetbrains-mono:400" rel="stylesheet" /> <link href="https://fonts.bunny.net/css?family=jetbrains-mono:400,400i,700,700i" rel="stylesheet" />
<link rel="stylesheet" href="/static/css/styles.css" /> <link rel="stylesheet" href="/static/css/styles.css" />
@ -15,19 +15,39 @@
</head> </head>
<body> <body>
<div id="wrapper"> <div id="wrapper">
<nav id="navbar" class="double-border"> <header class="double-border section-margin section-light-padding">
<div id="navbar-brand"><a href="/">dergrimm's blog</a></div> <nav id="navbar">
<div id="navbar-links"> <div id="navbar-brand" class="navbar-link"><a href="/">dergrimm's blog</a></div>
<ul> <div id="navbar-links">
<li><a href="/posts">posts</a></li> <ul>
<li><a href="/about">about</a></li> <li><a href="/posts">posts</a></li>
</ul> <li><a href="/about">about</a></li>
</div> </ul>
</nav> </div>
</nav>
</header>
<div id="content" class="double-border"> <div id="main-wrapper" class="section-margin">
{% block content %}{% endblock %} <nav class="breadcrumb">
<ul>
<li><a href="/">/</a></li>
{% block breadcrumb %}{% endblock %}
</ul>
</nav>
<hr />
<main>
{% block content %}{% endblock %}
</main>
</div> </div>
<footer id="footer" class="double-border section-margin section-light-padding">
(C) 2023 Dominic Grimm
&lt;<a href='m&#97;ilto&#58;%64o%6Di&#110;&#105;c&#64;&#37;&#54;4ergr&#37;&#54;9mm&#37;2En&#101;t'>dominic&#64;&#100;e&#114;grimm&#46;net</a>&gt;
| <a href="https://git.dergrimm.net/dergrimm/blog">repo</a>
| <a href="/">dergrimm.net</a>
</footer>
</div> </div>
</body> </body>
</html> </html>

View file

@ -1,11 +0,0 @@
{% extends "base.html" %}
{% block title %}{{ post.name }}{% endblock %}
{% block head %}{% endblock %}
{% block content %}
<div class="blog">
{{ post.content|cmark|safe }}
</div>
{% endblock %}

View file

@ -1,14 +1,19 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ status_code }}{% endblock %} {% block title %}{{ status_code|lower }}{% endblock %}
{% block head %}{% endblock %} {% block head %}{% endblock %}
{% block breadcrumb %}{% endblock %}
{% block content %} {% block content %}
<h1>{{ status_code }}!</h1> <div id="status-code">
{% match message %} <p><b>{{ status_code }}!</b></p>
{% when Some with (x) %} {% match message %}
<p>{{ x }}</p> {% when Some with (x) %}
{% when None %} <p id="status-code-message"><i>{{ x }}</i></p>
{% endmatch %} {% when None %}
{% endmatch %}
<p>:(</p>
</div>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block title %}index{% endblock %}
{% block head %}{% endblock %}
{% block breadcrumb %}{% endblock %}
{% block content %}
<p><i>...fearlessly conquering the world of backend</i></p>
<h1>About</h1>
<p>Hey, I'm Dominic (aka. dergrimm) and am currently a student going to school in Germany.</p>
<p>
You can contact me at:
<a href='m&#97;ilto&#58;%64o%6Di&#110;&#105;c&#64;&#37;&#54;4ergr&#37;&#54;9mm&#37;2En&#101;t'>dominic&#64;&#100;e&#114;grimm&#46;net</a>
</p>
{% endblock %}

View file

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block title %}index{% endblock %}
{% block head %}{% endblock %}
{% block breadcrumb %}{% endblock %}
{% block content %}
<h1>This is my site</h1>
<p>Hey, I'm Dominic (aka. dergrimm) and this is my blog!</p>
<p>I mostly plan to talk about modern tech and server side stuff like Rust, Crystal, C, databases and APIs.</p>
<p>Keep posted by following my feed.</p>
{% endblock %}

View file

@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block title %}posts{% endblock %}
{% block head %}{% endblock %}
{% block breadcrumb %}
<li><a href="/posts">posts</a></li>
{% endblock %}
{% block content %}
<ul id="post-index" class="dashed">
{% for post in posts %}
<li>
<span>
<a href="/posts/{{ post.slug }}">{{ post.name }}</a>
(<i>{{ post.published_at }}{% match post.edited_at %}{% when Some with (x) %} -> {{ x }}{% when None %}{% endmatch %}</i>)
</span>
<br />
<span>{{ post.description }}</span>
</li>
{% endfor %}
</ul>
{% endblock %}

View file

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}{{ name }}{% endblock %}
{% block head %}{% endblock %}
{% block breadcrumb %}
<li><a href="/posts">posts</a></li>
<li><a href="/posts/{{ slug }}">{{ slug }}</a></li>
{% endblock %}
{% block content %}
<article class="wysiwyg">
{{content|safe }}
</article>
{% endblock %}

View file

@ -4,7 +4,7 @@ name: Hello world!
slug: hello_world slug: hello_world
description: Hello world to the internet. Set up my first blog! description: Hello world to the internet. Set up my first blog!
published_at: 2023-02-06 published_at: 2023-02-06
edited_at: null edited_at: 2023-02-09
active: true active: true
--- ---

View file

@ -2,7 +2,7 @@ version: "3"
services: services:
db: db:
image: docker.io/postgres:alpine image: docker.io/postgres:15-alpine
restart: always restart: always
environment: environment:
POSTGRES_USER: ${POSTGRES_USER} POSTGRES_USER: ${POSTGRES_USER}
@ -11,27 +11,45 @@ services:
- db:/var/lib/postgresql/data - db:/var/lib/postgresql/data
adminer: adminer:
image: docker.io/adminer:standalone image: docker.io/adminer:4-standalone
restart: always restart: always
ports: ports:
- 8080:8080 - 8080:8080
depends_on: depends_on:
- db - db
backend: redis:
image: git.dergrimm.net/dergrimm/blog_backend:latest 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: build:
context: ./backend context: ./backend
restart: always restart: always
command: worker command: worker
environment: environment:
BACKEND_BIND_URL: 0.0.0.0:80
BACKEND_DB_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_USER} BACKEND_DB_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_USER}
BACKEND_REDIS_URL: redis://redis
BACKEND_CACHE_POST_CONTENT_TTL: 3600
volumes: volumes:
- ./blog:/blog - ./blog:/blog
ports: ports:
- 80:80 - 80:80
depends_on: depends_on:
- db - db
- redis
volumes: volumes:
db: db: