rustinsight/
cacher.rs

1use crate::{app_info, built_info, VERSION};
2use anyhow::Error;
3use chrono::{DateTime, Duration, Utc};
4use derive_more::{Deref, DerefMut};
5use semver::Version;
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8use tokio::fs;
9use tokio::fs::File;
10
11#[derive(Debug, Deserialize, Serialize)]
12pub struct GlobalConfig {
13    pub system: String,
14}
15
16impl Default for GlobalConfig {
17    fn default() -> Self {
18        Self {
19            system: built_info::CFG_OS.into(),
20        }
21    }
22}
23
24#[derive(Debug, Deserialize, Serialize)]
25pub struct LauncherConfig {
26    pub global: GlobalConfig,
27    pub launcher: AppState,
28    pub ri_learn: AppState,
29    pub ri_stack: AppState,
30}
31
32impl Default for LauncherConfig {
33    fn default() -> Self {
34        Self {
35            global: GlobalConfig::default(),
36            launcher: AppState {
37                version: Some(VERSION.clone()),
38                last_check: None,
39            },
40            ri_learn: AppState {
41                version: None,
42                last_check: None,
43            },
44            ri_stack: AppState {
45                version: None,
46                last_check: None,
47            },
48        }
49    }
50}
51
52#[derive(Debug, Deserialize, Serialize)]
53pub struct AppState {
54    pub version: Option<Version>,
55    pub last_check: Option<DateTime<Utc>>,
56}
57
58impl AppState {
59    pub fn is_update_required(&self) -> bool {
60        self.is_not_exist() || self.is_allowed_to_check()
61    }
62
63    pub fn is_not_exist(&self) -> bool {
64        self.version.is_none()
65    }
66
67    pub fn is_allowed_to_check(&self) -> bool {
68        let deadline = Utc::now() - Duration::days(1);
69        match &self.last_check {
70            None => true,
71            Some(last) if last <= &deadline => true,
72            Some(_recent) => false,
73        }
74    }
75
76    pub fn reset(&mut self) {
77        self.version.take();
78        self.last_check.take();
79    }
80
81    pub fn update_check(&mut self) {
82        self.last_check = Some(Utc::now());
83    }
84
85    pub fn is_outdated(&self, recent_version: Version) -> bool {
86        if let Some(current_version) = self.version.as_ref() {
87            current_version < &recent_version
88        } else {
89            true
90        }
91    }
92
93    pub fn get_version(&self) -> Result<Version, Error> {
94        self.version
95            .clone()
96            .ok_or_else(|| Error::msg("Version is not available"))
97    }
98}
99
100#[derive(Debug, Deref, DerefMut)]
101pub struct Cacher {
102    cache_dir: PathBuf,
103    bin_dir: PathBuf,
104    state_path: PathBuf,
105    #[deref]
106    #[deref_mut]
107    state: LauncherConfig,
108}
109
110impl Cacher {
111    pub async fn create() -> Result<Self, Error> {
112        // Create paths
113        let mut cache_dir =
114            dirs::cache_dir().ok_or_else(|| Error::msg("Cache directory is not available."))?;
115        cache_dir.push("rustinsight");
116
117        let mut bin_dir = cache_dir.clone();
118        bin_dir.push("bin");
119
120        let mut state_path = cache_dir.clone();
121        state_path.push("launcher.toml");
122
123        let state = LauncherConfig::default();
124        Ok(Self {
125            cache_dir,
126            bin_dir,
127            state_path,
128            state,
129        })
130    }
131
132    pub async fn initialize(&mut self) -> Result<(), Error> {
133        self.create_dirs().await?;
134        if let Err(_err) = self.try_read_state().await {
135            // Can't read a config file, it doesn't exist.
136        }
137        self.repair_config().await?; // In case if something removed
138        self.write_state().await?;
139        Ok(())
140    }
141
142    async fn create_dirs(&mut self) -> Result<(), Error> {
143        // Create dirs
144        fs::create_dir_all(&self.bin_dir).await?;
145        Ok(())
146    }
147
148    /// In case if binaries were deleted
149    async fn repair_config(&mut self) -> Result<(), Error> {
150        // Update launcher's version to the current
151        self.launcher.version = Some(VERSION.clone());
152        // Checking binaries
153        let mut entries = fs::read_dir(&self.bin_dir).await?;
154        let app_prefix = &app_info::LEARN.name;
155        while let Some(entry) = entries.next_entry().await? {
156            if entry.file_name().to_string_lossy().starts_with(app_prefix) {
157                return Ok(());
158            }
159        }
160        // File not found, than it has to be downloaded
161        self.state.ri_learn.reset();
162        Ok(())
163    }
164
165    pub fn bin_dir(&self) -> &PathBuf {
166        &self.bin_dir
167    }
168
169    /// Changes permissions and extension
170    pub async fn fix_binaries(&mut self) -> Result<(), Error> {
171        let mut entries = fs::read_dir(&self.bin_dir).await?;
172        while let Some(entry) = entries.next_entry().await? {
173            let bin_file = File::open(entry.path()).await?;
174            // Changes permissions on unix-like systems
175            #[cfg(not(target_os = "windows"))]
176            {
177                use std::os::unix::fs::PermissionsExt;
178                let mut perm = bin_file.metadata().await?.permissions();
179                let new_mode = perm.mode() | 0o511; // adds executable permission `(rx)xx`
180                perm.set_mode(new_mode);
181                bin_file.set_permissions(perm).await?;
182            }
183            #[cfg(target_os = "windows")]
184            {
185                let original_path = entry.path();
186                if original_path.extension().is_none() {
187                    let path_with_ext = original_path.with_extension("exe");
188                    fs::rename(original_path, path_with_ext).await?;
189                }
190            }
191        }
192        Ok(())
193    }
194
195    async fn try_read_state(&mut self) -> Result<(), Error> {
196        let contents = fs::read_to_string(&self.state_path).await?;
197        let state = toml::from_str(&contents)?;
198        self.state = state;
199        Ok(())
200    }
201
202    pub async fn write_state(&mut self) -> Result<(), Error> {
203        let contents = toml::to_string(&self.state)?;
204        fs::write(&self.state_path, contents).await?;
205        Ok(())
206    }
207
208    pub async fn remove_cache(self) -> Result<(), Error> {
209        fs::remove_dir_all(self.cache_dir).await?;
210        Ok(())
211    }
212}