use std::{future::ready, net::SocketAddr, path::PathBuf, sync::Arc}; use anyhow::{Context, bail}; use axum::{ Router, http::StatusCode, middleware::{self}, routing::{get, post}, }; use clap::Parser; use tokio::sync::Mutex; use tower_http::{compression::CompressionLayer, services::ServeDir, trace::TraceLayer}; use tracing_subscriber::EnvFilter; mod flatpak; mod upload; mod util; #[derive(Default)] struct SrvState { repo: Mutex, } type SharedState = Arc; #[derive(Parser)] struct Opt { /// IP and port to bind to. #[clap(long, env = "FR_BIND", default_value = "127.0.0.1:3000")] bind: SocketAddr, /// Path to the ostree repository directory #[clap(long, env = "FR_REPO_PATH")] repo: PathBuf, #[clap(long, env = "RUST_LOG", default_value = "debug")] log_level: String, } #[tokio::main] async fn main() -> anyhow::Result<()> { let opt = Opt::parse(); tracing_subscriber::fmt() .with_env_filter(EnvFilter::new(&opt.log_level)) .init(); // TODO: Create an empty archive-z2 repository // ostree init --repo=~/my-apps --mode=archive-z2 // Sanity check that opt.repo looks like a valid ostree dir let repo = opt.repo; let repo_looks_valid = repo.is_dir() && repo.join("objects").is_dir() && repo.join("refs").is_dir(); if !repo_looks_valid { bail!("{repo:?} must be a valid ostree directory.") } let serve_dir = ServeDir::new(&repo); let state = SrvState { repo: Mutex::new(repo.clone()), }; let state: SharedState = Arc::new(state); let app = Router::new() .route("/flatpak-bundle/upload", post(upload::flatpak_bundle)) // Explicitly deny access to internal OSTree directories. .route("/state/{*path}", get(|| ready(StatusCode::NOT_FOUND))) .route("/tmp/{*path}", get(|| ready(StatusCode::NOT_FOUND))) // Optional: serve a .flatpakrepo descriptor at a known endpoint. .route( "/repo.flatpakrepo", get(move || flatpak::serve_flatpakrepo(repo.clone())), ) // Everything else is served from the repo directory. .fallback_service(serve_dir) // Add Flatpak-specific cache headers and content types. .layer(middleware::from_fn(flatpak::flatpak_headers_middleware)) // Gzip compression for refs and small files. .layer(CompressionLayer::new()) // Request logging. .layer(TraceLayer::new_for_http()) .with_state(state); let listener = tokio::net::TcpListener::bind(&opt.bind) .await .context("Failed to bind tcp listener")?; tracing::info!("Listening on http://{}", opt.bind); axum::serve(listener, app).await?; Ok(()) }