Add html index page that lists repo contents
This commit is contained in:
@@ -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