diff --git a/Cargo.lock b/Cargo.lock index 1c2f608..93d55a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -307,6 +307,17 @@ version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" +[[package]] +name = "console" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" +dependencies = [ + "encode_unicode", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -368,6 +379,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -425,8 +442,10 @@ dependencies = [ "futures", "http", "http-body-util", + "insta", "reqwest", "serde", + "serde_json", "tempfile", "tokio", "tower-http 0.7.0", @@ -827,6 +846,18 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "insta" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" +dependencies = [ + "console", + "once_cell", + "similar", + "tempfile", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -1561,6 +1592,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "slab" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 5a0fc99..88027ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,10 @@ clap = { version = "4.6.5", features = ["derive", "env"] } futures = "0.3.33" http = "1.5.0" http-body-util = "0.1.4" +insta = "1.48.0" reqwest = "0.13.4" serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.151" tempfile = "3.27.0" tokio = { version = "1.53.1", features = ["full"] } tower-http = { version = "0.7.0", features = ["compression-gzip", "fs", "trace"] } diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..fda8c28 --- /dev/null +++ b/src/index.html @@ -0,0 +1,68 @@ + + + + + + +

Flatpak Repository

+ + + + + + + + + + + + + + + {list} +
+ Applications in repository +
NameApp IDVersionBranchRuntimeSize (Installed)Size (Download)Arch
+ + diff --git a/src/index.rs b/src/index.rs new file mode 100644 index 0000000..1b68e3f --- /dev/null +++ b/src/index.rs @@ -0,0 +1,132 @@ +use axum::{extract::State, response::Html}; +use http::StatusCode; +use serde::Deserialize; +use tokio::process::Command; + +use crate::SharedState; + +#[derive(Clone, Debug, Deserialize)] +struct FlatpakApp { + name: String, + application_id: String, + version: String, + branch: String, + runtime: String, + installed_size: String, + download_size: String, + arch: String, +} + +pub async fn index(State(state): State) -> Result, StatusCode> { + let output = { + let repo = state.repo.lock().await; + Command::new("flatpak") + .arg("remote-ls") + .arg("--json") + .arg("--arch=*") + .arg("--columns=name,description,application,version,branch,runtime,installed-size,download-size,arch") + .arg(format!("file://{}", repo.display())) + .output() + .await + .map_err(|e| { + tracing::error!("Failed to execute `flatpak remote-ls`: {e:?}"); + StatusCode::INTERNAL_SERVER_ERROR + })? + }; + + let stdout = String::from_utf8_lossy(&output.stdout); + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + tracing::error!("`flatpak remote-ls` returned {}", output.status); + tracing::error!("stdout:\n{stdout}\n"); + tracing::error!("stderr:\n{stderr}\n"); + } + + let list: Vec = serde_json::from_str(&stdout).map_err(|e| { + tracing::error!("Failed to deserialize `flatpak remote-ls --json`'s output: {e:?}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + Ok(to_html(&list)) +} + +fn to_html(apps: &[FlatpakApp]) -> Html { + let list: String = apps + .into_iter() + .map(|app| { + let FlatpakApp { + name, + application_id, + version, + branch, + runtime, + installed_size, + download_size, + arch, + } = app; + format!( + "\n\t\t +\t\t\t +\t\t\t\t{name} +\t\t\t\t{application_id} +\t\t\t\t{version} +\t\t\t\t{branch} +\t\t\t\t{runtime} +\t\t\t\t{installed_size} +\t\t\t\t{download_size} +\t\t\t\t{arch} +\t\t\t +\t\t" + ) + }) + .collect(); + + Html(include_str!("index.html").replace("{list}", &list)) +} + +#[cfg(test)] +mod test { + use crate::index::{FlatpakApp, to_html}; + use axum::response::Html; + + /// Example output from `flatpak remote-ls --json --arch=* --columns=<...>` + const REMOTE_LS_OUTPUT: &str = r#" + [ + { + "name" : "Foobar", + "description" : "An app", + "application_id" : "org.example.Foobar", + "version" : "", + "branch" : "master", + "runtime" : "org.freedesktop.Platform/x86_64/25.08", + "installed_size" : "103.6 MB", + "download_size" : "43.0 MB", + "arch" : "x86_64" + }, + { + "name" : "Barfoo", + "description" : "", + "application_id" : "com.example.Barfoo", + "version" : "1.2.3", + "branch" : "master", + "runtime" : "org.freedesktop.Platform/aarch64/25.08", + "installed_size" : "14.8 MB", + "download_size" : "6.2 MB", + "arch" : "aarch64" + } + ] + "#; + + #[test] + fn deserialize_remote_ls() { + let list: Vec = serde_json::from_str(&REMOTE_LS_OUTPUT).unwrap(); + assert_eq!(&list[0].name, "Foobar"); + } + + #[test] + fn render_html() { + let list: Vec = serde_json::from_str(&REMOTE_LS_OUTPUT).unwrap(); + let Html(html) = to_html(&list); + insta::assert_snapshot!(html); + } +} diff --git a/src/main.rs b/src/main.rs index 2e5d966..3e44861 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,11 +13,13 @@ use tower_http::{compression::CompressionLayer, services::ServeDir, trace::Trace use tracing_subscriber::EnvFilter; mod flatpak; +mod index; mod upload; mod util; #[derive(Default)] struct SrvState { + /// Absolute path to the flatpak ostree repository repo: Mutex, api_key: String, gpg_key_id: String, @@ -59,7 +61,10 @@ async fn main() -> anyhow::Result<()> { // 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 = opt + .repo + .canonicalize() + .context("Failed to canonicalize repo dir")?; let repo_looks_valid = repo.is_dir() && repo.join("objects").is_dir() && repo.join("refs").is_dir(); if !repo_looks_valid { @@ -76,6 +81,8 @@ async fn main() -> anyhow::Result<()> { let state: SharedState = Arc::new(state); let app = Router::new() + .route("/", get(index::index)) + .route("/index.html", get(index::index)) .route("/flatpak-bundle/upload", post(upload::flatpak_bundle)) // Explicitly deny access to internal OSTree directories. .route("/state/{*path}", get(|| ready(StatusCode::NOT_FOUND))) diff --git a/src/snapshots/flatpak_repo__index__test__render_html.snap b/src/snapshots/flatpak_repo__index__test__render_html.snap new file mode 100644 index 0000000..7f5205c --- /dev/null +++ b/src/snapshots/flatpak_repo__index__test__render_html.snap @@ -0,0 +1,96 @@ +--- +source: src/index.rs +expression: html +--- + + + + + + +

Flatpak Repository

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Applications in repository +
NameApp IDVersionBranchRuntimeSize (Installed)Size (Download)Arch
Foobarorg.example.Foobarmasterorg.freedesktop.Platform/x86_64/25.08103.6 MB43.0 MBx86_64
Barfoocom.example.Barfoo1.2.3masterorg.freedesktop.Platform/aarch64/25.0814.8 MB6.2 MBaarch64
+ +