oxide_cli/addons/
install.rs1use 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 let addons_dir = &ctx.paths.addons;
45 let addon_dir = addons_dir.join(addon_id);
46
47 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}