Skip to main content

oxide_cli/addons/
install.rs

1use std::path::Path;
2
3use anyhow::{Context, Result};
4use serde::Deserialize;
5
6use crate::{AppContext, auth::token::get_auth_user, utils::archive::download_and_extract};
7
8use super::{
9  cache::{get_cached_addon, update_addons_cache},
10  manifest::AddonManifest,
11};
12
13#[derive(Deserialize)]
14struct AddonUrlResponse {
15  archive_url: String,
16  commit_sha: String,
17}
18
19async fn get_addon_url(ctx: &AppContext, addon_id: &str) -> Result<AddonUrlResponse> {
20  let user = get_auth_user(&ctx.paths.auth)?;
21
22  let res: AddonUrlResponse = ctx
23    .client
24    .get(format!("{}/addon/{addon_id}/url", ctx.backend_url))
25    .bearer_auth(user.token)
26    .header("Content-Type", "application/json")
27    .send()
28    .await
29    .with_context(|| format!("Failed to fetch download URL for addon '{addon_id}'"))?
30    .error_for_status()
31    .with_context(|| format!("Server returned error for addon '{addon_id}'"))?
32    .json()
33    .await
34    .with_context(|| format!("Failed to parse URL response for addon '{addon_id}'"))?;
35
36  Ok(res)
37}
38
39pub async fn install_addon(ctx: &AppContext, addon_id: &str) -> Result<AddonManifest> {
40  let info = get_addon_url(ctx, addon_id).await?;
41
42  // The archive already contains a top-level `{addon_id}/` directory,
43  // so we extract into the addons root — files land at addons/{addon_id}/...
44  let addons_dir = &ctx.paths.addons;
45  let addon_dir = addons_dir.join(addon_id);
46
47  // Skip download if cached commit matches and addon dir exists
48  if let Some(cached) = get_cached_addon(addons_dir, addon_id)
49    .with_context(|| format!("Failed to read addons cache while checking '{addon_id}'"))?
50    && cached.commit_sha == info.commit_sha
51    && addon_dir.exists()
52  {
53    let manifest_path = addon_dir.join("oxide.addon.json");
54    let content = std::fs::read_to_string(&manifest_path)
55      .with_context(|| format!("Failed to read cached manifest at {}", manifest_path.display()))?;
56    let manifest: AddonManifest = serde_json::from_str(&content)
57      .with_context(|| format!("Failed to parse cached manifest at {}", manifest_path.display()))?;
58    println!("Addon '{}' is already up to date", addon_id);
59    return Ok(manifest);
60  }
61
62  {
63    let mut guard = ctx.cleanup_state.lock().unwrap_or_else(|e| e.into_inner());
64    *guard = Some(addon_dir.clone());
65  }
66
67  download_and_extract(&ctx.client, &info.archive_url, addons_dir, None)
68    .await
69    .with_context(|| {
70      format!(
71        "Failed to download and extract addon '{addon_id}' from {}",
72        info.archive_url
73      )
74    })?;
75
76  {
77    let mut guard = ctx.cleanup_state.lock().unwrap_or_else(|e| e.into_inner());
78    *guard = None;
79  }
80
81  let manifest_path = addon_dir.join("oxide.addon.json");
82  let content = std::fs::read_to_string(&manifest_path).with_context(|| {
83    format!(
84      "Addon '{addon_id}' was extracted but 'oxide.addon.json' was not found at {}. \
85       Make sure the addon archive contains a top-level '{addon_id}/' directory with 'oxide.addon.json' inside.",
86      manifest_path.display()
87    )
88  })?;
89
90  let manifest: AddonManifest = serde_json::from_str(&content)
91    .with_context(|| format!("Failed to parse oxide.addon.json for addon '{addon_id}'"))?;
92
93  update_addons_cache(addons_dir, addon_id, &manifest, &info.commit_sha)
94    .with_context(|| format!("Failed to update addons cache after installing '{addon_id}'"))?;
95
96  println!("Addon '{}' successfully downloaded", addon_id);
97
98  Ok(manifest)
99}
100
101pub fn read_cached_manifest(addons_dir: &Path, addon_id: &str) -> Result<AddonManifest> {
102  let manifest_path = addons_dir.join(addon_id).join("oxide.addon.json");
103  let content = std::fs::read_to_string(&manifest_path)
104    .with_context(|| format!("Failed to read manifest for addon '{addon_id}' at {}", manifest_path.display()))?;
105  serde_json::from_str(&content)
106    .with_context(|| format!("Failed to parse manifest for addon '{addon_id}'"))
107}