diff --git a/Cargo.lock b/Cargo.lock index 287700c..53d7bf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" [[package]] name = "aho-corasick" -version = "0.7.19" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] @@ -88,9 +88,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.58" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" +checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364" dependencies = [ "proc-macro2", "quote", @@ -168,9 +168,9 @@ checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" [[package]] name = "bytes" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" [[package]] name = "cached" @@ -211,9 +211,9 @@ checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663" [[package]] name = "cc" -version = "1.0.76" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a284da2e6fe2092f2353e51713435363112dfd60030e22add80be333fb928f" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" [[package]] name = "cfg-if" @@ -223,9 +223,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.0.24" +version = "4.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60494cedb60cb47462c0ff7be53de32c0e42a6fc2c772184554fa12bd9489c03" +checksum = "4d63b9e9c07271b9957ad22c173bae2a4d9a81127680962039296abcd2f8251d" dependencies = [ "bitflags", "clap_lex", @@ -331,9 +331,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer", "crypto-common", @@ -363,6 +363,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" + [[package]] name = "futures" version = "0.3.25" @@ -567,9 +573,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" dependencies = [ "http", "hyper", @@ -598,9 +604,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", "hashbrown", @@ -638,9 +644,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.137" +version = "0.2.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" [[package]] name = "libflate" @@ -682,10 +688,12 @@ dependencies = [ "regex", "route-recognizer", "rust-embed", + "sealed_test", "serde", "serde_json", "time", "tokio", + "toml", "url", ] @@ -792,9 +800,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "os_str_bytes" -version = "6.4.0" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5bf27447411e9ee3ff51186bf7a08e16c341efdde93f4d823e8844429bed7e" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" [[package]] name = "parking" @@ -814,9 +822,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" +checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" dependencies = [ "cfg-if", "libc", @@ -858,6 +866,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.21" @@ -923,6 +937,15 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "ring" version = "0.16.20" @@ -1018,6 +1041,18 @@ dependencies = [ "base64", ] +[[package]] +name = "rusty-forkfork" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce85af4dfa2fb0c0143121ab5e424c71ea693867357c9159b8777b59984c218" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.11" @@ -1059,6 +1094,28 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sealed_test" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a608d94641cc17fe203b102db2ae86d47a236630192f0244ddbbbb0044c0272" +dependencies = [ + "fs_extra", + "rusty-forkfork", + "sealed_test_derive", + "tempfile", +] + +[[package]] +name = "sealed_test_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b672e005ae58fef5da619d90b9f1c5b44b061890f4a371b3c96257a8a15e697" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "security-framework" version = "2.7.0" @@ -1084,18 +1141,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.147" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.147" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4" dependencies = [ "proc-macro2", "quote", @@ -1104,9 +1161,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.87" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" +checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ "itoa", "ryu", @@ -1172,15 +1229,29 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.37" @@ -1245,9 +1316,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.21.2" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" dependencies = [ "autocfg", "bytes", @@ -1260,14 +1331,14 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "winapi", + "windows-sys 0.42.0", ] [[package]] name = "tokio-macros" -version = "1.8.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", "quote", @@ -1299,6 +1370,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -1333,9 +1413,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "typenum" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "unicase" @@ -1390,6 +1470,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "waker-fn" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 680e7ad..8d90338 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,10 +27,12 @@ url = "2.3.1" rust-embed = { version = "6.4.2", features = ["include-exclude"] } libflate = "1.2.0" brotli = { version = "3.3.4", features = ["std"] } +toml = "0.5.9" once_cell = "1.16.0" [dev-dependencies] lipsum = "0.8.2" +sealed_test = "1.0.0" [profile.release] codegen-units = 1 diff --git a/README.md b/README.md index 5f3c647..7386272 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,13 @@ Assign a default value for each user-modifiable setting by passing environment v | `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` | | `AUTOPLAY_VIDEOS` | `["on", "off"]` | `off` | +You can also configure Libreddit with a configuration file. An example `libreddit.toml` can be found below: + +```toml +LIBREDDIT_DEFAULT_WIDE = "on" +LIBREDDIT_DEFAULT_USE_HLS = "on" +``` + ### Examples ```bash diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..c2d2055 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,130 @@ +use once_cell::sync::Lazy; +use std::{env::var, fs::read_to_string}; + +// Waiting for https://github.com/rust-lang/rust/issues/74465 to land, so we +// can reduce reliance on once_cell. +// +// This is the local static that is initialized at runtime (technically at +// first request) and contains the instance settings. +static CONFIG: Lazy = Lazy::new(Config::load); + +/// Stores the configuration parsed from the environment variables and the +/// config file. `Config::Default()` contains None for each setting. +#[derive(Default, serde::Deserialize)] +pub struct Config { + #[serde(rename = "LIBREDDIT_SFW_ONLY")] + sfw_only: Option, + + #[serde(rename = "LIBREDDIT_DEFAULT_THEME")] + default_theme: Option, + + #[serde(rename = "LIBREDDIT_DEFAULT_FRONT_PAGE")] + default_front_page: Option, + + #[serde(rename = "LIBREDDIT_DEFAULT_LAYOUT")] + default_layout: Option, + + #[serde(rename = "LIBREDDIT_DEFAULT_WIDE")] + default_wide: Option, + + #[serde(rename = "LIBREDDIT_DEFAULT_COMMENT_SORT")] + default_comment_sort: Option, + + #[serde(rename = "LIBREDDIT_DEFAULT_POST_SORT")] + default_post_sort: Option, + + #[serde(rename = "LIBREDDIT_DEFAULT_SHOW_NSFW")] + default_show_nsfw: Option, + + #[serde(rename = "LIBREDDIT_DEFAULT_BLUR_NSFW")] + default_blur_nsfw: Option, + + #[serde(rename = "LIBREDDIT_DEFAULT_USE_HLS")] + default_use_hls: Option, + + #[serde(rename = "LIBREDDIT_DEFAULT_HIDE_HLS_NOTIFICATION")] + default_hide_hls_notification: Option, +} + +impl Config { + /// Load the configuration from the environment variables and the config file. + /// In the case that there are no environment variables set and there is no + /// config file, this function returns a Config that contains all None values. + pub fn load() -> Self { + // Read from libreddit.toml config file. If for any reason, it fails, the + // default `Config` is used (all None values) + let config: Config = toml::from_str(&read_to_string("libreddit.toml").unwrap_or_default()).unwrap_or_default(); + // This function defines the order of preference - first check for + // environment variables with "LIBREDDIT", then check the config, then if + // both are `None`, return a `None` via the `map_or_else` function + let parse = |key: &str| -> Option { var(key).ok().map_or_else(|| get_setting_from_config(key, &config), Some) }; + Self { + sfw_only: parse("LIBREDDIT_SFW_ONLY"), + default_theme: parse("LIBREDDIT_DEFAULT_THEME"), + default_front_page: parse("LIBREDDIT_DEFAULT_FRONT_PAGE"), + default_layout: parse("LIBREDDIT_DEFAULT_LAYOUT"), + default_post_sort: parse("LIBREDDIT_DEFAULT_POST_SORT"), + default_wide: parse("LIBREDDIT_DEFAULT_WIDE"), + default_comment_sort: parse("LIBREDDIT_DEFAULT_COMMENT_SORT"), + default_show_nsfw: parse("LIBREDDIT_DEFAULT_SHOW_NSFW"), + default_blur_nsfw: parse("LIBREDDIT_DEFAULT_BLUR_NSFW"), + default_use_hls: parse("LIBREDDIT_DEFAULT_USE_HLS"), + default_hide_hls_notification: parse("LIBREDDIT_DEFAULT_HIDE_HLS"), + } + } +} + +fn get_setting_from_config(name: &str, config: &Config) -> Option { + match name { + "LIBREDDIT_SFW_ONLY" => config.sfw_only.clone(), + "LIBREDDIT_DEFAULT_THEME" => config.default_theme.clone(), + "LIBREDDIT_DEFAULT_FRONT_PAGE" => config.default_front_page.clone(), + "LIBREDDIT_DEFAULT_LAYOUT" => config.default_layout.clone(), + "LIBREDDIT_DEFAULT_COMMENT_SORT" => config.default_comment_sort.clone(), + "LIBREDDIT_DEFAULT_POST_SORT" => config.default_post_sort.clone(), + "LIBREDDIT_DEFAULT_SHOW_NSFW" => config.default_show_nsfw.clone(), + "LIBREDDIT_DEFAULT_BLUR_NSFW" => config.default_blur_nsfw.clone(), + "LIBREDDIT_DEFAULT_USE_HLS" => config.default_use_hls.clone(), + "LIBREDDIT_DEFAULT_HIDE_HLS_NOTIFICATION" => config.default_hide_hls_notification.clone(), + "LIBREDDIT_DEFAULT_WIDE" => config.default_wide.clone(), + _ => None, + } +} + +/// Retrieves setting from environment variable or config file. +pub(crate) fn get_setting(name: &str) -> Option { + get_setting_from_config(name, &CONFIG) +} + +#[cfg(test)] +use {sealed_test::prelude::*, std::fs::write}; + +#[test] +#[sealed_test(env = [("LIBREDDIT_SFW_ONLY", "1")])] +fn test_env_var() { + assert!(crate::utils::sfw_only()) +} + +#[test] +#[sealed_test] +fn test_config() { + let config_to_write = r#"LIBREDDIT_DEFAULT_COMMENT_SORT = "best""#; + write("libreddit.toml", config_to_write).unwrap(); + assert_eq!(get_setting("LIBREDDIT_DEFAULT_COMMENT_SORT"), Some("best".into())); +} + +#[test] +#[sealed_test(env = [("LIBREDDIT_DEFAULT_COMMENT_SORT", "top")])] +fn test_env_config_precedence() { + let config_to_write = r#"LIBREDDIT_DEFAULT_COMMENT_SORT = "best""#; + write("libreddit.toml", config_to_write).unwrap(); + assert_eq!(get_setting("LIBREDDIT_DEFAULT_COMMENT_SORT"), Some("top".into())) +} + +#[test] +#[sealed_test(env = [("LIBREDDIT_DEFAULT_COMMENT_SORT", "top")])] +fn test_alt_env_config_precedence() { + let config_to_write = r#"LIBREDDIT_DEFAULT_COMMENT_SORT = "best""#; + write("libreddit.toml", config_to_write).unwrap(); + assert_eq!(get_setting("LIBREDDIT_DEFAULT_COMMENT_SORT"), Some("top".into())) +} diff --git a/src/main.rs b/src/main.rs index 25c2aea..bffe99c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ #![allow(clippy::cmp_owned)] // Reference local files +mod config; mod duplicates; mod post; mod search; diff --git a/src/utils.rs b/src/utils.rs index fee97e9..8c8d273 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -680,8 +680,8 @@ pub fn setting(req: &Request, name: &str) -> String { req .cookie(name) .unwrap_or_else(|| { - // If there is no cookie for this setting, try receiving a default from an environment variable - if let Ok(default) = std::env::var(format!("LIBREDDIT_DEFAULT_{}", name.to_uppercase())) { + // If there is no cookie for this setting, try receiving a default from the config + if let Some(default) = crate::config::get_setting(&format!("LIBREDDIT_DEFAULT_{}", name.to_uppercase())) { Cookie::new(name, default) } else { Cookie::named(name) @@ -866,7 +866,7 @@ pub async fn error(req: Request, msg: impl ToString) -> Result, msg: impl ToString) -> Result bool { - match env::var("LIBREDDIT_SFW_ONLY") { - Ok(val) => val == "on", - Err(_) => false, + match crate::config::get_setting("LIBREDDIT_SFW_ONLY") { + Some(val) => val == "on", + None => false, } }