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

Bojangalic/Add git command functionality into serve command, and keep both commands #65

Merged
merged 9 commits into from
Feb 5, 2025
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,24 @@ and this project adheres to a _modified_ form of _[Semantic Versioning][semver]_

### Removed

## [0.5.0-alpha.1]

### Added

- Added tests fot `stelae git` ([64])

### Changed

- Merged `stelae git` and `stelae serve` into single command ([64])

### Fixed

- git serve now support commitish that contains / in name ([64])

### Removed

[64]: https://github.com/openlawlibrary/stelae/pull/64

## [0.4.1]

### Added
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.4.1"
version = "0.5.0-alpha.1"
edition = "2021"
readme = "README.md"
license = "AGPL-3.0"
Expand Down
7 changes: 7 additions & 0 deletions dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM rust:1.83

# Install necessary tools (if needed)
RUN apt-get update && apt-get install -y bash

# Set the container's working directory
WORKDIR /workspace
3 changes: 3 additions & 0 deletions docs/build_stelae_binaries_for_linux.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
1. Install Docker
2. From powershell run `cd <path to stelae project root>` and then run `docker build -t rust-compiler .` to download image
3. After image is downloaded run `docker run --rm -v "C:\<pathtostelae>\:/workspace" rust-compiler bash -c "cargo build --release"`
1 change: 1 addition & 0 deletions src/server/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
pub mod routes;
pub mod serve;
pub mod state;
pub mod stelae;
pub mod versions;
11 changes: 11 additions & 0 deletions src/server/api/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use actix_web::{
guard, web, App, Error, Scope,
};

use super::stelae::get_blob;
use super::{serve::serve, state::Global, versions::versions};

/// Name of the header to guard current documents
Expand Down Expand Up @@ -76,6 +77,16 @@ pub fn register_app<
)
.app_data(web::Data::new(state.clone()));

app = app
.app_data(web::Data::new(state.archive().path.clone()))
.service(
web::scope("_stelae").service(
web::resource("/{namespace}/{name}")
.route(web::get().to(get_blob))
.route(web::head().to(get_blob)),
),
);

app = register_dynamic_routes(app, state)?;
Ok(app)
}
Expand Down
67 changes: 67 additions & 0 deletions src/server/api/stelae/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//! API endpoint for serving git blobs.

use std::path::PathBuf;

use actix_web::{web, HttpRequest, HttpResponse, Responder};
use request::StelaeQueryData;

use crate::utils::git::{Repo, GIT_REQUEST_NOT_FOUND};
use crate::utils::http::get_contenttype;
use crate::utils::paths::clean_path;
use git2::{self, ErrorCode};

use super::super::errors::HTTPError;

/// Module that maps the HTTP web request body to structs.
pub mod request;

/// Return the content in the stelae archive in the `{namespace}/{name}`
/// repo at the `commitish` commit at the `remainder` path.
/// Return 404 if any are not found or there are any errors.
#[tracing::instrument(name = "Retrieving a Git blob", skip(path, data, query))]
#[expect(
clippy::future_not_send,
reason = "We don't worry about git2-rs not implementing `Send` trait"
)]
pub async fn get_blob(
req: HttpRequest,
path: web::Path<(String, String)>,
query: web::Query<StelaeQueryData>,
data: web::Data<PathBuf>,
) -> impl Responder {
let (namespace, name) = path.into_inner();
let query_data: StelaeQueryData = query.into_inner();
let commitish = query_data.commitish.unwrap_or_else(|| String::from("HEAD"));
let remainder = query_data.remainder.unwrap_or_default();
let archive_path = &data;
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 {
Ok(content) => HttpResponse::Ok().insert_header(contenttype).body(content),
Err(error) => blob_error_response(&error, &namespace, &name),
}
}

/// A centralised place to match potentially unsafe internal errors to safe user-facing error responses
#[expect(clippy::wildcard_enum_match_arm, reason = "Allows _ for enum matching")]
#[tracing::instrument(name = "Error with Git blob request", skip(error, namespace, name))]
fn blob_error_response(error: &anyhow::Error, namespace: &str, name: &str) -> HttpResponse {
tracing::error!("{error}",);
if let Some(git_error) = error.downcast_ref::<git2::Error>() {
return match git_error.code() {
// TODO: check this is the right error
ErrorCode::NotFound => {
HttpResponse::NotFound().body(format!("repo {namespace}/{name} does not exist"))
}
_ => HttpResponse::InternalServerError().body("Unexpected Git error"),
};
}
match error {
// TODO: Obviously it's better to use custom `Error` types
_ if error.to_string() == GIT_REQUEST_NOT_FOUND => {
HttpResponse::NotFound().body(HTTPError::NotFound.to_string())
}
_ => HttpResponse::InternalServerError().body(HTTPError::InternalServerError.to_string()),
}
}
9 changes: 9 additions & 0 deletions src/server/api/stelae/request/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use serde::Deserialize;
/// Structure for passing query parameters for stelae endpoint
#[derive(Debug, Deserialize)]
pub struct StelaeQueryData {
/// commit (or reference) to the repo. Can pass in `HEAD`, a branch ref (e.g. main), or a commit SHA. If nothing is passed by default it will look for HEAD
pub commitish: Option<String>,
/// path of the file (e.g. \us\ca\cities\san-mateo\index.html). If nothing is passed by default it will look for index.html
pub remainder: Option<String>,
}
6 changes: 5 additions & 1 deletion src/server/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ pub async fn serve_archive(
}
};

let archive = match Archive::parse(archive_path, &PathBuf::from(raw_archive_path), individual) {
let archive = match Archive::parse(
archive_path.clone(),
&PathBuf::from(raw_archive_path),
individual,
) {
Ok(archive) => archive,
Err(err) => {
tracing::error!("Unable to parse archive at '{raw_archive_path}'.");
Expand Down
1 change: 0 additions & 1 deletion src/server/git.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
//! Legacy git microserver.

use actix_web::{get, route, web, App, HttpResponse, HttpServer, Responder};
use git2::{self, ErrorCode};
use std::path::PathBuf;
Expand Down
2 changes: 1 addition & 1 deletion tests/api/archive_basic_test.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::archive_testtools::config::{ArchiveType, Jurisdiction};
use crate::common;
use crate::common::{self};
use actix_web::test;

#[actix_web::test]
Expand Down
1 change: 1 addition & 0 deletions tests/api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod archive_basic_test;
mod archive_multihost_test;
mod archive_multijursidiction_test;
mod stelae;
64 changes: 64 additions & 0 deletions tests/api/stelae/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use actix_http::{Method, Request};
use actix_service::Service;
use actix_web::body::MessageBody;
use actix_web::dev::ServiceResponse;
use actix_web::{test, Error};
mod stelae_basic_test;
mod stelae_multihost_test;

/// Helper method which test all `fille_paths`` in `org_name`/`repo_name` repository on `branch_name`` branch with `expected` result
async fn test_stelae_paths(
org_name: &str,
repo_name: &str,
file_paths: Vec<&str>,
branch_name: &str,
app: &impl Service<Request, Response = ServiceResponse<impl MessageBody>, Error = Error>,
expected: bool,
) {
for file_path in file_paths {
let req = test::TestRequest::get()
.uri(&format!(
"/_stelae/{}/{}?commitish={}&remainder={}",
org_name, repo_name, branch_name, file_path
))
.to_request();

let resp = test::call_service(&app, req).await;
let actual = if expected {
resp.status().is_success()
} else {
resp.status().is_client_error()
};
let expected = true;
assert_eq!(actual, expected);
}
}

/// Helper method which test all `fille_paths`` in `org_name`/`repo_name` repository on `branch_name`` branch with `expected` result
async fn test_stelae_paths_with_head_method(
org_name: &str,
repo_name: &str,
file_paths: Vec<&str>,
branch_name: &str,
app: &impl Service<Request, Response = ServiceResponse<impl MessageBody>, Error = Error>,
expected: bool,
) {
for file_path in file_paths {
let req = test::TestRequest::default()
.method(Method::HEAD)
.uri(&format!(
"/_stelae/{}/{}?commitish={}&remainder={}",
org_name, repo_name, branch_name, file_path
))
.to_request();

let resp = test::call_service(&app, req).await;
let actual = if expected {
resp.status().is_success()
} else {
resp.status().is_client_error()
};
let expected = true;
assert_eq!(actual, expected);
}
}
Loading