oxide_cli/templates/
install.rs1use std::path::Path;
2
3use anyhow::Result;
4use reqwest::Client;
5use serde::Deserialize;
6
7use crate::{
8 AppContext,
9 auth::token::get_auth_user,
10 cache::{CachedTemplate, get_cached_template, update_templates_cache},
11 utils::archive::download_and_extract,
12};
13
14#[derive(Deserialize)]
15struct TemplateInfoRes {
16 archive_url: String,
17 commit_sha: String,
18 subdir: Option<String>,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum InstallResult {
23 Installed,
24 Updated { version: String },
25 UpToDate,
26}
27
28impl InstallResult {
29 pub fn message(&self, template_name: &str) -> Option<String> {
30 match self {
31 Self::Installed => Some(format!(
32 "Template '{template_name}' downloaded successfully"
33 )),
34 Self::Updated { version } => {
35 Some(format!("Template '{template_name}' updated to v{version}"))
36 }
37 Self::UpToDate => None,
38 }
39 }
40
41 pub fn up_to_date_message(template_name: &str) -> String {
42 format!("Template '{template_name}' is already up to date")
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47enum InstallState {
48 Install,
49 Update,
50 UpToDate,
51}
52
53fn classify_install_state(
54 cached_template: Option<&CachedTemplate>,
55 template_dir_exists: bool,
56 latest_commit_sha: &str,
57) -> InstallState {
58 let Some(cached_template) = cached_template else {
59 return InstallState::Install;
60 };
61
62 if !template_dir_exists {
63 return InstallState::Install;
64 }
65
66 if cached_template.commit_sha == latest_commit_sha {
67 InstallState::UpToDate
68 } else {
69 InstallState::Update
70 }
71}
72
73#[doc(hidden)]
74pub fn classify_install_state_for_tests(
75 cached_template: Option<&CachedTemplate>,
76 template_dir_exists: bool,
77 latest_commit_sha: &str,
78) -> &'static str {
79 match classify_install_state(cached_template, template_dir_exists, latest_commit_sha) {
80 InstallState::Install => "install",
81 InstallState::Update => "update",
82 InstallState::UpToDate => "up_to_date",
83 }
84}
85
86async fn get_template_info(
87 template_name: &str,
88 client: &Client,
89 auth_path: &Path,
90 backend_url: &str,
91) -> Result<TemplateInfoRes> {
92 let user = get_auth_user(auth_path)?;
93
94 let res: TemplateInfoRes = client
95 .get(format!("{backend_url}/template/{template_name}/url"))
96 .bearer_auth(user.token)
97 .header("Content-Type", "application/json")
98 .send()
99 .await?
100 .error_for_status()?
101 .json()
102 .await?;
103
104 Ok(res)
105}
106
107pub async fn install_template(ctx: &AppContext, template_name: &str) -> Result<InstallResult> {
108 let info = get_template_info(
109 template_name,
110 &ctx.client,
111 &ctx.paths.auth,
112 &ctx.backend_url,
113 )
114 .await?;
115
116 let dest = ctx.paths.templates.join(template_name);
117 let cached_template = get_cached_template(ctx, template_name)?;
118 let install_state =
119 classify_install_state(cached_template.as_ref(), dest.exists(), &info.commit_sha);
120
121 if install_state == InstallState::UpToDate {
122 return Ok(InstallResult::UpToDate);
123 }
124
125 {
126 let mut guard = ctx.cleanup_state.lock().unwrap_or_else(|e| e.into_inner());
127 *guard = Some(dest.clone());
128 }
129
130 let download_result = download_and_extract(
131 &ctx.client,
132 &info.archive_url,
133 &dest,
134 info.subdir.as_deref(),
135 )
136 .await;
137
138 {
139 let mut guard = ctx.cleanup_state.lock().unwrap_or_else(|e| e.into_inner());
140 *guard = None;
141 }
142
143 download_result?;
144
145 let cached_template = update_templates_cache(
146 &ctx.paths.templates,
147 Path::new(template_name),
148 &info.commit_sha,
149 )?;
150
151 Ok(match install_state {
152 InstallState::Install => InstallResult::Installed,
153 InstallState::Update => InstallResult::Updated {
154 version: cached_template.version,
155 },
156 InstallState::UpToDate => unreachable!("up-to-date templates should return early"),
157 })
158}