use anyhow::Context; use axum::{body::Body, extract::State, http::StatusCode}; use futures::{StreamExt, future::ready, stream}; use tempfile::NamedTempFile; use tokio::{io::AsyncWriteExt, process::Command, task::block_in_place}; use crate::{SharedState, util::e}; pub async fn flatpak_bundle( State(state): State, body: Body, ) -> Result<(), StatusCode> { let mut stream = body.into_data_stream(); // Get first chunks let Some(Ok(first_chunk)) = stream.next().await else { tracing::warn!("Unexpected EOF"); return Err(StatusCode::BAD_REQUEST); }; // Sanity check that it looks like a flatpak // Technically not correct to require that the first chunk is 7 bytes or more, but who cares. if !first_chunk.starts_with(b"flatpak") { tracing::warn!("Not a valid flatpak bundle. Missing 'flatpak' magic string."); return Err(StatusCode::BAD_REQUEST); } // Create a tmpfile let (tmp_path, mut tmp_file) = block_in_place(|| { let mut file = NamedTempFile::new()?; file.disable_cleanup(true); let path = file.path().to_path_buf(); let file = tokio::fs::File::from_std(file.into_file()); std::io::Result::Ok((path, file)) }) .context("Failed to create tmpfile") .map_err(e(StatusCode::BAD_REQUEST))?; // Write bundle to tmpfile tracing::info!("Downloading flatpak to {tmp_path:?}"); let mut stream = stream::once(ready(Ok(first_chunk))).chain(stream); loop { let Some(chunk) = stream.next().await else { break; }; let chunk = chunk .context("Stream error") .map_err(e(StatusCode::BAD_REQUEST))?; tmp_file .write_all(&chunk) .await .context("Tmpfile write error") .map_err(e(StatusCode::INTERNAL_SERVER_ERROR))?; } let repo = state.repo.lock().await; // Import bundle into repository tracing::info!("Importing {tmp_path:?} into repo {:?}", &*repo); let output = Command::new("flatpak") .arg("build-import-bundle") .args([&*repo, &tmp_path]) .output() .await .context("Failed to execute flatpak build-import-bundle") .map_err(e(StatusCode::INTERNAL_SERVER_ERROR))?; if !output.status.success() { tracing::error!("flatpak build-import-bundle failed"); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); for line in stdout.lines() { tracing::error!("[stdout] {line}"); } for line in stderr.lines() { tracing::error!("[stderr] {line}"); } return Err(StatusCode::INTERNAL_SERVER_ERROR); } drop(tmp_file); if let Err(e) = tokio::fs::remove_file(&tmp_path).await { tracing::warn!("Failed to remove tmpfile at {tmp_path:?}: {e}"); } tracing::info!("Success"); Ok(()) }