Skip to main content

upstream_rs/services/integration/
appimage_extractor.rs

1use crate::services::integration::permission_handler;
2use anyhow::{Result, anyhow};
3use std::fs::{self, File};
4use std::path::{Path, PathBuf};
5use tokio::process::Command;
6
7macro_rules! message {
8    ($cb:expr, $($arg:tt)*) => {{
9        if let Some(cb) = $cb.as_mut() {
10            cb(&format!($($arg)*));
11        }
12    }};
13}
14
15pub struct AppImageExtractor {
16    extract_cache: PathBuf,
17}
18
19impl AppImageExtractor {
20    pub fn new() -> Result<Self> {
21        let temp_path = std::env::temp_dir().join(format!("upstream-{}", std::process::id()));
22        let extract_cache = temp_path.join("appimage_extract");
23        fs::create_dir_all(&extract_cache)?;
24        Ok(Self { extract_cache })
25    }
26
27    /// Extract an AppImage and return the path to squashfs-root.
28    /// Caches by name — calling twice with the same name skips re-extraction.
29    pub async fn extract<H>(
30        &self,
31        name: &str,
32        appimage_path: &Path,
33        message_callback: &mut Option<H>,
34    ) -> Result<PathBuf>
35    where
36        H: FnMut(&str),
37    {
38        let extract_path = self.extract_cache.join(name);
39        let squashfs_root = extract_path.join("squashfs-root");
40
41        // Already extracted this session — reuse it.
42        if squashfs_root.exists() {
43            message!(message_callback, "Using cached extraction for '{}'", name);
44            return Ok(squashfs_root);
45        }
46
47        fs::create_dir_all(&extract_path)?;
48
49        let temp_appimage = extract_path.join("appimage");
50        fs::copy(appimage_path, &temp_appimage)?;
51        permission_handler::make_executable(&temp_appimage)?;
52
53        message!(message_callback, "Extracting AppImage ...");
54
55        let status = Command::new(&temp_appimage)
56            .arg("--appimage-extract")
57            .current_dir(&extract_path)
58            .stdout(File::open("/dev/null")?)
59            .stderr(File::open("/dev/null")?)
60            .status()
61            .await?;
62
63        // Clean up the copied appimage — we only needed it to run the extract.
64        let _ = fs::remove_file(&temp_appimage);
65
66        if !status.success() {
67            return Err(anyhow!("AppImage extraction failed with status {}", status));
68        }
69
70        if !squashfs_root.exists() {
71            return Err(anyhow!("Extraction completed but squashfs-root not found"));
72        }
73
74        message!(message_callback, "AppImage extracted");
75        Ok(squashfs_root)
76    }
77}
78
79impl Drop for AppImageExtractor {
80    fn drop(&mut self) {
81        let _ = fs::remove_dir_all(&self.extract_cache);
82    }
83}