Cargo fmt and making clippy happy

This commit is contained in:
Karcsesz 2024-02-14 19:48:08 +01:00
parent e7d2312a18
commit e9fbe574e4
15 changed files with 277 additions and 166 deletions

View file

@ -1,6 +1,6 @@
use clap::{Args, Parser, Subcommand, ValueEnum};
use std::net::IpAddr; use std::net::IpAddr;
use std::path::PathBuf; use std::path::PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum};
use tracing::Level; use tracing::Level;
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
@ -11,7 +11,7 @@ pub struct MainCommand {
#[command(flatten)] #[command(flatten)]
pub data_paths: DataPaths, pub data_paths: DataPaths,
#[command(subcommand)] #[command(subcommand)]
pub run_mode: Command pub run_mode: Command,
} }
impl MainCommand { impl MainCommand {
pub fn log_level(&self) -> Level { pub fn log_level(&self) -> Level {
@ -20,7 +20,7 @@ impl MainCommand {
LoggingLevel::Debug => Level::DEBUG, LoggingLevel::Debug => Level::DEBUG,
LoggingLevel::Info => Level::INFO, LoggingLevel::Info => Level::INFO,
LoggingLevel::Warning => Level::WARN, LoggingLevel::Warning => Level::WARN,
LoggingLevel::Error => Level::ERROR LoggingLevel::Error => Level::ERROR,
} }
} }
} }
@ -33,14 +33,13 @@ pub struct DataPaths {
pub pid_file_path: PathBuf, pub pid_file_path: PathBuf,
} }
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
pub enum LoggingLevel { pub enum LoggingLevel {
Trace, Trace,
Debug, Debug,
Info, Info,
Warning, Warning,
Error Error,
} }
#[derive(Debug, Clone, Args)] #[derive(Debug, Clone, Args)]
@ -56,7 +55,7 @@ pub struct ServerParameters {
pub force_pid: bool, pub force_pid: bool,
/// Limits how many rel parameters will be processed in a single query /// Limits how many rel parameters will be processed in a single query
#[arg(long, default_value = "10")] #[arg(long, default_value = "10")]
pub max_allowed_rels_in_request: usize pub max_allowed_rels_in_request: usize,
} }
#[derive(Debug, Args)] #[derive(Debug, Args)]
@ -73,7 +72,7 @@ pub struct SaveSettings {
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]
pub enum Command { pub enum Command {
/// Serve the database /// Serve the database
Serve (ServerParameters), Serve(ServerParameters),
/// Fetch one or multiple handles from Fediverse-compatible software /// Fetch one or multiple handles from Fediverse-compatible software
Fetch { Fetch {
#[command(flatten)] #[command(flatten)]
@ -92,7 +91,7 @@ pub enum Command {
/// Runs a single query against the database and returns the Resource associated with the value /// Runs a single query against the database and returns the Resource associated with the value
Query { Query {
/// The resource to query for /// The resource to query for
resource: String resource: String,
}, },
/// Open the resource in your system editor /// Open the resource in your system editor
Editor { Editor {
@ -102,7 +101,7 @@ pub enum Command {
resource: String, resource: String,
#[command(flatten)] #[command(flatten)]
server_reload: ServerReloadOptions, server_reload: ServerReloadOptions,
} },
} }
#[derive(ValueEnum, Debug, Eq, PartialEq, Copy, Clone)] #[derive(ValueEnum, Debug, Eq, PartialEq, Copy, Clone)]
@ -115,8 +114,7 @@ pub enum CollisionHandling {
/// Only overwrites if there's just a single resource the new item collides with /// Only overwrites if there's just a single resource the new item collides with
OverwriteSingleSkipMultiple, OverwriteSingleSkipMultiple,
/// Overwrites every already existing resource that the new item collides with /// Overwrites every already existing resource that the new item collides with
OverwriteMultiple OverwriteMultiple, //TODO: Handlers to remove only the offending aliases, not the whole record?
//TODO: Handlers to remove only the offending aliases, not the whole record?
} }
#[derive(Args, Debug)] #[derive(Args, Debug)]

View file

@ -1,3 +1,3 @@
pub mod editor;
pub mod fetch; pub mod fetch;
pub mod query; pub mod query;
pub mod editor;

View file

@ -1,11 +1,12 @@
use std::path::PathBuf;
use crate::args_parser::SaveSettings; use crate::args_parser::SaveSettings;
use crate::editor::open_in_editor::open_resource_in_editor; use crate::editor::open_in_editor::open_resource_in_editor;
use crate::schema::lookup_handler::LookupHandler; use crate::schema::lookup_handler::LookupHandler;
use std::path::PathBuf;
pub fn editor(database_path: PathBuf, save_settings: SaveSettings, resource: String) { pub fn editor(database_path: PathBuf, save_settings: SaveSettings, resource: String) {
let resources = LookupHandler::load(&database_path).unwrap(); let resources = LookupHandler::load(&database_path).unwrap();
let (index, resource) = resources.lookup_with_index(resource.as_str()) let (index, resource) = resources
.lookup_with_index(resource.as_str())
.expect("Couldn't find a resource for that query"); .expect("Couldn't find a resource for that query");
let resource = open_resource_in_editor(resource).unwrap(); let resource = open_resource_in_editor(resource).unwrap();
if save_settings.save { if save_settings.save {

View file

@ -1,12 +1,18 @@
use crate::args_parser::SaveSettings;
use crate::editor::finger_remote::finger_many_fedi;
use crate::schema::resource_list::ResourceList;
use std::io::{BufRead, BufReader}; use std::io::{BufRead, BufReader};
use std::path::PathBuf; use std::path::PathBuf;
use tracing::{debug, error, info, instrument, trace, warn}; use tracing::{debug, error, info, instrument, trace, warn};
use crate::args_parser::{DataPaths, SaveSettings};
use crate::editor::finger_remote::finger_many_fedi;
use crate::schema::resource_list::ResourceList;
#[instrument(skip_all)] #[instrument(skip_all)]
pub fn fetch(database_path: PathBuf, save_settings: SaveSettings, handles: impl Iterator<Item = String>, handles_are_files: bool, new_domain: Option<String>) { pub fn fetch(
database_path: PathBuf,
save_settings: SaveSettings,
handles: impl Iterator<Item = String>,
handles_are_files: bool,
new_domain: Option<String>,
) {
let handles = handles.flat_map(|handle| { let handles = handles.flat_map(|handle| {
if handles_are_files { if handles_are_files {
match std::fs::File::open(&handle) { match std::fs::File::open(&handle) {
@ -26,7 +32,9 @@ pub fn fetch(database_path: PathBuf, save_settings: SaveSettings, handles: impl
vec![] vec![]
} }
} }
} else {vec![handle]} } else {
vec![handle]
}
}); });
let new_resources = finger_many_fedi(handles).map(|mut new_resource| { let new_resources = finger_many_fedi(handles).map(|mut new_resource| {
info!("Fetched {}", new_resource.subject); info!("Fetched {}", new_resource.subject);
@ -36,15 +44,17 @@ pub fn fetch(database_path: PathBuf, save_settings: SaveSettings, handles: impl
let (start, old_domain) = match current_subject.rsplit_once('@') { let (start, old_domain) = match current_subject.rsplit_once('@') {
None => { None => {
warn!("Failed to parse the domain of {current_subject}, storing as-is."); warn!("Failed to parse the domain of {current_subject}, storing as-is.");
return new_resource return new_resource;
}, }
Some(data) => data Some(data) => data,
}; };
debug!("Replacing {old_domain} with {new_domain}"); debug!("Replacing {old_domain} with {new_domain}");
new_resource.add_new_primary_subject(format!("{start}@{new_domain}")); new_resource.add_new_primary_subject(format!("{start}@{new_domain}"));
trace!("{new_resource:?}"); trace!("{new_resource:?}");
new_resource new_resource
} else { new_resource } } else {
new_resource
}
}); });
if save_settings.save { if save_settings.save {
let mut resource_list = ResourceList::load(&database_path).unwrap(); let mut resource_list = ResourceList::load(&database_path).unwrap();

View file

@ -1,6 +1,6 @@
use crate::schema::lookup_handler::LookupHandler;
use std::io::stdout; use std::io::stdout;
use std::path::PathBuf; use std::path::PathBuf;
use crate::schema::lookup_handler::LookupHandler;
pub fn query(database_path: PathBuf, handle: String) { pub fn query(database_path: PathBuf, handle: String) {
let data = LookupHandler::load(database_path).unwrap(); let data = LookupHandler::load(database_path).unwrap();

View file

@ -1,10 +1,10 @@
use std::fmt::Display;
use reqwest::blocking::Client;
use reqwest::Url;
use thiserror::Error;
use tracing::{debug, error, instrument};
use crate::editor::finger_remote::FediFingerError::UrlBuildFailed; use crate::editor::finger_remote::FediFingerError::UrlBuildFailed;
use crate::schema::resource::Resource; use crate::schema::resource::Resource;
use reqwest::blocking::Client;
use reqwest::Url;
use std::fmt::Display;
use thiserror::Error;
use tracing::{debug, error, instrument};
/// Error type returned by `finger_url` /// Error type returned by `finger_url`
#[derive(Debug, Error)] #[derive(Debug, Error)]
@ -23,8 +23,7 @@ pub enum FingerError {
pub fn finger_url(client: &Client, url: Url) -> Result<Resource, FingerError> { pub fn finger_url(client: &Client, url: Url) -> Result<Resource, FingerError> {
use FingerError::*; use FingerError::*;
debug!("Fingering {url}..."); debug!("Fingering {url}...");
let request = client.get(url) let request = client.get(url).build().map_err(RequestBuildFailed)?;
.build().map_err(RequestBuildFailed)?;
let response = client.execute(request).map_err(RequestFailed)?; let response = client.execute(request).map_err(RequestFailed)?;
debug!("Received response: {response:?}"); debug!("Received response: {response:?}");
let response = response.json::<Resource>().map_err(ParseFailed)?; let response = response.json::<Resource>().map_err(ParseFailed)?;
@ -40,7 +39,7 @@ pub enum FediFingerError {
#[error("Failed to build request URL")] #[error("Failed to build request URL")]
UrlBuildFailed, UrlBuildFailed,
#[error("Failed to run WebFinger request: {0}")] #[error("Failed to run WebFinger request: {0}")]
FingerFailed(#[from] FingerError) FingerFailed(#[from] FingerError),
} }
/// Finger a Fediverse compatible handle /// Finger a Fediverse compatible handle
@ -49,27 +48,32 @@ pub enum FediFingerError {
#[instrument(skip(client))] #[instrument(skip(client))]
pub fn finger_fedi(client: &Client, handle: &str) -> Result<Resource, FediFingerError> { pub fn finger_fedi(client: &Client, handle: &str) -> Result<Resource, FediFingerError> {
let handle = handle.strip_prefix('@').unwrap_or(handle); let handle = handle.strip_prefix('@').unwrap_or(handle);
let (_account, domain) = handle.split_once('@').ok_or(FediFingerError::MissingMiddleAt)?; let (_account, domain) = handle
.split_once('@')
.ok_or(FediFingerError::MissingMiddleAt)?;
let account = format!("acct:{handle}"); let account = format!("acct:{handle}");
let url = Url::parse(format!("https://{domain}/.well-known/webfinger?resource={account}").as_str()).map_err(|_e| UrlBuildFailed)?; let url =
Url::parse(format!("https://{domain}/.well-known/webfinger?resource={account}").as_str())
.map_err(|_e| UrlBuildFailed)?;
Ok(finger_url(client, url)?) Ok(finger_url(client, url)?)
} }
/// Finger multiple Fediverse compatible handles /// Finger multiple Fediverse compatible handles
/// ///
/// Runs `finger_fedi` on the provided list of handles. If any requests fail, prints an error and continues. /// Runs `finger_fedi` on the provided list of handles. If any requests fail, prints an error and continues.
pub fn finger_many_fedi(handles: impl Iterator<Item = impl AsRef<str> + Display>) -> impl Iterator<Item = Resource> { pub fn finger_many_fedi(
handles: impl Iterator<Item = impl AsRef<str> + Display>,
) -> impl Iterator<Item = Resource> {
let client = Client::builder() let client = Client::builder()
.user_agent(concat!("Fingerlink/", env!("CARGO_PKG_VERSION"))) .user_agent(concat!("Fingerlink/", env!("CARGO_PKG_VERSION")))
.build().unwrap(); //Safety: setting user_agent can't cause an error. .build()
handles.filter_map(move |handle| { .unwrap(); //Safety: setting user_agent can't cause an error.
match finger_fedi(&client, handle.as_ref()) { handles.filter_map(move |handle| match finger_fedi(&client, handle.as_ref()) {
Err(e) => { Err(e) => {
error!("Failed to finger {handle}: {e}"); error!("Failed to finger {handle}: {e}");
None None
} }
Ok(data) => Some(data) Ok(data) => Some(data),
}
}) })
} }

View file

@ -1,4 +1,4 @@
pub mod commands;
pub mod finger_remote; pub mod finger_remote;
pub mod open_in_editor; pub mod open_in_editor;
pub mod try_reload_server; pub mod try_reload_server;
pub mod commands;

View file

@ -1,10 +1,10 @@
use crate::schema::resource::Resource;
use std::fmt::{Debug, Display}; use std::fmt::{Debug, Display};
use std::io::{Read, Seek, SeekFrom, Write}; use std::io::{Read, Seek, SeekFrom, Write};
use std::process::Command; use std::process::Command;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use thiserror::Error; use thiserror::Error;
use tracing::{debug, instrument, trace}; use tracing::{debug, instrument, trace};
use crate::schema::resource::Resource;
/// Error type returned by `spawn_editor` /// Error type returned by `spawn_editor`
#[derive(Debug, Error)] #[derive(Debug, Error)]
@ -40,23 +40,28 @@ pub fn spawn_editor(buffer: impl AsRef<str> + Display) -> Result<String, EditorS
debug!("$EDITOR's full path is {editor:?}"); debug!("$EDITOR's full path is {editor:?}");
let mut temp_file = NamedTempFile::new().map_err(TempFileCreationFailed)?; let mut temp_file = NamedTempFile::new().map_err(TempFileCreationFailed)?;
debug!("Created temporary file at {temp_file:?}"); debug!("Created temporary file at {temp_file:?}");
temp_file.write_all(buffer.as_ref().as_bytes()).map_err(BufferWriteFailed)?; temp_file
.write_all(buffer.as_ref().as_bytes())
.map_err(BufferWriteFailed)?;
debug!("Written buffer"); debug!("Written buffer");
let mut editor = Command::new(editor); let mut editor = Command::new(editor);
let editor = editor let editor = editor.args([temp_file.path().as_os_str()]);
.args([temp_file.path().as_os_str()]);
debug!("Spawning editor with command {editor:?}"); debug!("Spawning editor with command {editor:?}");
let mut editor = editor.spawn().map_err(EditorSpawnFailed)?; let mut editor = editor.spawn().map_err(EditorSpawnFailed)?;
let editor = editor.wait().map_err(EditorWaitFailed)?; let editor = editor.wait().map_err(EditorWaitFailed)?;
debug!("Editor closed with {editor:?}"); debug!("Editor closed with {editor:?}");
match editor.code() { match editor.code() {
Some(0) => {/*All good*/} Some(0) => { /*All good*/ }
None | Some(_) => {return Err(EditorExitCode)} None | Some(_) => return Err(EditorExitCode),
} }
temp_file.seek(SeekFrom::Start(0)).map_err(BufferSeekFailed)?; temp_file
.seek(SeekFrom::Start(0))
.map_err(BufferSeekFailed)?;
let mut buffer = Default::default(); let mut buffer = Default::default();
temp_file.read_to_string(&mut buffer).map_err(BufferReadbackFailed)?; temp_file
.read_to_string(&mut buffer)
.map_err(BufferReadbackFailed)?;
trace!("Read back buffer: {buffer}"); trace!("Read back buffer: {buffer}");
Ok(buffer) Ok(buffer)
} }
@ -69,7 +74,7 @@ pub enum ResourceEditingError {
#[error("Downstream editor spawn failed: {0}")] #[error("Downstream editor spawn failed: {0}")]
EditorSpawnError(#[from] EditorSpawnError), EditorSpawnError(#[from] EditorSpawnError),
#[error("Failed to parse edited JSON")] #[error("Failed to parse edited JSON")]
ParseFailed(serde_json::Error) ParseFailed(serde_json::Error),
} }
/// Opens the provided `resource` in the system editor and returns the edited version or an error if something goes wrong. /// Opens the provided `resource` in the system editor and returns the edited version or an error if something goes wrong.

View file

@ -1,11 +1,11 @@
use std::path::{Path, PathBuf}; use crate::args_parser::ServerReloadOptions;
use std::io::Read;
use std::str::FromStr;
use nix::sys::signal::{kill, Signal}; use nix::sys::signal::{kill, Signal};
use nix::unistd::Pid; use nix::unistd::Pid;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use thiserror::Error; use thiserror::Error;
use tracing::{error, info}; use tracing::{error, info};
use crate::args_parser::ServerReloadOptions;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum ServerReloadError { pub enum ServerReloadError {
@ -24,11 +24,12 @@ pub enum ServerReloadError {
fn load_pid(pid_file_path: &Path) -> Result<Pid, ServerReloadError> { fn load_pid(pid_file_path: &Path) -> Result<Pid, ServerReloadError> {
use ServerReloadError::*; use ServerReloadError::*;
if !pid_file_path.exists() { if !pid_file_path.exists() {
return Err(PIDFileNotFound(pid_file_path.to_string_lossy().to_string())) return Err(PIDFileNotFound(pid_file_path.to_string_lossy().to_string()));
} }
let mut file = std::fs::File::open(pid_file_path).map_err(FailedToOpenPIDFile)?; let mut file = std::fs::File::open(pid_file_path).map_err(FailedToOpenPIDFile)?;
let mut buffer = Default::default(); let mut buffer = Default::default();
file.read_to_string(&mut buffer).map_err(FailedToReadPIDFile)?; file.read_to_string(&mut buffer)
.map_err(FailedToReadPIDFile)?;
let pid = FromStr::from_str(buffer.trim())?; let pid = FromStr::from_str(buffer.trim())?;
Ok(Pid::from_raw(pid)) Ok(Pid::from_raw(pid))
} }
@ -43,8 +44,12 @@ pub fn reload(pid_file_path: PathBuf, options: ServerReloadOptions) {
if options.reload_server { if options.reload_server {
info!("Attempting to reload server..."); info!("Attempting to reload server...");
match try_reload_server(pid_file_path.as_path()) { match try_reload_server(pid_file_path.as_path()) {
Ok(_) => {info!("Server reloading!")} Ok(_) => {
Err(e) => {error!("Failed to reload server: {e}")} info!("Server reloading!")
}
Err(e) => {
error!("Failed to reload server: {e}")
}
} }
} }
} }

View file

@ -1,20 +1,17 @@
use clap::{Parser};
use tracing::info;
use tracing_subscriber::util::SubscriberInitExt;
use crate::args_parser::Command; use crate::args_parser::Command;
use crate::editor::open_in_editor::open_resource_in_editor;
use editor::commands::fetch::fetch;
use editor::commands::query::query;
use crate::editor::commands::editor::editor; use crate::editor::commands::editor::editor;
use crate::editor::try_reload_server::reload; use crate::editor::try_reload_server::reload;
use crate::schema::lookup_handler::LookupHandler; use clap::Parser;
use editor::commands::fetch::fetch;
use editor::commands::query::query;
use tracing_subscriber::util::SubscriberInitExt;
mod schema; mod args_parser;
#[cfg(feature = "editor")] #[cfg(feature = "editor")]
mod editor; mod editor;
mod schema;
#[cfg(feature = "server")] #[cfg(feature = "server")]
mod server; mod server;
mod args_parser;
#[cfg(all(not(feature = "editor"), not(feature = "server")))] #[cfg(all(not(feature = "editor"), not(feature = "server")))]
compile_error!("Please enable either the \"editor\" or the \"server\" feature"); compile_error!("Please enable either the \"editor\" or the \"server\" feature");
@ -23,8 +20,13 @@ fn main() {
let args = args_parser::MainCommand::parse(); let args = args_parser::MainCommand::parse();
tracing_subscriber::FmtSubscriber::builder() tracing_subscriber::FmtSubscriber::builder()
.with_max_level(args.log_level()) .with_max_level(args.log_level())
.finish().init(); .finish()
let args_parser::MainCommand { data_paths, run_mode, .. } = args; .init();
let args_parser::MainCommand {
data_paths,
run_mode,
..
} = args;
match run_mode { match run_mode {
Command::Serve(params) => { Command::Serve(params) => {
#[cfg(not(feature = "server"))] #[cfg(not(feature = "server"))]
@ -34,14 +36,28 @@ fn main() {
#[cfg(feature = "server")] #[cfg(feature = "server")]
server::init(data_paths, params); server::init(data_paths, params);
} }
Command::Fetch { save, handles, handles_are_files, new_domain, server_reload } => { Command::Fetch {
fetch(data_paths.database_path, save, handles.into_iter(), handles_are_files, new_domain); save,
handles,
handles_are_files,
new_domain,
server_reload,
} => {
fetch(
data_paths.database_path,
save,
handles.into_iter(),
handles_are_files,
new_domain,
);
reload(data_paths.pid_file_path, server_reload); reload(data_paths.pid_file_path, server_reload);
} }
Command::Query { resource } => { Command::Query { resource } => query(data_paths.database_path, resource),
query(data_paths.database_path, resource) Command::Editor {
} save,
Command::Editor { save, resource, server_reload } => { resource,
server_reload,
} => {
editor(data_paths.database_path, save, resource); editor(data_paths.database_path, save, resource);
reload(data_paths.pid_file_path, server_reload); reload(data_paths.pid_file_path, server_reload);

View file

@ -1,10 +1,10 @@
use std::collections::HashMap;
use std::fmt::Debug;
use tracing::{debug, info, instrument};
use std::path::Path;
use thiserror::Error;
use crate::schema::resource::Resource; use crate::schema::resource::Resource;
use crate::schema::resource_list::{ResourceList, ResourceLoadError}; use crate::schema::resource_list::{ResourceList, ResourceLoadError};
use std::collections::HashMap;
use std::fmt::Debug;
use std::path::Path;
use thiserror::Error;
use tracing::{debug, info, instrument};
/// Handles looking up resources based on the ?resource={} URL parameter /// Handles looking up resources based on the ?resource={} URL parameter
#[derive(Debug)] #[derive(Debug)]
@ -30,6 +30,8 @@ impl LookupHandler {
pub fn load(path: impl AsRef<Path> + Debug) -> Result<Self, DataLoadError> { pub fn load(path: impl AsRef<Path> + Debug) -> Result<Self, DataLoadError> {
Self::build_from_resource_list(ResourceList::load(path)?) Self::build_from_resource_list(ResourceList::load(path)?)
} }
#[cfg(test)]
fn load_from_reader(reader: impl std::io::Read) -> Result<Self, DataLoadError> { fn load_from_reader(reader: impl std::io::Read) -> Result<Self, DataLoadError> {
Self::build_from_resource_list(ResourceList::load_from_reader(reader)?) Self::build_from_resource_list(ResourceList::load_from_reader(reader)?)
} }
@ -56,9 +58,10 @@ impl LookupHandler {
Ok(LookupHandler { resources, lookup }) Ok(LookupHandler { resources, lookup })
} }
#[instrument(level="debug")] #[instrument(level = "debug")]
pub fn lookup(&self, resource_query: &str) -> Option<&Resource> { pub fn lookup(&self, resource_query: &str) -> Option<&Resource> {
self.lookup_with_index(resource_query).map(|(_index, resource)| resource) self.lookup_with_index(resource_query)
.map(|(_index, resource)| resource)
} }
pub fn lookup_with_index(&self, resource_query: &str) -> Option<(usize, &Resource)> { pub fn lookup_with_index(&self, resource_query: &str) -> Option<(usize, &Resource)> {
@ -92,7 +95,11 @@ mod tests {
fn duplicate_subject() { fn duplicate_subject() {
let duplicated = "[{\"subject\": \"testing\"},{\"subject\": \"testing\"}]".as_bytes(); let duplicated = "[{\"subject\": \"testing\"},{\"subject\": \"testing\"}]".as_bytes();
let result = LookupHandler::load_from_reader(duplicated); let result = LookupHandler::load_from_reader(duplicated);
if let Err(DataLoadError::DuplicateLookupFound { duplicated, subjects }) = result { if let Err(DataLoadError::DuplicateLookupFound {
duplicated,
subjects,
}) = result
{
assert_eq!(duplicated, "testing".to_string()); assert_eq!(duplicated, "testing".to_string());
assert_eq!(subjects, ["testing".to_string(), "testing".to_string()]); assert_eq!(subjects, ["testing".to_string(), "testing".to_string()]);
} else { } else {
@ -104,10 +111,17 @@ mod tests {
fn duplicate_alias() { fn duplicate_alias() {
let duplicated = "[{\"subject\": \"testing\"},{\"subject\": \"more_testing\", \"aliases\": [\"testing\"]}]".as_bytes(); let duplicated = "[{\"subject\": \"testing\"},{\"subject\": \"more_testing\", \"aliases\": [\"testing\"]}]".as_bytes();
let result = LookupHandler::load_from_reader(duplicated); let result = LookupHandler::load_from_reader(duplicated);
if let Err(DataLoadError::DuplicateLookupFound { duplicated, mut subjects }) = result { if let Err(DataLoadError::DuplicateLookupFound {
duplicated,
mut subjects,
}) = result
{
assert_eq!(duplicated, "testing".to_string()); assert_eq!(duplicated, "testing".to_string());
subjects.sort(); // Because we don't care about order for testing purposes subjects.sort(); // Because we don't care about order for testing purposes
assert_eq!(subjects, ["more_testing".to_string(), "testing".to_string()]); assert_eq!(
subjects,
["more_testing".to_string(), "testing".to_string()]
);
} else { } else {
panic!("LookupHandler::load() returned {result:?}"); panic!("LookupHandler::load() returned {result:?}");
}; };
@ -118,12 +132,15 @@ mod tests {
let data = "[{\"subject\":\"testing\"},{\"subject\":\"more_testing\"}]".as_bytes(); let data = "[{\"subject\":\"testing\"},{\"subject\":\"more_testing\"}]".as_bytes();
let data = LookupHandler::load_from_reader(data).unwrap(); let data = LookupHandler::load_from_reader(data).unwrap();
for subject in ["testing", "more_testing"] { for subject in ["testing", "more_testing"] {
assert_eq!(data.lookup(subject), Some(&Resource { assert_eq!(
data.lookup(subject),
Some(&Resource {
subject: subject.to_string(), subject: subject.to_string(),
aliases: None, aliases: None,
properties: None, properties: None,
links: None, links: None,
})); })
);
} }
} }
@ -132,12 +149,15 @@ mod tests {
let data = "[{\"subject\":\"testing\",\"aliases\":[\"alias1\",\"alias2\"]},{\"subject\":\"red herring\",\"aliases\":[\"alias\",\"1\", \"2\"]}]".as_bytes(); let data = "[{\"subject\":\"testing\",\"aliases\":[\"alias1\",\"alias2\"]},{\"subject\":\"red herring\",\"aliases\":[\"alias\",\"1\", \"2\"]}]".as_bytes();
let data = LookupHandler::load_from_reader(data).unwrap(); let data = LookupHandler::load_from_reader(data).unwrap();
for subject in ["alias1", "alias2"] { for subject in ["alias1", "alias2"] {
assert_eq!(data.lookup(subject), Some(&Resource { assert_eq!(
data.lookup(subject),
Some(&Resource {
subject: "testing".to_string(), subject: "testing".to_string(),
aliases: Some(vec!["alias1".to_string(), "alias2".to_string()]), aliases: Some(vec!["alias1".to_string(), "alias2".to_string()]),
properties: None, properties: None,
links: None, links: None,
})); })
);
} }
} }
} }

View file

@ -1,3 +1,3 @@
pub mod resource;
pub mod lookup_handler; pub mod lookup_handler;
pub mod resource;
pub mod resource_list; pub mod resource_list;

View file

@ -81,16 +81,13 @@ pub mod test_data {
pub fn user_with_single_alias() -> Resource { pub fn user_with_single_alias() -> Resource {
Resource { Resource {
subject: "acct:user@domain.tld".to_string(), subject: "acct:user@domain.tld".to_string(),
aliases: Some(vec![ aliases: Some(vec!["https://domain.tld/@user".to_string()]),
"https://domain.tld/@user".to_string()
]),
properties: None, properties: None,
links: None, links: None,
} }
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -99,12 +96,14 @@ mod tests {
fn insert_new_primary_subject_into_barebones_user() { fn insert_new_primary_subject_into_barebones_user() {
let mut data = test_data::barebones_user(); let mut data = test_data::barebones_user();
data.add_new_primary_subject("username".to_string()); data.add_new_primary_subject("username".to_string());
assert_eq!(data, Resource { assert_eq!(
data,
Resource {
subject: "username".to_string(), subject: "username".to_string(),
aliases: Some(vec![test_data::barebones_user().subject]), aliases: Some(vec![test_data::barebones_user().subject]),
properties: None, properties: None,
links: None, links: None,
}) }
)
} }
} }

View file

@ -1,10 +1,10 @@
use std::collections::HashSet;
use tracing::{debug, info, instrument, trace, warn};
use std::path::Path;
use std::fmt::Debug;
use thiserror::Error;
use crate::args_parser::CollisionHandling; use crate::args_parser::CollisionHandling;
use crate::schema::resource::Resource; use crate::schema::resource::Resource;
use std::collections::HashSet;
use std::fmt::Debug;
use std::path::Path;
use thiserror::Error;
use tracing::{debug, info, instrument, trace, warn};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ResourceList(pub Vec<Resource>); pub struct ResourceList(pub Vec<Resource>);
@ -65,27 +65,39 @@ impl ResourceList {
/// Merges `new_records` into the `ResourceList` with lookup collisions being handled as defined in `collision_handling` /// Merges `new_records` into the `ResourceList` with lookup collisions being handled as defined in `collision_handling`
#[instrument(level = "debug", skip(self, new_records))] #[instrument(level = "debug", skip(self, new_records))]
pub fn merge_records(&mut self, new_records: impl Iterator<Item = Resource>, collision_handling: CollisionHandling) -> &ResourceList { pub fn merge_records(
&mut self,
new_records: impl Iterator<Item = Resource>,
collision_handling: CollisionHandling,
) -> &ResourceList {
debug!("Building hashset of already taken queries..."); debug!("Building hashset of already taken queries...");
let unique_check: HashSet<String> = HashSet::from_iter(self.0.iter().flat_map(Resource::keys).cloned()); let unique_check: HashSet<String> =
HashSet::from_iter(self.0.iter().flat_map(Resource::keys).cloned());
for record in new_records { for record in new_records {
let record_keys = HashSet::from_iter(record.keys().cloned()); let record_keys = HashSet::from_iter(record.keys().cloned());
let collisions = unique_check.intersection(&record_keys).collect::<HashSet<_>>(); let collisions = unique_check
.intersection(&record_keys)
.collect::<HashSet<_>>();
if !collisions.is_empty() { if !collisions.is_empty() {
warn!("Resource collision detected with {}: {collisions:?}", record.subject); warn!(
"Resource collision detected with {}: {collisions:?}",
record.subject
);
match collision_handling { match collision_handling {
CollisionHandling::Skip => { CollisionHandling::Skip => {
warn!("Skipping record..."); warn!("Skipping record...");
continue continue;
} }
CollisionHandling::OverwriteSingleSkipMultiple => { CollisionHandling::OverwriteSingleSkipMultiple => {
let mut collided_resources = let mut collided_resources =
self.0.iter().enumerate() self.0.iter().enumerate().filter(|record| {
.filter(|record| record.1.keys().any(|elem| collisions.contains(elem))); record.1.keys().any(|elem| collisions.contains(elem))
if let Some((collided_index, collided_resource)) = collided_resources.next() { });
if let Some((collided_index, collided_resource)) = collided_resources.next()
{
if collided_resources.next().is_some() { if collided_resources.next().is_some() {
warn!("Resource collides with multiple records, skipping..."); warn!("Resource collides with multiple records, skipping...");
continue continue;
} }
warn!("Removing {}", collided_resource.subject); warn!("Removing {}", collided_resource.subject);
self.0.remove(collided_index); self.0.remove(collided_index);
@ -97,12 +109,14 @@ impl ResourceList {
if record.keys().any(|elem| collisions.contains(elem)) { if record.keys().any(|elem| collisions.contains(elem)) {
warn!("Removing {record:?}"); warn!("Removing {record:?}");
false false
} else {true} } else {
true
}
}); });
} }
CollisionHandling::Terminate => { CollisionHandling::Terminate => {
warn!("Collision found, terminating merge process..."); warn!("Collision found, terminating merge process...");
return self return self;
} }
} }
} }

View file

@ -1,24 +1,27 @@
use crate::args_parser::{DataPaths, ServerParameters};
use crate::schema::lookup_handler::LookupHandler;
use crate::schema::resource::Resource;
use axum::body::Body;
use axum::extract::{Request, State};
use axum::http::StatusCode;
use axum::response::Response;
use axum::routing::get;
use axum::Router;
use std::io::Read; use std::io::Read;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::ops::DerefMut; use std::ops::DerefMut;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::RwLock;
use axum::body::Body;
use axum::extract::{Request, State};
use axum::http::StatusCode;
use axum::response::Response;
use axum::Router;
use axum::routing::get;
use tokio::select; use tokio::select;
use tokio::signal::unix::SignalKind; use tokio::signal::unix::SignalKind;
use tokio::sync::RwLock;
use tracing::{debug, error, info, instrument, trace}; use tracing::{debug, error, info, instrument, trace};
use crate::args_parser::{DataPaths, ServerParameters};
use crate::schema::lookup_handler::LookupHandler;
use crate::schema::resource::Resource;
pub fn init(data_paths: DataPaths, server_parameters: ServerParameters) { pub fn init(data_paths: DataPaths, server_parameters: ServerParameters) {
let DataPaths { database_path, pid_file_path } = data_paths; let DataPaths {
database_path,
pid_file_path,
} = data_paths;
//TODO: Apparently you're supposed to keep this file around and verify that the PID in it is still active? //TODO: Apparently you're supposed to keep this file around and verify that the PID in it is still active?
debug!("Attempting to install PID file at {pid_file_path:?}"); debug!("Attempting to install PID file at {pid_file_path:?}");
if let Ok(mut file) = std::fs::File::open(&pid_file_path) { if let Ok(mut file) = std::fs::File::open(&pid_file_path) {
@ -38,7 +41,7 @@ pub fn init(data_paths: DataPaths, server_parameters: ServerParameters) {
error!("Failed to create tokio runtime: {e}"); error!("Failed to create tokio runtime: {e}");
return; return;
} }
Ok(runtime) => runtime Ok(runtime) => runtime,
}; };
if let Err(e) = runtime.block_on(async_main(database_path, server_parameters)) { if let Err(e) = runtime.block_on(async_main(database_path, server_parameters)) {
error!("Failed to block on server's async_main: {e}"); error!("Failed to block on server's async_main: {e}");
@ -48,7 +51,10 @@ pub fn init(data_paths: DataPaths, server_parameters: ServerParameters) {
} }
#[instrument(skip_all)] #[instrument(skip_all)]
async fn async_main(db_path: PathBuf, server_parameters: ServerParameters) -> Result<(), std::io::Error> { async fn async_main(
db_path: PathBuf,
server_parameters: ServerParameters,
) -> Result<(), std::io::Error> {
info!("Initialising server..."); info!("Initialising server...");
let datastore = Arc::new(RwLock::new(LookupHandler::load(&db_path).unwrap())); let datastore = Arc::new(RwLock::new(LookupHandler::load(&db_path).unwrap()));
let _handler = tokio::spawn(hangup_handler(db_path, datastore.clone())); let _handler = tokio::spawn(hangup_handler(db_path, datastore.clone()));
@ -65,35 +71,64 @@ async fn async_main(db_path: PathBuf, server_parameters: ServerParameters) -> Re
.await .await
} }
async fn run_webfinger_query(State(datastore): State<Arc<RwLock<LookupHandler>>>, request: Request) -> Result<Response, StatusCode> { async fn run_webfinger_query(
State(datastore): State<Arc<RwLock<LookupHandler>>>,
request: Request,
) -> Result<Response, StatusCode> {
let uri = request.uri(); let uri = request.uri();
info!("Received query with {uri}"); info!("Received query with {uri}");
let query = uri.query().ok_or(StatusCode::BAD_REQUEST)?; let query = uri.query().ok_or(StatusCode::BAD_REQUEST)?;
debug!("Query string is {query}"); debug!("Query string is {query}");
let params = query.split('&').filter_map(|query_part| { let params = query
.split('&')
.filter_map(|query_part| {
trace!("Processing part {query_part}"); trace!("Processing part {query_part}");
query_part.split_once('=') query_part.split_once('=')
}).collect::<Vec<_>>(); })
.collect::<Vec<_>>();
trace!("Query parts are {params:?}"); trace!("Query parts are {params:?}");
let mut resource_query_iter = params.iter().filter_map(|(key, value)| if key.trim() == "resource" {Some(value)} else {None}); let mut resource_query_iter = params.iter().filter_map(|(key, value)| {
if key.trim() == "resource" {
Some(value)
} else {
None
}
});
let resource_query = *resource_query_iter.next().ok_or(StatusCode::BAD_REQUEST)?; let resource_query = *resource_query_iter.next().ok_or(StatusCode::BAD_REQUEST)?;
if resource_query_iter.next().is_some() { if resource_query_iter.next().is_some() {
debug!("Multiple resource parameters provided, bailing"); debug!("Multiple resource parameters provided, bailing");
return Err(StatusCode::BAD_REQUEST) return Err(StatusCode::BAD_REQUEST);
} }
let resource = datastore.read().await.lookup(resource_query).ok_or(StatusCode::NOT_FOUND)?.clone(); let resource = datastore
.read()
.await
.lookup(resource_query)
.ok_or(StatusCode::NOT_FOUND)?
.clone();
debug!("Found resource: {resource:?}"); debug!("Found resource: {resource:?}");
let mut rels = params.into_iter().filter_map(|(key, value)| if key.trim() == "rel" {Some(value)} else {None}).peekable(); let mut rels = params
.into_iter()
.filter_map(|(key, value)| {
if key.trim() == "rel" {
Some(value)
} else {
None
}
})
.peekable();
let response_body = if rels.peek().is_some() { let response_body = if rels.peek().is_some() {
debug!("There were rel parameters in the query"); debug!("There were rel parameters in the query");
if let Some(links) = resource.links { if let Some(links) = resource.links {
debug!("Filtering links..."); debug!("Filtering links...");
Resource { Resource {
links: Some(rels.filter_map(|rel| links.iter().find(|link| link.rel == rel).cloned()).collect()), links: Some(
rels.filter_map(|rel| links.iter().find(|link| link.rel == rel).cloned())
.collect(),
),
..resource ..resource
} }
} else { } else {
@ -107,10 +142,12 @@ async fn run_webfinger_query(State(datastore): State<Arc<RwLock<LookupHandler>>>
Response::builder() Response::builder()
.header("Content-Type", "application/jrd+json") .header("Content-Type", "application/jrd+json")
.body(Body::new(serde_json::to_string(&response_body).map_err(|e| { .body(Body::new(serde_json::to_string(&response_body).map_err(
|e| {
error!("Server error occurred while serialising response body: {e}"); error!("Server error occurred while serialising response body: {e}");
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
})?)) },
)?))
.map_err(|e| { .map_err(|e| {
error!("Server error occurred while building response: {e}"); error!("Server error occurred while building response: {e}");
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
@ -132,7 +169,9 @@ async fn hangup_handler(db_path: PathBuf, datastore: Arc<RwLock<LookupHandler>>)
std::mem::swap(lock.deref_mut(), &mut handler); std::mem::swap(lock.deref_mut(), &mut handler);
info!("Data updated!"); info!("Data updated!");
} }
Err(error) => {error!("Failed to reload datastore: {error}")} Err(error) => {
error!("Failed to reload datastore: {error}")
}
} }
} }
} }