From a280a749da70ab54fe8cfb161bb7bd0095620b81 Mon Sep 17 00:00:00 2001
From: ThetaDev
Date: Sun, 26 Feb 2023 19:17:54 +0100
Subject: [PATCH 1/2] feat: add get_file from store
---
Cargo.lock | 67 +++++++
Cargo.toml | 3 +
src/db/mod.rs | 8 +-
src/storage.rs | 203 ++++++++++++++++++++-
src/util.rs | 131 ++++++++++++-
tests/fixtures/mod.rs | 100 ++++++----
tests/testfiles/RustyPipe/index.html | 3 +
tests/testfiles/RustyPipe/page2/index.html | 109 +++++++++++
tests/tests.rs | 122 ++++++++++---
9 files changed, 681 insertions(+), 65 deletions(-)
create mode 100644 tests/testfiles/RustyPipe/page2/index.html
diff --git a/Cargo.lock b/Cargo.lock
index d8cbf64..0ba3c4f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -17,6 +17,21 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
[[package]]
name = "async-trait"
version = "0.1.64"
@@ -55,6 +70,27 @@ dependencies = [
"generic-array",
]
+[[package]]
+name = "brotli"
+version = "3.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "2.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
[[package]]
name = "byteorder"
version = "1.4.3"
@@ -103,6 +139,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+[[package]]
+name = "compressible"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe7853faa8a7c37cddc40823bf5463d368a5207ebb4e7d4d83846da656f493d3"
+dependencies = [
+ "mime",
+]
+
[[package]]
name = "console"
version = "0.15.5"
@@ -572,6 +617,16 @@ version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+[[package]]
+name = "mime_guess"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
[[package]]
name = "miniz_oxide"
version = "0.6.2"
@@ -1027,10 +1082,13 @@ dependencies = [
name = "talon"
version = "0.1.0"
dependencies = [
+ "brotli",
+ "compressible",
"flate2",
"hex",
"hex-literal",
"insta",
+ "mime_guess",
"path_macro",
"poem",
"rmp-serde",
@@ -1270,6 +1328,15 @@ dependencies = [
"version_check",
]
+[[package]]
+name = "unicase"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
+dependencies = [
+ "version_check",
+]
+
[[package]]
name = "unicode-ident"
version = "1.0.6"
diff --git a/Cargo.toml b/Cargo.toml
index 9a07518..12f6cf2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,6 +30,9 @@ zip = { version = "0.6.4", default-features = false, features = [
] }
tar = "0.4.38"
flate2 = "1.0.25"
+brotli = "3.3.4"
+mime_guess = { version = "2.0.4", default-features = false }
+compressible = "0.2.0"
[dev-dependencies]
rstest = "0.16.0"
diff --git a/src/db/mod.rs b/src/db/mod.rs
index 80552e2..6557366 100644
--- a/src/db/mod.rs
+++ b/src/db/mod.rs
@@ -378,11 +378,15 @@ impl Db {
}
fn file_key(version: u32, path: &str) -> String {
- // Remove leading/trailing slashes from path
- let path = path.trim_matches('/');
format!("{version}:{path}")
}
+ /// Get the hash of a file in the database
+ pub fn get_file_opt(&self, version: u32, path: &str) -> Result
+
+ Page 2
+
Carrot cake biscuit icing pudding danish topping powder. Croissant sugar plum
pudding halvah chocolate. Cotton candy tart cake bonbon tart. Shortbread jelly
diff --git a/tests/testfiles/RustyPipe/page2/index.html b/tests/testfiles/RustyPipe/page2/index.html
new file mode 100644
index 0000000..697a387
--- /dev/null
+++ b/tests/testfiles/RustyPipe/page2/index.html
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+ RustyPipe #2
+
+
+
+
RustyPipe #2
+
+ Client for the public YouTube / YouTube Music API (Innertube), inspired by
+ NewPipe.
+
+
+ Page 1
+
+
+ Carrot cake biscuit icing pudding danish topping powder. Croissant sugar plum
+ pudding halvah chocolate. Cotton candy tart cake bonbon tart. Shortbread jelly
+ fruitcake icing pastry. Dragée dessert cupcake cake sesame snaps toffee pie.
+ Sweet roll sweet roll chupa chups jelly-o gummies tootsie roll sweet halvah oat
+ cake. Carrot cake carrot cake muffin bonbon sesame snaps brownie. Bonbon candy
+ macaroon fruitcake candy canes. Cake pudding danish liquorice cupcake jelly-o
+ ice cream. Liquorice lollipop danish tootsie roll toffee. Gingerbread chocolate
+ candy canes donut lemon drops apple pie danish bear claw. Caramels cake jelly
+ jelly sweet chocolate bar gingerbread icing. Cake soufflé lollipop pudding
+ marshmallow candy canes tootsie roll danish.
+
+
+ Carrot cake biscuit icing pudding danish topping powder. Croissant sugar plum
+ pudding halvah chocolate. Cotton candy tart cake bonbon tart. Shortbread jelly
+ fruitcake icing pastry. Dragée dessert cupcake cake sesame snaps toffee pie.
+ Sweet roll sweet roll chupa chups jelly-o gummies tootsie roll sweet halvah oat
+ cake. Carrot cake carrot cake muffin bonbon sesame snaps brownie. Bonbon candy
+ macaroon fruitcake candy canes. Cake pudding danish liquorice cupcake jelly-o
+ ice cream. Liquorice lollipop danish tootsie roll toffee. Gingerbread chocolate
+ candy canes donut lemon drops apple pie danish bear claw. Caramels cake jelly
+ jelly sweet chocolate bar gingerbread icing. Cake soufflé lollipop pudding
+ marshmallow candy canes tootsie roll danish.
+
+
+ Carrot cake biscuit icing pudding danish topping powder. Croissant sugar plum
+ pudding halvah chocolate. Cotton candy tart cake bonbon tart. Shortbread jelly
+ fruitcake icing pastry. Dragée dessert cupcake cake sesame snaps toffee pie.
+ Sweet roll sweet roll chupa chups jelly-o gummies tootsie roll sweet halvah oat
+ cake. Carrot cake carrot cake muffin bonbon sesame snaps brownie. Bonbon candy
+ macaroon fruitcake candy canes. Cake pudding danish liquorice cupcake jelly-o
+ ice cream. Liquorice lollipop danish tootsie roll toffee. Gingerbread chocolate
+ candy canes donut lemon drops apple pie danish bear claw. Caramels cake jelly
+ jelly sweet chocolate bar gingerbread icing. Cake soufflé lollipop pudding
+ marshmallow candy canes tootsie roll danish.
+
+
+ Carrot cake biscuit icing pudding danish topping powder. Croissant sugar plum
+ pudding halvah chocolate. Cotton candy tart cake bonbon tart. Shortbread jelly
+ fruitcake icing pastry. Dragée dessert cupcake cake sesame snaps toffee pie.
+ Sweet roll sweet roll chupa chups jelly-o gummies tootsie roll sweet halvah oat
+ cake. Carrot cake carrot cake muffin bonbon sesame snaps brownie. Bonbon candy
+ macaroon fruitcake candy canes. Cake pudding danish liquorice cupcake jelly-o
+ ice cream. Liquorice lollipop danish tootsie roll toffee. Gingerbread chocolate
+ candy canes donut lemon drops apple pie danish bear claw. Caramels cake jelly
+ jelly sweet chocolate bar gingerbread icing. Cake soufflé lollipop pudding
+ marshmallow candy canes tootsie roll danish.
+
+
+ Carrot cake biscuit icing pudding danish topping powder. Croissant sugar plum
+ pudding halvah chocolate. Cotton candy tart cake bonbon tart. Shortbread jelly
+ fruitcake icing pastry. Dragée dessert cupcake cake sesame snaps toffee pie.
+ Sweet roll sweet roll chupa chups jelly-o gummies tootsie roll sweet halvah oat
+ cake. Carrot cake carrot cake muffin bonbon sesame snaps brownie. Bonbon candy
+ macaroon fruitcake candy canes. Cake pudding danish liquorice cupcake jelly-o
+ ice cream. Liquorice lollipop danish tootsie roll toffee. Gingerbread chocolate
+ candy canes donut lemon drops apple pie danish bear claw. Caramels cake jelly
+ jelly sweet chocolate bar gingerbread icing. Cake soufflé lollipop pudding
+ marshmallow candy canes tootsie roll danish.
+
+
+ Carrot cake biscuit icing pudding danish topping powder. Croissant sugar plum
+ pudding halvah chocolate. Cotton candy tart cake bonbon tart. Shortbread jelly
+ fruitcake icing pastry. Dragée dessert cupcake cake sesame snaps toffee pie.
+ Sweet roll sweet roll chupa chups jelly-o gummies tootsie roll sweet halvah oat
+ cake. Carrot cake carrot cake muffin bonbon sesame snaps brownie. Bonbon candy
+ macaroon fruitcake candy canes. Cake pudding danish liquorice cupcake jelly-o
+ ice cream. Liquorice lollipop danish tootsie roll toffee. Gingerbread chocolate
+ candy canes donut lemon drops apple pie danish bear claw. Caramels cake jelly
+ jelly sweet chocolate bar gingerbread icing. Cake soufflé lollipop pudding
+ marshmallow candy canes tootsie roll danish.
+
+
+ Carrot cake biscuit icing pudding danish topping powder. Croissant sugar plum
+ pudding halvah chocolate. Cotton candy tart cake bonbon tart. Shortbread jelly
+ fruitcake icing pastry. Dragée dessert cupcake cake sesame snaps toffee pie.
+ Sweet roll sweet roll chupa chups jelly-o gummies tootsie roll sweet halvah oat
+ cake. Carrot cake carrot cake muffin bonbon sesame snaps brownie. Bonbon candy
+ macaroon fruitcake candy canes. Cake pudding danish liquorice cupcake jelly-o
+ ice cream. Liquorice lollipop danish tootsie roll toffee. Gingerbread chocolate
+ candy canes donut lemon drops apple pie danish bear claw. Caramels cake jelly
+ jelly sweet chocolate bar gingerbread icing. Cake soufflé lollipop pudding
+ marshmallow candy canes tootsie roll danish.
+
+
+
+
diff --git a/tests/tests.rs b/tests/tests.rs
index f963f19..eeb44cd 100644
--- a/tests/tests.rs
+++ b/tests/tests.rs
@@ -19,13 +19,13 @@ mod database {
}
#[rstest]
- fn export(db: DbWrap) {
+ fn export(db: DbTest) {
let data = get_export(&db);
insta::assert_snapshot!("export", data);
}
#[rstest]
- fn export_import(db: DbWrap) {
+ fn export_import(db: DbTest) {
let td = temp_testdir::TempDir::default();
let p_export = td.join("export.jsonl");
let p_db2 = td.join("db2");
@@ -41,7 +41,7 @@ mod database {
}
#[rstest]
- fn get_website(db: DbWrap) {
+ fn get_website(db: DbTest) {
let ws1 = db.get_website("").unwrap();
let ws2 = db.get_website("spotify-gender-ex").unwrap();
let ws3 = db.get_website("rustypipe").unwrap();
@@ -49,7 +49,7 @@ mod database {
}
#[rstest]
- fn delete_website(db: DbWrap) {
+ fn delete_website(db: DbTest) {
db.delete_website("", true).unwrap();
assert!(matches!(
@@ -67,7 +67,7 @@ mod database {
}
#[rstest]
- fn update_website(db: DbWrap) {
+ fn update_website(db: DbTest) {
db.update_website(
"",
WebsiteUpdate {
@@ -87,19 +87,19 @@ mod database {
}
#[rstest]
- fn get_websites(db: DbWrap) {
+ fn get_websites(db: DbTest) {
let websites = db.get_websites().map(|w| w.unwrap()).collect::>();
insta::assert_ron_snapshot!(websites);
}
#[rstest]
- fn get_version(db: DbWrap) {
+ fn get_version(db: DbTest) {
let version = db.get_version("", VERSION_1_1).unwrap();
insta::assert_ron_snapshot!(version);
}
#[rstest]
- fn delete_version(db: DbWrap) {
+ fn delete_version(db: DbTest) {
db.delete_version("", VERSION_1_2, true).unwrap();
assert!(matches!(
db.get_version("", VERSION_1_2).unwrap_err(),
@@ -120,7 +120,7 @@ mod database {
}
#[rstest]
- fn get_website_versions(db: DbWrap) {
+ fn get_website_versions(db: DbTest) {
let versions = db
.get_website_versions("")
.map(|v| v.unwrap())
@@ -129,7 +129,7 @@ mod database {
}
#[rstest]
- fn get_website_version_ids(db: DbWrap) {
+ fn get_website_version_ids(db: DbTest) {
let ids = db
.get_website_version_ids("")
.map(|v| v.unwrap())
@@ -138,13 +138,13 @@ mod database {
}
#[rstest]
- fn get_file(db: DbWrap) {
+ fn get_file(db: DbTest) {
let hash = db.get_file(VERSION_1_1, "index.html").unwrap();
assert_eq!(hash, HASH_1_1_INDEX);
}
#[rstest]
- fn delete_file(db: DbWrap) {
+ fn delete_file(db: DbTest) {
db.delete_file(VERSION_1_1, "index.html", true).unwrap();
assert!(matches!(
db.get_file(VERSION_1_1, "index.html").unwrap_err(),
@@ -158,7 +158,7 @@ mod database {
}
#[rstest]
- fn get_version_files(db: DbWrap) {
+ fn get_version_files(db: DbTest) {
let files = db
.get_version_files(VERSION_1_1)
.map(|f| f.unwrap())
@@ -173,7 +173,7 @@ mod database {
}
#[rstest]
- fn get_file_hashes(db: DbWrap) {
+ fn get_file_hashes(db: DbTest) {
let hashes = db.get_file_hashes().unwrap();
assert_eq!(hashes.len(), 12)
}
@@ -181,18 +181,20 @@ mod database {
mod storage {
use hex::ToHex;
+ use poem::http::{header, HeaderMap};
+ use talon::storage::StorageCompression;
use super::*;
#[rstest]
- fn insert_files(db_sp: DbWrap) {
+ fn insert_files(db_empty: DbTest) {
let dir = path!("tests" / "testfiles" / "ThetaDev1");
let temp = temp_testdir::TempDir::default();
- let store = Storage::new(temp.to_path_buf(), db_sp.clone());
+ let store = Storage::new(temp.to_path_buf(), db_empty.clone(), Default::default());
store.insert_dir(dir, 1).unwrap();
- let files = db_sp
+ let files = db_empty
.get_version_files(1)
.map(|f| f.unwrap())
.collect::>();
@@ -206,16 +208,16 @@ mod storage {
}
#[rstest]
- fn insert_zip_archive(db_sp: DbWrap) {
+ fn insert_zip_archive(db_empty: DbTest) {
let archive = path!("tests" / "testfiles" / "archive" / "ThetaDev1.zip");
let temp = temp_testdir::TempDir::default();
- let store = Storage::new(temp.to_path_buf(), db_sp.clone());
+ let store = Storage::new(temp.to_path_buf(), db_empty.clone(), Default::default());
store
.insert_zip_archive(File::open(archive).unwrap(), 1)
.unwrap();
- let files = db_sp
+ let files = db_empty
.get_version_files(1)
.map(|f| f.unwrap())
.collect::>();
@@ -229,16 +231,16 @@ mod storage {
}
#[rstest]
- fn insert_tgz_archive(db_sp: DbWrap) {
+ fn insert_tgz_archive(db_empty: DbTest) {
let archive = path!("tests" / "testfiles" / "archive" / "ThetaDev1.tar.gz");
let temp = temp_testdir::TempDir::default();
- let store = Storage::new(temp.to_path_buf(), db_sp.clone());
+ let store = Storage::new(temp.to_path_buf(), db_empty.clone(), Default::default());
store
.insert_tgz_archive(File::open(archive).unwrap(), 1)
.unwrap();
- let files = db_sp
+ let files = db_empty
.get_version_files(1)
.map(|f| f.unwrap())
.collect::>();
@@ -250,4 +252,78 @@ mod storage {
assert!(path.is_file());
}
}
+
+ #[rstest]
+ #[case::gzip(StorageCompression {gzip_en: true, ..Default::default()}, "gz")]
+ #[case::brotli(StorageCompression {brotli_en: true, ..Default::default()}, "br")]
+ fn insert_files_compressed(
+ db_empty: DbTest,
+ #[case] compression: StorageCompression,
+ #[case] ext: &str,
+ ) {
+ let dir = path!("tests" / "testfiles" / "ThetaDev1");
+ let temp = temp_testdir::TempDir::default();
+ let store = Storage::new(temp.to_path_buf(), db_empty.clone(), compression);
+
+ store.insert_dir(dir, 1).unwrap();
+
+ for f in db_empty.get_version_files(1) {
+ let hash = f.unwrap().1;
+ let hash_str = hash.encode_hex::();
+ let path = temp.join(&hash_str[..2]).join(&hash_str);
+ let path_compressed = path.with_extension(ext);
+ assert!(path.is_file());
+
+ // Images should not be compressed
+ let expect = &hash_str
+ != "901d291a47a8a9b55c06f84e5e5f82fd2dcee65cac1406d6e878b805d45c1e93"
+ && &hash_str != "9f7e7971b4bfdb75429e534dea461ed90340886925078cda252cada9aa0e25f7";
+ assert_eq!(path_compressed.is_file(), expect)
+ }
+ }
+
+ #[rstest]
+ #[case::nocmp("", VERSION_1_2, "", true, "text/html", None)]
+ #[case::gzip("gzip", VERSION_1_2, "", true, "text/html", None)]
+ #[case::br("br", VERSION_1_2, "", true, "text/html", None)]
+ #[case::image("br", VERSION_1_2, "assets/image.jpg", false, "image/jpeg", None)]
+ #[case::subdir("br", VERSION_3_1, "page2", true, "text/html", Some("page2/"))]
+ fn get_file(
+ store: StorageTest,
+ #[case] encoding: &str,
+ #[case] version: u32,
+ #[case] path: &str,
+ #[case] compressible: bool,
+ #[case] mime: &str,
+ #[case] rd_path: Option<&str>,
+ ) {
+ let mut headers = HeaderMap::new();
+ headers.insert(header::ACCEPT_ENCODING, encoding.parse().unwrap());
+
+ let expect_ext = if compressible {
+ match encoding {
+ "gzip" => Some("gz"),
+ "" => None,
+ e => Some(e),
+ }
+ } else {
+ None
+ };
+
+ let index_file = store.get_file(version, path, &headers).unwrap();
+ assert!(index_file.file_path.is_file());
+ assert_eq!(
+ index_file
+ .file_path
+ .extension()
+ .map(|s| s.to_str().unwrap()),
+ expect_ext
+ );
+ assert_eq!(
+ index_file.encoding,
+ Some(encoding).filter(|s| compressible && !s.is_empty())
+ );
+ assert_eq!(index_file.mime.unwrap().essence_str(), mime);
+ assert_eq!(index_file.rd_path.as_deref(), rd_path);
+ }
}
From 28c2884a2e08d33d7dd50c23a4dd2b5e7bc4d912 Mon Sep 17 00:00:00 2001
From: ThetaDev
Date: Sun, 26 Feb 2023 23:26:52 +0100
Subject: [PATCH 2/2] feat: add config
---
Cargo.lock | 2 +
Cargo.toml | 4 +-
src/config.rs | 177 ++++++++++++++++++++
src/db/model.rs | 2 -
src/lib.rs | 1 +
src/storage.rs | 68 +++-----
src/util.rs | 38 +++--
tests/fixtures/mod.rs | 20 ++-
tests/snapshots/tests__config__default.snap | 30 ++++
tests/snapshots/tests__config__sparse.snap | 28 ++++
tests/testfiles/config/config.toml | 23 +++
tests/testfiles/config/config_sparse.toml | 16 ++
tests/tests.rs | 36 +++-
13 files changed, 360 insertions(+), 85 deletions(-)
create mode 100644 src/config.rs
create mode 100644 tests/snapshots/tests__config__default.snap
create mode 100644 tests/snapshots/tests__config__sparse.snap
create mode 100644 tests/testfiles/config/config.toml
create mode 100644 tests/testfiles/config/config_sparse.toml
diff --git a/Cargo.lock b/Cargo.lock
index 0ba3c4f..7852659 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1088,9 +1088,11 @@ dependencies = [
"hex",
"hex-literal",
"insta",
+ "log",
"mime_guess",
"path_macro",
"poem",
+ "regex",
"rmp-serde",
"rstest",
"serde",
diff --git a/Cargo.toml b/Cargo.toml
index 12f6cf2..648985d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,7 +12,7 @@ sled = "0.34.7"
serde = "1.0.152"
serde_json = "1.0.93"
rmp-serde = "1.1.1"
-toml = "0.7.2"
+toml = { version = "0.7.2", default-features = false, features = ["parse"] }
thiserror = "1.0.38"
time = { version = "0.3.15", features = [
"macros",
@@ -33,6 +33,8 @@ flate2 = "1.0.25"
brotli = "3.3.4"
mime_guess = { version = "2.0.4", default-features = false }
compressible = "0.2.0"
+regex = "1.7.1"
+log = "0.4.17"
[dev-dependencies]
rstest = "0.16.0"
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..0e52b69
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,177 @@
+use std::{collections::BTreeMap, ops::Deref, path::Path, sync::Arc};
+
+use regex::Regex;
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Default)]
+pub struct Config {
+ i: Arc,
+}
+
+#[derive(Debug, Default, Clone, Serialize, Deserialize)]
+#[serde(default)]
+pub struct ConfigInner {
+ pub server: ServerCfg,
+ pub compression: CompressionCfg,
+ pub keys: BTreeMap,
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum ConfigError {
+ #[error("io error: {0}")]
+ Io(#[from] std::io::Error),
+ #[error("parsing error: {0}")]
+ Parse(#[from] toml::de::Error),
+}
+
+type Result = std::result::Result;
+
+impl Deref for Config {
+ type Target = ConfigInner;
+
+ fn deref(&self) -> &Self::Target {
+ &self.i
+ }
+}
+
+impl Serialize for Config {
+ fn serialize(&self, serializer: S) -> std::result::Result
+ where
+ S: serde::Serializer,
+ {
+ ConfigInner::serialize(self, serializer)
+ }
+}
+
+impl<'de> Deserialize<'de> for Config {
+ fn deserialize(deserializer: D) -> std::result::Result
+ where
+ D: serde::Deserializer<'de>,
+ {
+ ConfigInner::deserialize(deserializer).map(|c| Self { i: c.into() })
+ }
+}
+
+impl Config {
+ pub fn new(cfg: ConfigInner) -> Self {
+ Self { i: cfg.into() }
+ }
+
+ pub fn from_file>(path: P) -> Result {
+ let cfg_str = std::fs::read_to_string(path)?;
+ Ok(toml::from_str::(&cfg_str)?)
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(default)]
+pub struct ServerCfg {
+ pub address: String,
+ pub port: u32,
+}
+
+impl Default for ServerCfg {
+ fn default() -> Self {
+ Self {
+ address: "0.0.0.0".to_owned(),
+ port: 8080,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(default)]
+pub struct CompressionCfg {
+ /// Enable gzip compression
+ pub gzip_en: bool,
+ /// Gzip compression level (0-9)
+ pub gzip_level: u8,
+ /// Enable brotli compression
+ pub brotli_en: bool,
+ /// Brozli compression level (0-11)
+ pub brotli_level: u8,
+}
+
+impl Default for CompressionCfg {
+ fn default() -> Self {
+ Self {
+ gzip_en: false,
+ gzip_level: 6,
+ brotli_en: false,
+ brotli_level: 7,
+ }
+ }
+}
+
+impl CompressionCfg {
+ pub fn enabled(&self) -> bool {
+ self.gzip_en || self.brotli_en
+ }
+}
+
+#[derive(Debug, Default, Clone, Serialize, Deserialize)]
+#[serde(default)]
+pub struct KeyCfg {
+ #[serde(skip_serializing_if = "Domains::is_none")]
+ pub domains: Domains,
+}
+
+#[derive(Debug, Default, Clone, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum Domains {
+ #[default]
+ None,
+ Single(String),
+ Multiple(Vec),
+}
+
+impl Domains {
+ fn is_none(&self) -> bool {
+ matches!(self, Domains::None)
+ }
+
+ fn pattern_matches_domain(pattern: &str, domain: &str) -> bool {
+ if pattern == "*" {
+ true
+ } else if pattern.starts_with('/') && pattern.ends_with('/') {
+ let regex_str = &pattern[1..pattern.len() - 1];
+ let re = match Regex::new(regex_str) {
+ Ok(re) => re,
+ Err(e) => {
+ log::error!("could not parse regex `{regex_str}`, error: {e}");
+ return false;
+ }
+ };
+ re.is_match(domain)
+ } else {
+ domain == pattern
+ }
+ }
+
+ pub fn matches_domain(&self, domain: &str) -> bool {
+ match self {
+ Domains::None => false,
+ Domains::Single(pattern) => Self::pattern_matches_domain(pattern, domain),
+ Domains::Multiple(patterns) => patterns
+ .iter()
+ .any(|pattern| Self::pattern_matches_domain(pattern, domain)),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use rstest::rstest;
+
+ #[rstest]
+ #[case("*", "hello-world", true)]
+ #[case("hello-world", "hello-world", true)]
+ #[case("hello-world", "hello-world2", false)]
+ #[case("/^talon-\\d+/", "talon-1", true)]
+ #[case("/^talon-\\d+/", "talon-x", false)]
+ fn pattern_matches_domain(#[case] pattern: &str, #[case] domain: &str, #[case] expect: bool) {
+ assert_eq!(Domains::pattern_matches_domain(pattern, domain), expect);
+ }
+}
diff --git a/src/db/model.rs b/src/db/model.rs
index 0ae94f2..a56e21b 100644
--- a/src/db/model.rs
+++ b/src/db/model.rs
@@ -60,8 +60,6 @@ pub struct WebsiteUpdate {
pub source_url: Option