Skip to main content

stout_cask/
install.rs

1//! Cask installation logic
2
3use crate::download::{download_cask_artifact, ArtifactType};
4use crate::error::{Error, Result};
5use crate::state::{now_timestamp, InstalledArtifact, InstalledCask, InstalledCasks};
6use crate::detect_artifact_type;
7use stout_index::Cask;
8use std::path::{Path, PathBuf};
9use tracing::{debug, info, warn};
10
11/// Options for cask installation
12#[derive(Debug, Clone, Default)]
13pub struct CaskInstallOptions {
14    /// Force reinstall even if already installed
15    pub force: bool,
16    /// Skip checksum verification
17    pub no_verify: bool,
18    /// Custom application directory (default: /Applications)
19    pub appdir: Option<PathBuf>,
20    /// Dry run - don't actually install
21    pub dry_run: bool,
22}
23
24/// Install a cask
25pub async fn install_cask(
26    cask: &Cask,
27    cache_dir: &Path,
28    state_path: &Path,
29    options: &CaskInstallOptions,
30) -> Result<PathBuf> {
31    let token = &cask.token;
32
33    // Check if already installed
34    let mut installed_casks = InstalledCasks::load(state_path)?;
35    if installed_casks.is_installed(token) && !options.force {
36        return Err(Error::InstallFailed(format!(
37            "{} is already installed. Use --force to reinstall.",
38            token
39        )));
40    }
41
42    // Get download URL
43    let url = cask.download_url().ok_or_else(|| {
44        Error::InstallFailed(format!("No download URL for cask {}", token))
45    })?;
46
47    // Detect artifact type
48    let artifact_type = detect_artifact_type(url);
49
50    // Get expected checksum
51    let sha256 = if options.no_verify {
52        None
53    } else {
54        cask.sha256.as_str()
55    };
56
57    info!("Downloading {}...", token);
58
59    // Warn if verification is disabled
60    if options.no_verify {
61        warn!("Checksum verification is disabled - this is a security risk");
62    }
63
64    if options.dry_run {
65        info!("[dry-run] Would download {} from {}", token, url);
66        info!("[dry-run] Would install to /Applications");
67        return Ok(PathBuf::from("/Applications"));
68    }
69
70    // Download artifact
71    let artifact_path = download_cask_artifact(url, cache_dir, token, sha256, artifact_type).await?;
72
73    // Install based on platform and artifact type
74    let install_result = install_artifact(cask, &artifact_path, artifact_type, options).await?;
75
76    // Record installation
77    let installed = InstalledCask {
78        version: cask.version.clone(),
79        installed_at: now_timestamp(),
80        artifact_path: install_result.clone(),
81        auto_updates: cask.auto_updates,
82        artifacts: vec![], // Will be populated by install functions
83    };
84
85    installed_casks.add(token, installed);
86    installed_casks.save(state_path)?;
87
88    info!("Installed {} to {}", token, install_result.display());
89    Ok(install_result)
90}
91
92/// Install an artifact based on type
93async fn install_artifact(
94    cask: &Cask,
95    artifact_path: &Path,
96    artifact_type: ArtifactType,
97    options: &CaskInstallOptions,
98) -> Result<PathBuf> {
99    #[cfg(target_os = "macos")]
100    {
101        crate::macos::install_artifact(cask, artifact_path, artifact_type, options).await
102    }
103
104    #[cfg(target_os = "linux")]
105    {
106        crate::linux::install_artifact(cask, artifact_path, artifact_type, options).await
107    }
108
109    #[cfg(not(any(target_os = "macos", target_os = "linux")))]
110    {
111        Err(Error::UnsupportedPlatform(
112            std::env::consts::OS.to_string(),
113        ))
114    }
115}
116
117/// Uninstall a cask
118pub async fn uninstall_cask(
119    token: &str,
120    state_path: &Path,
121    zap: bool,
122) -> Result<()> {
123    let mut installed_casks = InstalledCasks::load(state_path)?;
124
125    let installed = installed_casks.get(token).ok_or_else(|| {
126        Error::UninstallFailed(format!("{} is not installed", token))
127    })?;
128
129    let artifact_path = installed.artifact_path.clone();
130
131    // Remove the installed artifact
132    if artifact_path.exists() {
133        if artifact_path.is_dir() {
134            info!("Removing {}", artifact_path.display());
135            std::fs::remove_dir_all(&artifact_path)
136                .map_err(|e| Error::UninstallFailed(format!("Failed to remove {}: {}", artifact_path.display(), e)))?;
137        } else if artifact_path.is_file() {
138            info!("Removing {}", artifact_path.display());
139            std::fs::remove_file(&artifact_path)
140                .map_err(|e| Error::UninstallFailed(format!("Failed to remove {}: {}", artifact_path.display(), e)))?;
141        }
142    } else {
143        warn!("Artifact path {} does not exist", artifact_path.display());
144    }
145
146    // Remove from state
147    installed_casks.remove(token);
148    installed_casks.save(state_path)?;
149
150    if zap {
151        info!("Zap requested - note: full zap (preferences, caches, support files) not yet implemented");
152        // TODO: Implement zap - would require tracking additional file locations
153        // Typical locations: ~/Library/Preferences/, ~/Library/Caches/, ~/Application Support/
154    }
155
156    info!("Uninstalled {}", token);
157    Ok(())
158}