diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dad9d74..3e3bb67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: with: tool: nextest - name: Run tests (nextest) - run: just nextest + run: just test lints: name: Lints diff --git a/Cargo.lock b/Cargo.lock index 12d1ead..b87a992 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3110,7 +3110,7 @@ dependencies = [ [[package]] name = "stelae" -version = "0.3.0-alpha.5" +version = "0.3.0-alpha.6" dependencies = [ "actix-http", "actix-service", diff --git a/Cargo.toml b/Cargo.toml index eb13f82..9d77e60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "stelae" description = "A collection of tools in Rust and Python for preserving, authenticating, and accessing laws in perpetuity." -version = "0.3.0-alpha.5" +version = "0.3.0-alpha.6" edition = "2021" readme = "README.md" license = "AGPL-3.0" diff --git a/justfile b/justfile index 349f55e..4a57641 100644 --- a/justfile +++ b/justfile @@ -13,8 +13,6 @@ format: # Run all tests test: - cargo test -nextest: cargo nextest run --all --no-fail-fast && cargo test --doc # Run clippy maximum strictness. Passes through any flags to clippy. @@ -25,7 +23,7 @@ clippy *FLAGS: -D warnings \ # Continuous integration - test, lint, benchmark -ci: lint nextest bench +ci: lint test bench # Run all benchmarks bench: diff --git a/src/server/api/serve/mod.rs b/src/server/api/serve/mod.rs index 2d22303..de824c9 100644 --- a/src/server/api/serve/mod.rs +++ b/src/server/api/serve/mod.rs @@ -24,7 +24,7 @@ pub async fn serve( let mut path = format!("{prefix}/{tail}"); path = clean_path(&path); let contenttype = get_contenttype(&path); - let blob = find_current_blob(&data.repo, &shared, &path); + let blob = find_current_blob(&data, &shared, &path); match blob { Ok(content) => HttpResponse::Ok().insert_header(contenttype).body(content), Err(error) => { @@ -38,13 +38,23 @@ pub async fn serve( /// Latest blob is found by looking at the HEAD commit #[allow(clippy::panic_in_result_fn, clippy::unreachable)] #[tracing::instrument(name = "Finding document", skip(repo, shared))] -fn find_current_blob(repo: &Repo, shared: &SharedState, path: &str) -> anyhow::Result> { - let blob = repo.get_bytes_at_path(HEAD_COMMIT, path); +fn find_current_blob( + repo: &RepoState, + shared: &SharedState, + path: &str, +) -> anyhow::Result> { + let blob = Repo::find_blob(&repo.archive_path, &repo.org, &repo.name, path, HEAD_COMMIT); match blob { Ok(content) => Ok(content), Err(error) => { if let Some(fallback) = shared.fallback.as_ref() { - let fallback_blob = fallback.repo.get_bytes_at_path(HEAD_COMMIT, path); + let fallback_blob = Repo::find_blob( + &fallback.archive_path, + &fallback.org, + &fallback.name, + path, + HEAD_COMMIT, + ); return fallback_blob.map_or_else( |err| anyhow::bail!("No fallback blob found - {}", err.to_string()), Ok, diff --git a/src/server/api/state.rs b/src/server/api/state.rs index 9932e36..dd819af 100644 --- a/src/server/api/state.rs +++ b/src/server/api/state.rs @@ -4,9 +4,8 @@ use std::{fmt, path::PathBuf}; use crate::{ db, stelae::{archive::Archive, stele::Stele, types::repositories::Repository}, - utils::{archive::get_name_parts, git}, + utils::archive::get_name_parts, }; -use git2::Repository as GitRepository; /// Global, read-only state pub trait Global { @@ -37,12 +36,36 @@ impl Global for App { /// Repository to serve pub struct RepoData { - /// git2 wrapper repository pointing to the repo in the archive. - pub repo: git::Repo, + /// Path to the archive + pub archive_path: PathBuf, + /// Path to the Stele + pub path: PathBuf, + /// Repo organization + pub org: String, + /// Repo name + pub name: String, + // /// path to the git repository + // pub repo_path: PathBuf; ///Latest or historical pub serve: String, } +impl RepoData { + /// Create a new Repo state object + #[must_use] + pub fn new(archive_path: &str, org: &str, name: &str, serve: &str) -> Self { + let mut repo_path = archive_path.to_owned(); + repo_path = format!("{repo_path}/{org}/{name}"); + Self { + archive_path: PathBuf::from(archive_path), + path: PathBuf::from(&repo_path), + org: org.to_owned(), + name: name.to_owned(), + serve: serve.to_owned(), + } + } +} + /// Shared, read-only app state pub struct Shared { /// Repository to fall back to if the current one is not found @@ -54,8 +77,8 @@ impl fmt::Debug for RepoData { write!( formatter, "Repo for {} in the archive at {}", - self.repo.name, - self.repo.path.display() + self.name, + self.path.display() ) } } @@ -67,8 +90,8 @@ impl fmt::Debug for Shared { Some(fallback) => write!( formatter, "Repo for {} in the archive at {}", - fallback.repo.name, - fallback.repo.path.display() + fallback.name, + fallback.path.display() ), None => write!(formatter, "No fallback repo"), } @@ -79,7 +102,10 @@ impl fmt::Debug for Shared { impl Clone for RepoData { fn clone(&self) -> Self { Self { - repo: self.repo.clone(), + archive_path: self.archive_path.clone(), + path: self.path.clone(), + org: self.org.clone(), + name: self.name.clone(), serve: self.serve.clone(), } } @@ -102,18 +128,12 @@ impl Clone for Shared { pub fn init_repo(repo: &Repository, stele: &Stele) -> anyhow::Result { let custom = &repo.custom; let (org, name) = get_name_parts(&repo.name)?; - let mut repo_path = stele.archive_path.to_string_lossy().into_owned(); - repo_path = format!("{repo_path}/{org}/{name}"); - Ok(RepoData { - repo: git::Repo { - archive_path: stele.archive_path.to_string_lossy().to_string(), - path: PathBuf::from(&repo_path), - org, - name, - repo: GitRepository::open(&repo_path)?, - }, - serve: custom.serve.clone(), - }) + Ok(RepoData::new( + &stele.archive_path.to_string_lossy(), + &org, + &name, + &custom.serve, + )) } /// Initialize the shared application state @@ -128,10 +148,12 @@ pub fn init_shared(stele: &Stele) -> anyhow::Result { .get_fallback_repo() .map(|repo| { let (org, name) = get_name_parts(&repo.name)?; - Ok::(RepoData { - repo: git::Repo::new(&stele.archive_path, &org, &name)?, - serve: repo.custom.serve.clone(), - }) + Ok::(RepoData::new( + &stele.archive_path.to_string_lossy(), + &org, + &name, + &repo.custom.serve, + )) }) .transpose()?; Ok(Shared { fallback }) diff --git a/src/server/api/versions/response/mod.rs b/src/server/api/versions/response/mod.rs index e82d6c3..1d58e72 100644 --- a/src/server/api/versions/response/mod.rs +++ b/src/server/api/versions/response/mod.rs @@ -1,5 +1,6 @@ use std::{cmp::Reverse, collections::BTreeMap}; +use chrono::NaiveDate; use serde::Deserialize; use serde::Serialize; @@ -166,13 +167,18 @@ impl Version { /// Insert a new version if it is not present in the list of versions. /// If the date is not in the list of versions, add it + /// Do nothing if the date is already in the list of versions. /// This for compatibility purposes with the previous implementation of historical versions pub fn insert_if_not_present(versions: &mut Vec, date: Option) { - if let Some(version_date) = date { - if versions.iter().all(|ver| ver.date != version_date) { - let version = Self::new(version_date.clone(), version_date, 0); - Self::insert_version_sorted(versions, version); - } + let Some(version_date) = date else { + return; + }; + if NaiveDate::parse_from_str(&version_date, "%Y-%m-%d").is_err() { + return; + } + if versions.iter().all(|ver| ver.date != version_date) { + let version = Self::new(version_date.clone(), version_date, 0); + Self::insert_version_sorted(versions, version); } } diff --git a/src/server/git.rs b/src/server/git.rs index 4254053..fb9a522 100644 --- a/src/server/git.rs +++ b/src/server/git.rs @@ -11,7 +11,7 @@ use actix_web::{get, web, App, HttpResponse, HttpServer, Responder}; use git2::{self, ErrorCode}; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use tracing_actix_web::TracingLogger; use super::errors::{CliError, StelaeError}; @@ -51,7 +51,7 @@ async fn get_blob( ) -> impl Responder { let (namespace, name, commitish, remainder) = path.into_inner(); let archive_path = &data.archive_path; - let blob = find_blob(archive_path, &namespace, &name, &remainder, &commitish); + let blob = Repo::find_blob(archive_path, &namespace, &name, &remainder, &commitish); let blob_path = clean_path(&remainder); let contenttype = get_contenttype(&blob_path); match blob { @@ -60,21 +60,6 @@ async fn get_blob( } } -/// Do the work of looking for the requested Git object. -// TODO: This looks like it could live in `utils::git::Repo` -fn find_blob( - archive_path: &Path, - namespace: &str, - name: &str, - remainder: &str, - commitish: &str, -) -> anyhow::Result> { - let repo = Repo::new(archive_path, namespace, name)?; - let blob_path = clean_path(remainder); - let blob = repo.get_bytes_at_path(commitish, &blob_path)?; - Ok(blob) -} - /// A centralised place to match potentially unsafe internal errors to safe user-facing error responses #[allow(clippy::wildcard_enum_match_arm)] #[tracing::instrument(name = "Error with Git blob request", skip(error, namespace, name))] diff --git a/src/utils/git.rs b/src/utils/git.rs index eb68a28..57fd269 100644 --- a/src/utils/git.rs +++ b/src/utils/git.rs @@ -1,5 +1,6 @@ //! The git module contains structs for interacting with git repositories //! in the Stelae Archive. +use crate::utils::paths::clean_path; use anyhow::Context; use git2::Repository; use std::{ @@ -70,6 +71,24 @@ impl Repo { }) } + /// Do the work of looking for the requested Git object. + /// + /// + /// # Errors + /// Will error if the Repo couldn't be found, or if there was a problem with the Git object. + pub fn find_blob( + archive_path: &Path, + namespace: &str, + name: &str, + remainder: &str, + commitish: &str, + ) -> anyhow::Result> { + let repo = Self::new(archive_path, namespace, name)?; + let blob_path = clean_path(remainder); + let blob = repo.get_bytes_at_path(commitish, &blob_path)?; + Ok(blob) + } + /// Returns bytes of blob found in the commit `commitish` at path `path` /// if a blob is not found at path, it will try adding ".html", "index.html, /// and "/index.html".