use std::collections::HashMap;
use std::path::{Path, PathBuf};
use pijul_srv_lite::parse_url;
use tiny_http::{Header, Response, StatusCode};
const SERVER_NAME: &str = "pijul-srv-lite/0.1.0";
fn main() {
let port = std::env::var("PIJUL_SRV_PORT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(8080);
let repo_base = std::env::var("PIJUL_SRV_REPO_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("/vcs/pijul"));
let repo_path = if repo_base.join(".pijul").exists() {
repo_base
} else {
repo_base.join("default")
};
if !repo_path.join(".pijul").exists() {
eprintln!("Repository not found at {:?}", repo_path);
std::process::exit(1);
}
let server = tiny_http::Server::http(format!("0.0.0.0:{port}")).unwrap();
println!("{SERVER_NAME} listening on port {port}");
println!("Serving repository from {:?}", repo_path);
for request in server.incoming_requests() {
if request.method() == &tiny_http::Method::Head {
let _ = request.respond(Response::empty(200));
continue;
}
let response = handle_request(request.url(), &repo_path);
let _ = request.respond(response);
}
}
fn handle_request(url: &str, repo_path: &Path) -> Response<std::io::Cursor<Vec<u8>>> {
let (path, query) = parse_url(url);
if !path.starts_with(".pijul") {
return not_found("not found");
}
if query.is_empty() {
return serve_info(repo_path);
}
if let Some(change) = query.get("change") {
return serve_change(repo_path, change);
}
if query.get("changelist").map(String::as_str) == Some("0")
&& let Some(channel) = query.get("channel")
{
return serve_changelist(repo_path, channel);
}
if let Some(channel) = query.get("channel") {
if query.contains_key("id") {
return serve_channel_id(repo_path, channel);
}
return serve_channel(repo_path, channel);
}
if query.contains_key("state") {
return serve_state(repo_path);
}
if query.contains_key("identities") {
return serve_identities();
}
if query.contains_key("log") {
return serve_info(repo_path);
}
if query.contains_key("tag") {
return serve_tag(repo_path);
}
bad_request()
}
fn serve_channel_id(repo_path: &Path, _channel: &str) -> Response<std::io::Cursor<Vec<u8>>> {
let path = repo_path.join(".pijul").join("pristine").join("db");
serve_file(&path, "application/octet-stream")
}
fn serve_channel(repo_path: &Path, _channel: &str) -> Response<std::io::Cursor<Vec<u8>>> {
let path = repo_path.join(".pijul").join("pristine").join("db");
serve_file(&path, "application/octet-stream")
}
fn serve_changelist(repo_path: &Path, channel: &str) -> Response<std::io::Cursor<Vec<u8>>> {
let path = repo_path.join("changelists").join(channel);
serve_file(&path, "text/plain")
}
fn serve_change(repo_path: &Path, hash: &str) -> Response<std::io::Cursor<Vec<u8>>> {
if hash.len() < 3 || !hash.chars().all(|c| c.is_ascii_alphanumeric()) {
return not_found("invalid change hash");
}
let (pfx, sfx) = hash.split_at(2);
let path = repo_path
.join(".pijul")
.join("changes")
.join(pfx)
.join(format!("{}.change", sfx));
serve_file(&path, "application/octet-stream")
}
fn serve_state(repo_path: &Path) -> Response<std::io::Cursor<Vec<u8>>> {
let path = repo_path.join(".pijul").join("pristine").join("db");
serve_file(&path, "application/octet-stream")
}
fn serve_tag(repo_path: &Path) -> Response<std::io::Cursor<Vec<u8>>> {
let path = repo_path.join(".pijul").join("pristine").join("db");
serve_file(&path, "application/octet-stream")
}
fn serve_identities() -> Response<std::io::Cursor<Vec<u8>>> {
ok_json(r#"{"id":[],"rev":0}"#)
}
fn serve_info(repo_path: &Path) -> Response<std::io::Cursor<Vec<u8>>> {
let mut out = String::from("{");
let path = repo_path.join("changelists");
if let Ok(entries) = std::fs::read_dir(path) {
let names: Vec<String> = entries
.filter_map(|e| e.ok().and_then(|e| e.file_name().into_string().ok()))
.map(|s| format!("\"{s}\""))
.collect();
if !names.is_empty() {
out.push_str("\"channels\":[");
out.push_str(&names.join(","));
out.push_str("],");
}
}
out.push_str("\"head\":null}");
ok_json(&out)
}
fn serve_file(path: &Path, ty: &str) -> Response<std::io::Cursor<Vec<u8>>> {
match std::fs::read(path) {
Ok(data) => Response::from_data(data)
.with_status_code(StatusCode(200))
.with_header(Header::from_bytes("Content-Type", ty).unwrap())
.with_header(
Header::from_bytes("Cache-Control", "public, max-age=31536000, immutable").unwrap(),
),
Err(_) => not_found("not found"),
}
}
fn ok_bin(data: Vec<u8>) -> Response<std::io::Cursor<Vec<u8>>> {
Response::from_data(data)
.with_status_code(StatusCode(200))
.with_header(Header::from_bytes("Content-Type", "application/octet-stream").unwrap())
}
fn ok_text(s: String) -> Response<std::io::Cursor<Vec<u8>>> {
Response::from_data(s)
.with_status_code(StatusCode(200))
.with_header(Header::from_bytes("Content-Type", "text/plain").unwrap())
}
fn ok_json(s: &str) -> Response<std::io::Cursor<Vec<u8>>> {
Response::from_string(s)
.with_status_code(StatusCode(200))
.with_header(Header::from_bytes("Content-Type", "application/json").unwrap())
}
fn bad_request() -> Response<std::io::Cursor<Vec<u8>>> {
Response::from_string("bad request\n").with_status_code(StatusCode(400))
}
fn not_found(msg: &str) -> Response<std::io::Cursor<Vec<u8>>> {
Response::from_string(format!("{msg}\n")).with_status_code(StatusCode(404))
}
fn internal_error() -> Response<std::io::Cursor<Vec<u8>>> {
Response::from_string("internal error\n").with_status_code(StatusCode(500))
}