#![forbid(unsafe_code)]
use actix_web::{
get,
http::header::{HeaderValue, LOCATION},
middleware, web, App, HttpResponse, HttpServer, Responder,
};
use clap::Parser;
use env_logger::Env;
use git2::{ReferenceType, Repository};
use log::error;
use std::collections::HashMap;
use std::sync::Mutex;
fn ensure_repo(repos_dir: &str, username: &str, repo_name: &str) -> RepoOr404 {
let not_found = lilgit::render_404(
format!("404 - {}", repo_name),
format!("Could not find repo {}", repo_name),
);
let not_found_result = RepoOr404::R404(not_found);
if repo_name.contains("/") {
return not_found_result;
}
if repo_name.contains("\\") {
return not_found_result;
}
let repo = Repository::open(&format!("{}/{}/{}", repos_dir, username, repo_name));
repo.map_or_else(|_| not_found_result, |v| RepoOr404::Repo(v))
}
enum RepoOr404 {
Repo(Repository),
R404(String),
}
#[get("/~{username}/{repo_name}/info/refs")]
async fn get_info_refs(
app_data: web::Data<lilgit::AppState>,
path: web::Path<(String, String)>,
) -> impl Responder {
let (username, repo_name) = path.into_inner();
if let Some(response) = lilgit::guard_username_configured(&username, &app_data) {
return response;
}
let repo = match ensure_repo(&app_data.config.repos_dir, &username, &repo_name) {
RepoOr404::Repo(r) => r,
RepoOr404::R404(r) => {
return HttpResponse::NotFound().body(r);
}
};
let references = match repo.references() {
Ok(x) => x,
Err(x) => {
error!("Error rendering index listing: {:?}", x);
return HttpResponse::InternalServerError().body("Error rendering index listing");
}
};
let branch_references_str = references.fold("".to_string(), |acc, curr| {
let reference = match curr {
Ok(x) => x,
_ => return acc,
};
if !reference.is_branch() {
return acc;
}
if reference.kind() != Some(ReferenceType::Direct) {
return acc;
}
let target = match reference.target() {
Some(x) => x,
_ => return acc,
};
let name = match reference.name() {
Some(x) => x,
_ => return acc,
};
format!("{}\n{}\t{}", acc, target, name)
});
HttpResponse::Ok().body(format!("{}\n", branch_references_str.trim()))
}
#[get("/~{username}/{repo_name}/HEAD")]
async fn get_head(
app_data: web::Data<lilgit::AppState>,
path: web::Path<(String, String)>,
) -> impl Responder {
let (username, repo_name) = path.into_inner();
if let Some(response) = lilgit::guard_username_configured(&username, &app_data) {
return response;
}
let repo = match ensure_repo(&app_data.config.repos_dir, &username, &repo_name) {
RepoOr404::Repo(r) => r,
RepoOr404::R404(r) => {
return HttpResponse::NotFound().body(r);
}
};
let s500 = HttpResponse::InternalServerError().body("Error getting repo head");
let head = match repo.head() {
Ok(x) => x,
Err(x) => {
error!("Error getting repo head: {:?}", x);
return s500;
}
};
if !head.is_branch() {
return s500;
}
if head.kind() != Some(ReferenceType::Direct) {
return s500;
}
let name = match head.name() {
Some(x) => x,
_ => return s500,
};
HttpResponse::Ok().body(format!("ref: {}\n", name))
}
#[get("/~{username}/{repo_name}")]
async fn get_repo(
app_data: web::Data<lilgit::AppState>,
path: web::Path<(String, String)>,
) -> impl Responder {
let (username, repo_name) = path.into_inner();
if let Some(response) = lilgit::guard_username_configured(&username, &app_data) {
return response;
}
let repo = match ensure_repo(&app_data.config.repos_dir, &username, &repo_name) {
RepoOr404::Repo(r) => r,
RepoOr404::R404(r) => {
return HttpResponse::NotFound().body(r);
}
};
let repo_config = app_data.config.get_repo_config(&username, &repo_name);
match lilgit::listing::render_repo_index(
&repo,
&username,
&repo_name,
repo_config,
app_data.config.folder_depth,
) {
Ok(x) => HttpResponse::Ok().content_type("text/html").body(x),
Err(x) => {
error!("Error rendering index listing: {:?}", x);
HttpResponse::InternalServerError().body("Error rendering index listing")
}
}
}
#[get("/~{username}/{repo_name}/tree/{tail:.*}")]
async fn get_file(
app_data: web::Data<lilgit::AppState>,
path: web::Path<(String, String, String)>,
) -> impl Responder {
let (username, repo_name, file_path) = path.into_inner();
let repo = match ensure_repo(&app_data.config.repos_dir, &username, &repo_name) {
RepoOr404::Repo(r) => r,
RepoOr404::R404(r) => {
return HttpResponse::NotFound().body(r);
}
};
if let Ok(obj) = repo.revparse_single(&format!("HEAD:{}", file_path)) {
if obj.kind() == Some(git2::ObjectType::Tree) {
let render_result = lilgit::listing::render_repo_folder(
&repo,
&username,
&repo_name,
app_data.config.folder_depth,
&file_path,
);
return match render_result {
Ok(x) => HttpResponse::Ok().content_type("text/html").body(x),
Err(_) => {
return HttpResponse::NotFound().body("404 not found");
}
};
}
if obj.kind() == Some(git2::ObjectType::Blob) {
if let Some(blob) = obj.as_blob() {
if blob.is_binary() {
return lilgit::file::render_binary_object(blob, &file_path);
}
}
}
};
let render_result = lilgit::file::render_file(&repo, &username, &repo_name, &file_path);
match render_result {
Ok(x) => HttpResponse::Ok().content_type("text/html").body(x),
Err(_) => HttpResponse::NotFound().body(lilgit::render_404(
format!("Could not render {}", file_path),
String::from(""),
)),
}
}
#[get("/~{username}/{repo_name}/tree")]
async fn get_tree_redirect(path: web::Path<(String, String)>) -> impl Responder {
let (username, repo_name) = path.into_inner();
let mut redirect = HttpResponse::PermanentRedirect().body(());
let header_value = HeaderValue::from_str(&format!("/~{}/{}", &username, &repo_name))
.unwrap_or(HeaderValue::from_static("/"));
redirect.headers_mut().insert(LOCATION, header_value);
redirect
}
/// Lilgit repo server
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Port to bind to
#[arg(short, long, default_value_t = 8080)]
port: u16,
/// Ipv4 address to bind to
#[arg(short, long, default_value_t = String::from("127.0.0.1"))]
ip: String,
/// folder containing repositories
#[arg(short, long, default_value_t = String::from("./"))]
repos_dir: String,
/// Optional max number of folders to render at one time
#[arg(short, long)]
folder_depth: Option<usize>,
/// Optional config file providing extra metadata like descriptions of repos
#[arg(short, long)]
config: Option<String>,
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let args = Args::parse();
env_logger::init_from_env(Env::default().default_filter_or("info"));
let config_path = args.config.clone();
let app_data = web::Data::new(lilgit::AppState {
config: lilgit::get_config(&config_path),
line_counts: Mutex::new(HashMap::new()),
});
HttpServer::new(move || {
App::new()
.wrap(middleware::NormalizePath::trim())
.wrap(middleware::Compress::default())
.wrap(middleware::Logger::default())
.app_data(app_data.clone())
.service(lilgit::get_index)
.service(get_info_refs)
.service(get_repo)
.service(lilgit::user::get_repos)
.service(lilgit::r#static::get_static_asset)
.service(get_file)
.service(get_tree_redirect)
.service(get_head)
})
.bind((args.ip, args.port))?
.run()
.await
}