Compare commits
2
Commits
| 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"
|
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"
|
||||||
|
|||||||
@@ -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"] }
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
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>
|
||||||
Reference in New Issue
Block a user