Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🏗️ Migrate from Rocket to Axum #1

Merged
merged 11 commits into from
Aug 25, 2024
1,130 changes: 408 additions & 722 deletions src/Cargo.lock

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion src/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,25 @@ members = [
resolver = "2"

[workspace.dependencies]
axum = { version = "0.7", features = ["macros"] }
axum-extra = { version = "0.9", features = ["cookie"] }
base64 = "0.22"
biscuit-auth = "5"
chrono = "0.4"
hex = "0.4"
iso8601-duration = "0.2"
lazy_static = "1"
rocket = "0.5"
mime = "0.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_urlencoded = "0.7"
serde_with = "3"
tera = "1"
thiserror = "1"
time = "0.3"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tower = "0.5"
tower-http = { version = "0.5", features = ["fs", "trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
urlencoding = "2"
7 changes: 5 additions & 2 deletions src/helpers/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
[package]
name = "orangutan-helpers"
version = "0.1.1"
version = "0.2.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
axum = { workspace = true }
biscuit-auth = { workspace = true }
hex = { workspace = true }
lazy_static = { workspace = true }
rocket = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_with = { workspace = true }
thiserror = { workspace = true }
tower-http = { workspace = true }
tracing = { workspace = true }
14 changes: 6 additions & 8 deletions src/helpers/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,24 @@ use lazy_static::lazy_static;

pub const THEME_NAME: &str = "Orangutan";
pub const DATA_FILE_EXTENSION: &str = "orangutan";
pub(super) const READ_ALLOWED_FIELD: &str = "read_allowed";
pub const PATH_FIELD: &str = "path";
pub const DEFAULT_PROFILE: &str = "_default";
pub const ROOT_KEY_NAME: &'static str = "_biscuit_root";

pub(super) const WEBSITE_DIR_NAME: &'static str = "website";

lazy_static! {
static ref WORK_DIR: PathBuf = env::current_dir().unwrap();
pub static ref WEBSITE_REPOSITORY: String = env::var("WEBSITE_REPOSITORY")
.expect("Environment variable `WEBSITE_REPOSITORY` is required.");
pub static ref MODE: Result<String, env::VarError> = env::var("MODE");
pub static ref KEYS_MODE: Result<String, env::VarError> = env::var("KEYS_MODE");
}
lazy_static! {
static ref WORK_DIR: PathBuf = env::current_dir().unwrap();
pub static ref BASE_DIR: PathBuf = WORK_DIR.join(".orangutan");
pub static ref TMP_DIR: PathBuf = BASE_DIR.join("tmp");
pub static ref KEYS_DIR: PathBuf = BASE_DIR.join("keys");
pub static ref MODE: Result<String, env::VarError> = env::var("MODE");
pub static ref KEYS_MODE: Result<String, env::VarError> = env::var("KEYS_MODE");
pub(super) static ref WEBSITE_ROOT: PathBuf = BASE_DIR.join("website");
pub(super) static ref WEBSITE_ROOT: PathBuf = BASE_DIR.join("website-src");
pub(super) static ref HUGO_CONFIG_DIR: PathBuf = BASE_DIR.join("hugo-config");
pub static ref DEST_DIR: PathBuf = BASE_DIR.join("out");
pub static ref WEBSITE_DATA_DIR: PathBuf = DEST_DIR.join("data");
pub(super) static ref SUFFIXED_EXTENSIONS: Vec<&'static str> =
vec!["html", "json", "xml", "css", "js", "txt"];
}
8 changes: 5 additions & 3 deletions src/helpers/src/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,14 +406,14 @@ pub fn trash_outdated_websites() -> Result<State, Error> {
hugo_config_generated: HUGO_CONFIG_GENERATED.load(Ordering::Relaxed),
data_files_generated: DATA_FILES_GENERATED.load(Ordering::Relaxed),
generated_websites: GENERATED_WEBSITES.lock().unwrap().to_owned(),
used_profiles: super::USED_PROFILES.lock().unwrap().to_owned(),
used_profiles: super::USED_PROFILES.read().unwrap().to_owned(),
};

// Clear caches
HUGO_CONFIG_GENERATED.store(false, Ordering::Relaxed);
DATA_FILES_GENERATED.store(false, Ordering::Relaxed);
GENERATED_WEBSITES.lock().unwrap().clear();
*super::USED_PROFILES.lock().unwrap() = None;
*super::USED_PROFILES.write().unwrap() = None;

Ok(state)
}
Expand All @@ -428,7 +428,7 @@ pub fn recover_trash(state: State) -> Result<(), Error> {
HUGO_CONFIG_GENERATED.store(state.hugo_config_generated, Ordering::Relaxed);
DATA_FILES_GENERATED.store(state.data_files_generated, Ordering::Relaxed);
*GENERATED_WEBSITES.lock().unwrap() = state.generated_websites;
*super::USED_PROFILES.lock().unwrap() = state.used_profiles;
*super::USED_PROFILES.write().unwrap() = state.used_profiles;

Ok(())
}
Expand Down Expand Up @@ -456,4 +456,6 @@ pub enum Error {
CannotCreateHugoConfigFile(io::Error),
#[error("IO error: {0}")]
IOError(#[from] io::Error),
#[error("Could not read page metadata: JSON error: {0}")]
CannotReadPageMetadata(serde_json::Error),
}
173 changes: 101 additions & 72 deletions src/helpers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,61 @@ use std::{
collections::HashSet,
fs::{self, File},
io,
path::{Path, PathBuf},
sync::{Arc, Mutex},
ops::Deref,
path::PathBuf,
sync::{Arc, RwLock},
};

use lazy_static::lazy_static;
use serde_json::Value;
use serde::{de::DeserializeOwned, Deserialize};
use serde_with::{serde_as, DefaultOnNull};
use tracing::{debug, error, trace};

use crate::config::*;

lazy_static! {
static ref USED_PROFILES: Arc<Mutex<Option<&'static HashSet<String>>>> =
Arc::new(Mutex::new(None));
static ref USED_PROFILES: Arc<RwLock<Option<&'static HashSet<String>>>> = Arc::default();
}

pub fn used_profiles<'a>() -> &'a HashSet<String> {
let mut used_profiles = USED_PROFILES.lock().unwrap();
if let Some(profiles) = used_profiles.clone() {
if let Some(profiles) = *USED_PROFILES.read().unwrap() {
trace!("Read used profiles from cache");
return profiles;
}

trace!("Reading used profiles…");
debug!("Reading all used profiles…");
let acc: &'static mut HashSet<String> = Box::leak(Box::new(HashSet::new()));

for data_file in find_data_files() {
// trace!("Reading <{}>…", data_file.display());

// Make sure this generator isn't broken (could be replaced by unit tests)
// let html_file = html_file(&data_file).unwrap();
// trace!("{}", html_file.display());

let read_allowed = read_allowed(&data_file).unwrap();
let metadata: PageMetadata = match deser(&data_file) {
Ok(Some(metadata)) => metadata,
Ok(None) => {
error!(
"Could not read page metadata at <{}>: File not found",
data_file.display(),
);
continue;
},
Err(err) => {
error!(
"Could not read page metadata at <{}>: {err}",
data_file.display(),
);
continue;
},
};
let read_allowed = metadata.read_allowed;
// trace!(" read_allowed: {:?}", read_allowed);

// Store new profiles
read_allowed.iter().for_each(|p| {
acc.insert(p.clone());
read_allowed.into_iter().for_each(|p| {
acc.insert(p.to_owned());
});
}

*used_profiles = Some(acc);
*USED_PROFILES.write().unwrap() = Some(acc);

acc
}
Expand Down Expand Up @@ -76,43 +89,13 @@ pub fn find(
}
}

fn _html_file(data_file: &PathBuf) -> Option<Option<PathBuf>> {
let file = File::open(data_file).ok()?;
let reader = io::BufReader::new(file);
let data: Value = match serde_json::from_reader(reader) {
Ok(data) => data,
Err(_) => return None,
};

let path = data.get(PATH_FIELD)?;

Some(serde_json::from_value(path.to_owned()).ok())
}

fn html_file(data_file: &PathBuf) -> Result<PathBuf, ()> {
match _html_file(data_file) {
Some(Some(path)) => Ok(path),
Some(None) => {
error!("Path not defined");
Err(())
},
None => {
error!("File not found");
Err(())
},
}
}

pub fn data_file(html_file: &PathBuf) -> PathBuf {
let mut data_file_rel = html_file
.strip_prefix(WEBSITE_DATA_DIR.to_path_buf())
.unwrap_or(html_file)
.with_extension(DATA_FILE_EXTENSION);
data_file_rel = match data_file_rel.strip_prefix("/") {
pub fn data_file_path(page_relpath: &PathBuf) -> PathBuf {
let mut data_file_relpath = page_relpath.with_extension(DATA_FILE_EXTENSION);
data_file_relpath = match data_file_relpath.strip_prefix("/") {
Ok(trimmed) => trimmed.to_path_buf(),
Err(_) => data_file_rel,
Err(_) => data_file_relpath,
};
WEBSITE_DATA_DIR.join(data_file_rel)
WEBSITE_DATA_DIR.join(data_file_relpath)
}

fn find_data_files() -> Vec<PathBuf> {
Expand All @@ -125,35 +108,81 @@ fn find_data_files() -> Vec<PathBuf> {
data_files
}

fn _read_allowed(data_file: &PathBuf) -> Option<Option<Vec<String>>> {
let file = File::open(data_file).ok()?;
let reader = io::BufReader::new(file);
let data: Value = match serde_json::from_reader(reader) {
Ok(data) => data,
Err(_) => return None,
};
#[serde_as]
#[derive(Deserialize)]
pub struct PageMetadata {
// NOTE: Hugo taxonomy term pages contain `read_allowed": null`.
// TODO: Investigate and fix this, to remove the `serde` default which could be in conflict with the user's preference.
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnNull")]
pub read_allowed: ReadAllowed,
pub path: PathBuf,
}

#[derive(Debug, Clone, Deserialize)]
pub struct ReadAllowed(Vec<String>);

let read_allowed = data.get(READ_ALLOWED_FIELD)?;
impl Deref for ReadAllowed {
type Target = Vec<String>;

Some(serde_json::from_value(read_allowed.to_owned()).ok())
fn deref(&self) -> &Self::Target {
&self.0
}
}

impl Default for ReadAllowed {
fn default() -> Self {
Self(vec![DEFAULT_PROFILE.to_owned()])
}
}

impl IntoIterator for ReadAllowed {
type Item = <<Self as Deref>::Target as IntoIterator>::Item;
type IntoIter = <<Self as Deref>::Target as IntoIterator>::IntoIter;

fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}

// `None` if file not found
pub fn read_allowed(data_file: &PathBuf) -> Option<Vec<String>> {
_read_allowed(data_file).map(|o| o.unwrap_or(vec![DEFAULT_PROFILE.to_string()]))
fn deser<T: DeserializeOwned>(file_path: &PathBuf) -> Result<Option<T>, serde_json::Error> {
let Ok(file) = File::open(file_path) else {
return Ok(None);
};
let res: T = serde_json::from_reader(file)?;
Ok(Some(res))
}

pub fn object_key<P: AsRef<Path>>(
path: &P,
profile: &str,
) -> String {
let path = path.as_ref();
if let Some(ext) = path.extension() {
if SUFFIXED_EXTENSIONS.contains(&ext.to_str().unwrap()) {
return format!("{}@{}", path.display(), profile);
fn deser_first_match<T: DeserializeOwned>(
file_paths: Vec<PathBuf>
) -> Result<Option<T>, serde_json::Error> {
for file_path in file_paths.iter() {
trace!("Trying {}…", file_path.display());
match deser(file_path)? {
Some(res) => {
// TODO: Check if this index is always the same.
// debug!("Found page metadata in match #{i}");
return Ok(Some(res));
},
None => continue,
}
}
path.display().to_string()
Ok(None)
}

// `Ok(None)` if file not found.
// `Err(_)` if file found but deserialization error.
// `Ok(Some(_))` if file found.
pub fn page_metadata(page_relpath: &PathBuf) -> Result<Option<PageMetadata>, serde_json::Error> {
let mut file_paths = vec![
data_file_path(page_relpath),
data_file_path(&page_relpath.join("index.html")),
];
// Don't try parsing the exact path if it points to a directory.
if page_relpath.is_dir() {
file_paths.remove(0);
}
deser_first_match(file_paths)
}

pub fn copy_directory(
Expand Down
1 change: 0 additions & 1 deletion src/helpers/src/readers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
pub mod keys_reader;
pub mod object_reader;
Loading
Loading