Skip to main content

oxide_cli/templates/
install.rs

1use 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}