Compare commits
2
Commits
a4c19b5f1f
...
a7c1663491
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7c1663491 | ||
|
|
6b56bdc2d8 |
Generated
+37
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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:
|
||||
|
||||
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
|
||||
|
||||
@@ -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 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
|
||||
|
||||
@@ -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
@@ -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
@@ -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<PathBuf>,
|
||||
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)))
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user