Compare commits

..
2 Commits
Author SHA1 Message Date
hulthe a7c1663491 Expand README.md 2026-08-27 23:08:30 +02:00
hulthe 6b56bdc2d8 Add html index page that lists repo contents 2026-08-27 23:08:10 +02:00
7 changed files with 363 additions and 4 deletions
Generated
+37
View File
@@ -307,6 +307,17 @@ version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" 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]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@@ -368,6 +379,12 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.35" version = "0.8.35"
@@ -425,8 +442,10 @@ dependencies = [
"futures", "futures",
"http", "http",
"http-body-util", "http-body-util",
"insta",
"reqwest", "reqwest",
"serde", "serde",
"serde_json",
"tempfile", "tempfile",
"tokio", "tokio",
"tower-http 0.7.0", "tower-http 0.7.0",
@@ -827,6 +846,18 @@ dependencies = [
"hashbrown", "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]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.12.0" version = "2.12.0"
@@ -1561,6 +1592,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "similar"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.12" version = "0.4.12"
+2
View File
@@ -10,8 +10,10 @@ clap = { version = "4.6.5", features = ["derive", "env"] }
futures = "0.3.33" futures = "0.3.33"
http = "1.5.0" http = "1.5.0"
http-body-util = "0.1.4" http-body-util = "0.1.4"
insta = "1.48.0"
reqwest = "0.13.4" reqwest = "0.13.4"
serde = { version = "1.0.229", features = ["derive"] } serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"
tempfile = "3.27.0" tempfile = "3.27.0"
tokio = { version = "1.53.1", features = ["full"] } tokio = { version = "1.53.1", features = ["full"] }
tower-http = { version = "0.7.0", features = ["compression-gzip", "fs", "trace"] } tower-http = { version = "0.7.0", features = ["compression-gzip", "fs", "trace"] }
+20 -3
View File
@@ -4,7 +4,17 @@ A minimal HTTP server for hosting an OSTree/Flatpak repository, written in Rust
Includes a non-standard route for uploading flatpak bundles: Includes a non-standard route for uploading flatpak bundles:
curl -v -XPOST -T ./org.example.Example.aarch64.flatpak http://localhost:3000/flatpak-bundle/upload curl -v -XPOST -T \
./org.example.Example.aarch64.flatpak \
http://localhost:3000/flatpak-bundle/upload?key=<api-key>
Set the api key using `FR_API_KEY=<api-key>`.
See `flatpak-repo --help` for a full list of env/cli arguments.
## Building
docker build . -t flatpak-repo
## Missing features ## Missing features
@@ -14,8 +24,15 @@ flatpak-repo does not generate an ostree folder for you. Create one like this:
flatpak-repo does not generate gpg keys for you. flatpak-repo does not generate gpg keys for you.
flatpak-repo does not generate a `flatpakrepo` file for you, but expects one to be places in the repo folder. flatpak-repo does not generate a `flatpakrepo` file for you, but expects one to be placed in the repo folder root.
add the flatpak remo like this: Example `flatpakremote`:
[Flatpak Repo]
Title=example.com
Url=https://flatpak-repo.example.com
GPGKey=<base64-encoded gpg public key>
add the flatpak remote like this:
sudo flatpak remote-add --from <name> https://<url>/flatpakrepo sudo flatpak remote-add --from <name> https://<url>/flatpakrepo
+68
View File
@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html>
<head>
<style>
table {
border-collapse: collapse;
border: 2px solid rgb(140 140 140);
font-family: sans-serif;
font-size: 0.8rem;
letter-spacing: 1px;
}
caption {
caption-side: bottom;
padding: 10px;
font-weight: bold;
}
thead,
tfoot {
background-color: rgb(228 240 245);
}
th,
td {
border: 1px solid rgb(160 160 160);
padding: 8px 10px;
}
td:last-of-type {
text-align: center;
}
tbody > tr:nth-of-type(even) {
background-color: rgb(237 238 242);
}
tfoot th {
text-align: right;
}
tfoot td {
font-weight: bold;
}
</style>
</head>
<body>
<h1>Flatpak Repository</h1>
<table>
<caption>
Applications in repository
</caption>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">App ID</th>
<th scope="col">Version</th>
<th scope="col">Branch</th>
<th scope="col">Runtime</th>
<th scope="col">Size (Installed)</th>
<th scope="col">Size (Download)</th>
<th scope="col">Arch</th>
</tr>
</thead>
{list}
</table>
</body>
</html>
+132
View File
@@ -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<SharedState>) -> Result<Html<String>, 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<FlatpakApp> = 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<String> {
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<tbody>
\t\t\t<tr>
\t\t\t\t<th scope=\"row\">{name}</th>
\t\t\t\t<td>{application_id}</td>
\t\t\t\t<td>{version}</td>
\t\t\t\t<td>{branch}</td>
\t\t\t\t<td>{runtime}</td>
\t\t\t\t<td>{installed_size}</td>
\t\t\t\t<td>{download_size}</td>
\t\t\t\t<td>{arch}</td>
\t\t\t</tr>
\t\t</tbody>"
)
})
.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<FlatpakApp> = serde_json::from_str(&REMOTE_LS_OUTPUT).unwrap();
assert_eq!(&list[0].name, "Foobar");
}
#[test]
fn render_html() {
let list: Vec<FlatpakApp> = serde_json::from_str(&REMOTE_LS_OUTPUT).unwrap();
let Html(html) = to_html(&list);
insta::assert_snapshot!(html);
}
}
+8 -1
View File
@@ -13,11 +13,13 @@ use tower_http::{compression::CompressionLayer, services::ServeDir, trace::Trace
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
mod flatpak; mod flatpak;
mod index;
mod upload; mod upload;
mod util; mod util;
#[derive(Default)] #[derive(Default)]
struct SrvState { struct SrvState {
/// Absolute path to the flatpak ostree repository
repo: Mutex<PathBuf>, repo: Mutex<PathBuf>,
api_key: String, api_key: String,
gpg_key_id: String, gpg_key_id: String,
@@ -59,7 +61,10 @@ async fn main() -> anyhow::Result<()> {
// ostree init --repo=~/my-apps --mode=archive-z2 // ostree init --repo=~/my-apps --mode=archive-z2
// Sanity check that opt.repo looks like a valid ostree dir // 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 = let repo_looks_valid =
repo.is_dir() && repo.join("objects").is_dir() && repo.join("refs").is_dir(); repo.is_dir() && repo.join("objects").is_dir() && repo.join("refs").is_dir();
if !repo_looks_valid { if !repo_looks_valid {
@@ -76,6 +81,8 @@ async fn main() -> anyhow::Result<()> {
let state: SharedState = Arc::new(state); let state: SharedState = Arc::new(state);
let app = Router::new() let app = Router::new()
.route("/", get(index::index))
.route("/index.html", get(index::index))
.route("/flatpak-bundle/upload", post(upload::flatpak_bundle)) .route("/flatpak-bundle/upload", post(upload::flatpak_bundle))
// Explicitly deny access to internal OSTree directories. // Explicitly deny access to internal OSTree directories.
.route("/state/{*path}", get(|| ready(StatusCode::NOT_FOUND))) .route("/state/{*path}", get(|| ready(StatusCode::NOT_FOUND)))
@@ -0,0 +1,96 @@
---
source: src/index.rs
expression: html
---
<!DOCTYPE html>
<html>
<head>
<style>
table {
border-collapse: collapse;
border: 2px solid rgb(140 140 140);
font-family: sans-serif;
font-size: 0.8rem;
letter-spacing: 1px;
}
caption {
caption-side: bottom;
padding: 10px;
font-weight: bold;
}
thead,
tfoot {
background-color: rgb(228 240 245);
}
th,
td {
border: 1px solid rgb(160 160 160);
padding: 8px 10px;
}
td:last-of-type {
text-align: center;
}
tbody > tr:nth-of-type(even) {
background-color: rgb(237 238 242);
}
tfoot th {
text-align: right;
}
tfoot td {
font-weight: bold;
}
</style>
</head>
<body>
<h1>Flatpak Repository</h1>
<table>
<caption>
Applications in repository
</caption>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">App ID</th>
<th scope="col">Version</th>
<th scope="col">Branch</th>
<th scope="col">Runtime</th>
<th scope="col">Size (Installed)</th>
<th scope="col">Size (Download)</th>
<th scope="col">Arch</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Foobar</th>
<td>org.example.Foobar</td>
<td></td>
<td>master</td>
<td>org.freedesktop.Platform/x86_64/25.08</td>
<td>103.6 MB</td>
<td>43.0 MB</td>
<td>x86_64</td>
</tr>
</tbody>
<tbody>
<tr>
<th scope="row">Barfoo</th>
<td>com.example.Barfoo</td>
<td>1.2.3</td>
<td>master</td>
<td>org.freedesktop.Platform/aarch64/25.08</td>
<td>14.8 MB</td>
<td>6.2 MB</td>
<td>aarch64</td>
</tr>
</tbody>
</table>
</body>
</html>