bile |
| a simple self-hosted git server |
| git clone https://git.wayver.dev/bile |
| README | tree | log | refs |
Commit: 3aec1a1bb245ce255f59532eba2bdfe1d2879510 (tree)
Parent: 7e0330303eec590c92eb6194f83f04d0e8fb2ea0 (tree)
Author: wayverd
Date: 2026 M02 20, Fri 13:35:08 -0500
15 files changed; 108 insertions 49 deletions
diff --git a/src/handlers/git.rs b/src/handlers/git.rs
index c593a78..1b31eb4 100644
use axum::{
- extract::{Path, State},
+ extract::State,
http::{HeaderValue, StatusCode, Uri, header},
response::{IntoResponse as _, Response},
};
git::Repository,
http::{
extractor::RepoName,
+ path::Path,
response::{ErrorPage, Result},
},
};
fn inner(state: &BileState, uri: &Uri, repo_name: &RepoName) -> Result<Response> {
let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
else {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
};
// cant canonicalize if it doesnt exist
if !path.exists() {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
}
// that path got us outside of the repository structure somehow
if !path.starts_with(repo.path()) {
tracing::warn!("Attempt to acces file outside of repo dir: {:?}", path);
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::FORBIDDEN)
.into_response());
}
// Either the requested resource does not exist or it is not
// a file, i.e. a directory.
if !path.is_file() {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
}
diff --git a/src/handlers/repo_commit.rs b/src/handlers/repo_commit.rs
index 823424d..f32701d 100644
use std::fmt::Write as _;
use axum::{
- extract::{Path, State},
+ extract::State,
http::StatusCode,
response::{IntoResponse as _, Response},
};
git::Repository,
http::{
extractor::{Commit, RepoName},
+ path::Path,
response::{ErrorPage, Html, Result},
},
utils::filters,
fn inner(state: &BileState, repo_name: &RepoName, commit: &Commit) -> Result<Response> {
let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
else {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
};
let Some(commit) = repo.commit(&commit.0).context("failed to get commit")? else {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
};
diff --git a/src/handlers/repo_file.rs b/src/handlers/repo_file.rs
index e83fe92..e103430 100644
use std::{fmt::Write as _, path};
use axum::{
- extract::{Path, State},
+ extract::State,
http::StatusCode,
response::{IntoResponse as _, Response},
};
git::Repository,
http::{
extractor::{ObjectName, Ref, RepoName},
+ path::Path,
response::{ErrorPage, Html, Redirect, Result},
},
utils::{blob_mime, filters},
) -> Result<Response> {
let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
else {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
};
.commit_tree(&spec)
.context("failed to get commit tree")?
else {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
};
};
let Some(tree_obj) = tree_obj else {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
};
let Some(last_commit) = repo.file_last_commit(&spec, path)? else {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
};
};
let Some(blob) = tree_obj.as_blob() else {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
};
diff --git a/src/handlers/repo_file_raw.rs b/src/handlers/repo_file_raw.rs
index 9072050..c173e5e 100644
use std::path;
use axum::{
- extract::{Path, State},
+ extract::State,
http::{StatusCode, header},
response::{IntoResponse as _, Response},
};
git::Repository,
http::{
extractor::{ObjectName, Ref, RepoName},
+ path::Path,
response::{ErrorPage, Result},
},
utils::blob_mime,
let repo = match Repository::open(&state.config, repo_name).context("opening repository") {
Ok(Some(repo)) => repo,
Ok(None) => {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
}
Err(err) => {
tracing::error!(err=?err, "failed to open repository");
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
}
.commit_tree(&r#ref.0)
.context("failed to get commit tree")?
else {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
};
let Some(blob) = repo.tree_blob(&tree, path)? else {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
};
diff --git a/src/handlers/repo_home.rs b/src/handlers/repo_home.rs
index 503b851..5ccb446 100644
use axum::{
- extract::{Path, State},
+ extract::State,
http::StatusCode,
response::{IntoResponse as _, Response},
};
git::Repository,
http::{
extractor::RepoName,
+ path::Path,
response::{ErrorPage, Html, Result},
},
utils::filters,
fn inner(state: &BileState, repo_name: &RepoName) -> Result<Response> {
let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
else {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
};
// TODO: let r = req.param("ref").unwrap_or("HEAD");
let r = "HEAD";
let Some(commits) = repo.commits(r, 3)? else {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
};
diff --git a/src/handlers/repo_log.rs b/src/handlers/repo_log.rs
index d41fb39..b90cbb6 100644
use axum::{
- extract::{Path, State},
+ extract::State,
http::StatusCode,
response::{IntoResponse as _, Response},
};
git::Repository,
http::{
extractor::{ObjectName, Ref, RepoName},
+ path::Path,
response::{ErrorPage, Html, Redirect, Result},
},
utils::filters,
) -> Result<Response> {
let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
else {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
};
.commits_for_obj(r, state.config.log_per_page + 1, object_name)
.context("failed to get commits for object")?
else {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
};
diff --git a/src/handlers/repo_log_feed.rs b/src/handlers/repo_log_feed.rs
index cd306d8..86f67bf 100644
use axum::{
- extract::{Path, State},
+ extract::State,
http::StatusCode,
response::{IntoResponse as _, Response},
};
git::Repository,
http::{
extractor::{Ref, RepoName},
+ path::Path,
response::{ErrorPage, Result, Xml},
},
utils::filters,
fn inner(state: &BileState, repo_name: &RepoName, r#ref: Option<&Ref>) -> Result<Response> {
let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
else {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
};
if repo.is_empty()? {
// show a server error
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::SERVICE_UNAVAILABLE)
.into_response());
}
let r = r#ref.map_or("HEAD", |r| r.0.as_str());
let Some(commits) = repo.commits(r, state.config.log_per_page)? else {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
};
diff --git a/src/handlers/repo_refs.rs b/src/handlers/repo_refs.rs
index efff748..55f823f 100644
use axum::{
- extract::{Path, State},
+ extract::State,
http::StatusCode,
response::{IntoResponse as _, Response},
};
git::{Repository, TagEntry},
http::{
extractor::RepoName,
+ path::Path,
response::{ErrorPage, Html, Redirect, Result},
},
utils::filters,
fn inner(state: &BileState, repo_name: &RepoName) -> Result<Response> {
let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
else {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
};
diff --git a/src/handlers/repo_refs_feed.rs b/src/handlers/repo_refs_feed.rs
index 203658a..3f072a7 100644
use axum::{
- extract::{Path, State},
+ extract::State,
http::StatusCode,
response::{IntoResponse as _, Response},
};
git::{Repository, TagEntry},
http::{
extractor::RepoName,
+ path::Path,
response::{ErrorPage, Result, Xml},
},
utils::filters,
fn inner(state: &BileState, repo_name: &RepoName) -> Result<Response> {
let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
else {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
};
if repo.is_empty()? {
// show a server error
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::SERVICE_UNAVAILABLE)
.into_response());
}
diff --git a/src/handlers/repo_tag.rs b/src/handlers/repo_tag.rs
index d78a4ca..ef8643d 100644
use axum::{
- extract::{Path, State},
+ extract::State,
http::StatusCode,
response::{IntoResponse, Response},
};
git::Repository,
http::{
extractor::{RepoName, Tag},
+ path::Path,
response::{ErrorPage, Html, Redirect, Result},
},
utils::filters,
fn inner(state: &BileState, repo_name: &RepoName, tag: &Tag) -> Result<Response> {
let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
else {
- return Ok(ErrorPage::new(&state.config)
+ return Ok(ErrorPage::from(state)
.with_status(StatusCode::NOT_FOUND)
.into_response());
};
diff --git a/src/http/mod.rs b/src/http/mod.rs
index 83e27db..19d0b35 100644
pub(crate) mod extractor;
+pub(crate) mod path;
pub(crate) mod response;
use std::sync::Arc;
let this = self.clone();
- spawn_blocking(move || span.in_scope(|| wrap_err(&this.config.clone(), f(this)))).await
+ spawn_blocking(move || span.in_scope(|| wrap_err(this.clone(), f(this)))).await
}
}
.expect("failed to join spawn_blocking call, this should only happen due to a panic")
}
-fn wrap_err(config: &Config, res: Result<Response>) -> Response {
+fn wrap_err(state: BileState, res: Result<Response>) -> Response {
match res {
Ok(res) => res,
Err(err) => {
tracing::error!(err=?err, "failed to handle response");
- ErrorPage::new(config)
+ ErrorPage::from(state)
.with_status(StatusCode::INTERNAL_SERVER_ERROR)
.into_response()
}
diff --git a/src/http/path.rs b/src/http/path.rs
new file mode 100644
index 0000000..6ecda99
+use axum::extract::{FromRequestParts, path::ErrorKind, rejection::PathRejection};
+use http::{StatusCode, request::Parts};
+use serde::de::DeserializeOwned;
+
+use crate::http::{BileState, response::ErrorPage};
+
+pub(crate) struct Path<T>(pub T);
+
+impl<T> FromRequestParts<BileState> for Path<T>
+where
+ T: DeserializeOwned + Send,
+{
+ type Rejection = ErrorPage;
+
+ async fn from_request_parts(
+ parts: &mut Parts,
+ state: &BileState,
+ ) -> Result<Self, Self::Rejection> {
+ match axum::extract::Path::<T>::from_request_parts(parts, state).await {
+ Ok(value) => Ok(Self(value.0)),
+ Err(rejection) => match rejection {
+ PathRejection::FailedToDeserializePathParams(inner) => {
+ let status = match inner.kind() {
+ ErrorKind::UnsupportedType { .. } => StatusCode::INTERNAL_SERVER_ERROR,
+ _ => StatusCode::BAD_REQUEST,
+ };
+
+ Err(ErrorPage::from(state).with_status(status))
+ }
+ _ => Err(ErrorPage::from(state).with_status(StatusCode::INTERNAL_SERVER_ERROR)),
+ },
+ }
+ }
+}
diff --git a/src/http/response.rs b/src/http/response.rs
index f4d8530..9ccc45b 100644
+use std::sync::Arc;
+
use axum::{
http::{HeaderValue, StatusCode, header},
response::{IntoResponse, Response},
};
-use crate::config::Config;
+use crate::{config::Config, http::BileState};
pub(crate) type Result<T = Response, E = crate::error::Error> = std::result::Result<T, E>;
#[derive(askama::Template)]
#[template(path = "error.html")]
-pub(crate) struct ErrorPage<'c> {
- config: &'c Config,
+pub(crate) struct ErrorPage {
+ config: Arc<Config>,
status: StatusCode,
}
-impl<'c> ErrorPage<'c> {
- pub(crate) const fn new(config: &'c Config) -> Self {
+impl ErrorPage {
+ pub(crate) fn with_status(self, status: StatusCode) -> Self {
+ Self {
+ config: self.config,
+ status,
+ }
+ }
+}
+
+impl From<BileState> for ErrorPage {
+ fn from(value: BileState) -> Self {
Self {
- config,
+ config: value.config,
status: StatusCode::INTERNAL_SERVER_ERROR,
}
}
+}
- pub(crate) const fn with_status(self, status: StatusCode) -> Self {
+impl<'s> From<&'s BileState> for ErrorPage {
+ fn from(value: &'s BileState) -> Self {
Self {
- config: self.config,
- status,
+ config: value.config.clone(),
+ status: StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
-impl IntoResponse for ErrorPage<'_> {
+impl IntoResponse for ErrorPage {
fn into_response(self) -> Response {
(self.status, Html(self)).into_response()
}
diff --git a/src/utils/cache.rs b/src/utils/cache.rs
new file mode 100644
index 0000000..8b13789
+
diff --git a/src/utils/cache.rs b/src/utils/cache.rs
deleted file mode 100644
index e69de29..0000000