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

fix: load paths to git repositories at start-time; versions endpoint fix #47

Merged
merged 4 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
with:
tool: nextest
- name: Run tests (nextest)
run: just nextest
run: just test

lints:
name: Lints
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
4 changes: 1 addition & 3 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand Down
18 changes: 14 additions & 4 deletions src/server/api/serve/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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<Vec<u8>> {
let blob = repo.get_bytes_at_path(HEAD_COMMIT, path);
fn find_current_blob(
repo: &RepoState,
shared: &SharedState,
path: &str,
) -> anyhow::Result<Vec<u8>> {
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,
Expand Down
72 changes: 47 additions & 25 deletions src/server/api/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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()
)
}
}
Expand All @@ -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"),
}
Expand All @@ -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(),
}
}
Expand All @@ -102,18 +128,12 @@ impl Clone for Shared {
pub fn init_repo(repo: &Repository, stele: &Stele) -> anyhow::Result<RepoData> {
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
Expand All @@ -128,10 +148,12 @@ pub fn init_shared(stele: &Stele) -> anyhow::Result<Shared> {
.get_fallback_repo()
.map(|repo| {
let (org, name) = get_name_parts(&repo.name)?;
Ok::<RepoData, anyhow::Error>(RepoData {
repo: git::Repo::new(&stele.archive_path, &org, &name)?,
serve: repo.custom.serve.clone(),
})
Ok::<RepoData, anyhow::Error>(RepoData::new(
&stele.archive_path.to_string_lossy(),
&org,
&name,
&repo.custom.serve,
))
})
.transpose()?;
Ok(Shared { fallback })
Expand Down
16 changes: 11 additions & 5 deletions src/server/api/versions/response/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{cmp::Reverse, collections::BTreeMap};

use chrono::NaiveDate;
use serde::Deserialize;
use serde::Serialize;

Expand Down Expand Up @@ -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<Self>, date: Option<String>) {
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);
}
}

Expand Down
19 changes: 2 additions & 17 deletions src/server/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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 {
Expand All @@ -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<Vec<u8>> {
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))]
Expand Down
19 changes: 19 additions & 0 deletions src/utils/git.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -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<Vec<u8>> {
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".
Expand Down
Loading