Compare commits

...

4 Commits

1 changed files with 148 additions and 23 deletions

View File

@ -145,6 +145,12 @@ mod conf {
/// example). See the definition of each of the member structs for more
/// information.
pub struct Config {
/// The "version" of the configuration, corresponding to the MAJOR in
/// semantic versioning. Should be increased every time the
/// configuration structure suffers breaking changes.
/// This value is optional because sufficiently old configuration files
/// may not have a version field.
pub version: Option<usize>,
/// Configuration regarding the Redis database.
pub db: DbConfig,
/// Configuration regarding the types of (URL shorten) slugs produced.
@ -153,6 +159,122 @@ mod conf {
pub serve_rules: ServeRules,
}
/// Get the configuration version field that this version of lonk expects.
pub fn config_version() -> usize {
usize::from_str(env!("CARGO_PKG_VERSION_MAJOR")).unwrap()
}
pub enum ConfigParseError {
SerdeError(serde_json::error::Error),
OldVersion(usize),
ServeFileNotFile(PathBuf),
ServeFileNotExists(PathBuf),
ServeDirNotDir(PathBuf),
ServeDirNotExists(PathBuf),
}
impl Config {
pub fn from_sync_buffer<R: std::io::Read>(
buffer: std::io::BufReader<R>,
) -> Result<Self, ConfigParseError> {
let parsed: Config =
serde_json::from_reader(buffer).map_err(|err| ConfigParseError::SerdeError(err))?;
parsed.validate()
}
fn validate(self) -> Result<Self, ConfigParseError> {
// Check configuration version
let parsed_version = self.version.unwrap_or(0);
if parsed_version != config_version() {
return Err(ConfigParseError::OldVersion(parsed_version));
}
// Check existence of serve file or directory
match &self.serve_rules.dir {
ServeDirRules::File(file) => {
if !file.exists() {
return Err(ConfigParseError::ServeFileNotExists(file.clone()));
}
if !file.is_file() {
return Err(ConfigParseError::ServeFileNotFile(file.clone()));
}
}
ServeDirRules::Dir(dir) => {
if !dir.exists() {
return Err(ConfigParseError::ServeDirNotExists(dir.clone()));
}
if !dir.is_dir() {
return Err(ConfigParseError::ServeDirNotDir(dir.clone()));
}
}
}
Ok(self)
}
}
impl ConfigParseError {
pub fn panic_with_message(self, config_file_name: &str) -> ! {
match self {
ConfigParseError::SerdeError(err) => match err.classify() {
serde_json::error::Category::Io => {
panic!("IO error when reading configuration file.")
}
serde_json::error::Category::Syntax => panic!(
"Configuration file is syntactically incorrect.
See {}:line {}, column {}.",
config_file_name,
err.line(),
err.column()
),
serde_json::error::Category::Data => panic!(
"Error deserializing configuration file; expected different data type.
See {}:line {}, column {}.",
config_file_name,
err.line(),
err.column()
),
serde_json::error::Category::Eof => {
panic!("Unexpected end of file when reading configuration file.")
}
},
ConfigParseError::OldVersion(old_version) => {
panic!(
"Configuration file has outdated version.
Expected version field to be {} but got {}.",
old_version,
config_version()
);
}
ConfigParseError::ServeDirNotExists(dir) => {
panic!(
"Configuration file indicates directory {} should be served, but it does not exist.",
dir.to_string_lossy()
)
}
ConfigParseError::ServeDirNotDir(dir) => {
panic!(
"Configuration file indicates directory {} should be served, but it is not a directory.",
dir.to_string_lossy()
)
}
ConfigParseError::ServeFileNotExists(file) => {
panic!(
"Configuration file indicates file {} should be served, but it does not exist.",
file.to_string_lossy()
)
}
ConfigParseError::ServeFileNotFile(file) => {
panic!(
"Configuration file indicates file {} should be served, but it is not a file.",
file.to_string_lossy()
)
}
}
}
}
// Default implementations
impl Default for DbConfig {
@ -185,7 +307,7 @@ mod conf {
impl Default for ServeDirRules {
fn default() -> Self {
ServeDirRules::Dir(PathBuf::from_str("/etc/lonk/served").unwrap())
ServeDirRules::Dir("/etc/lonk/served".into())
}
}
@ -210,6 +332,7 @@ mod conf {
impl Default for Config {
fn default() -> Self {
Self {
version: Some(config_version()),
db: Default::default(),
slug_rules: Default::default(),
serve_rules: Default::default(),
@ -225,6 +348,7 @@ mod service {
#[derive(Validator)]
#[validator(http_url(local(NotAllow)))]
#[derive(Clone, Debug)]
#[allow(dead_code)]
/// A struct representing a URL.
pub struct HttpUrl {
url: validators::url::Url,
@ -233,6 +357,7 @@ mod service {
#[derive(Validator)]
#[validator(domain(ipv4(Allow), local(NotAllow), at_least_two_labels(Must), port(Allow)))]
#[allow(dead_code)]
pub struct Domain {
domain: String,
port: Option<u16>,
@ -822,27 +947,16 @@ async fn serve() {
),
};
});
let config_buf = std::io::BufReader::new(config_file);
serde_json::from_reader(config_buf).unwrap_or_else(|err| match err.classify() {
serde_json::error::Category::Io => panic!("IO error when reading configuration file."),
serde_json::error::Category::Syntax => panic!(
"Configuration file is syntactically incorrect.
See {}:line {}, column {}.",
&config_file_name,
err.line(),
err.column()
),
serde_json::error::Category::Data => panic!(
"Error deserializing configuration file; expected different data type.
See {}:line {}, column {}.",
&config_file_name,
err.line(),
err.column()
),
serde_json::error::Category::Eof => {
panic!("Unexpected end of file when reading configuration file.")
}
let parse_result = tokio::task::spawn_blocking(move || {
conf::Config::from_sync_buffer(std::io::BufReader::new(config_file))
})
.await
.expect("Tokio error from blocking task.");
match parse_result {
Err(err) => err.panic_with_message(&config_file_name),
Ok(config) => config,
}
};
// Create slug factory
@ -918,21 +1032,32 @@ async fn serve() {
#[derive(FromArgs, PartialEq, Debug)]
/// Start lonk.
struct Run {
/// print the version and quit
#[argh(switch)]
version: bool,
/// write a default configuration to stdout and quit
#[argh(switch)]
print_default_config: bool,
}
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
fn main() {
let run = argh::from_env::<Run>();
if run.version {
println!("lonk v{}", VERSION);
std::process::exit(0);
}
if run.print_default_config {
println!(
"{}",
serde_json::to_string_pretty(&conf::Config::default())
.expect("Default configuration should always be JSON serializable")
);
} else {
serve();
std::process::exit(0);
}
serve();
}