/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ use std::{ fs::create_dir_all, path::{Path, PathBuf}, }; use futures::Future; use tokio::fs::remove_dir_all; use crate::{ state::PersistedState, util::errors::{wrap, AnyError, WrappedError}, }; const KEEP_LRU: usize = 5; const STAGING_SUFFIX: &str = ".staging"; #[derive(Clone)] pub struct DownloadCache { path: PathBuf, state: PersistedState>, } impl DownloadCache { pub fn new(path: PathBuf) -> DownloadCache { DownloadCache { state: PersistedState::new(path.join("lru.json")), path, } } /// Gets the download cache path. Names of cache entries can be formed by /// joining them to the path. pub fn path(&self) -> &Path { &self.path } /// Gets whether a cache exists with the name already. Marks it as recently /// used if it does exist. pub fn exists(&self, name: &str) -> Option { let p = self.path.join(name); if !p.exists() { return None; } let _ = self.touch(name.to_string()); Some(p) } /// Removes the item from the cache, if it exists pub fn delete(&self, name: &str) -> Result<(), WrappedError> { let f = self.path.join(name); if f.exists() { std::fs::remove_dir_all(f).map_err(|e| wrap(e, "error removing cached folder"))?; } self.state.update(|l| { l.retain(|n| n != name); }) } /// Calls the function to create the cached folder if it doesn't exist, /// returning the path where the folder is. Note that the path passed to /// the `do_create` method is a staging path and will not be the same as the /// final returned path. pub async fn create( &self, name: impl AsRef, do_create: F, ) -> Result where F: FnOnce(PathBuf) -> T, T: Future> + Send, { let name = name.as_ref(); let target_dir = self.path.join(name); if target_dir.exists() { return Ok(target_dir); } let temp_dir = self.path.join(format!("{}{}", name, STAGING_SUFFIX)); let _ = remove_dir_all(&temp_dir).await; // cleanup any existing create_dir_all(&temp_dir).map_err(|e| wrap(e, "error creating server directory"))?; do_create(temp_dir.clone()).await?; let _ = self.touch(name.to_string()); std::fs::rename(&temp_dir, &target_dir) .map_err(|e| wrap(e, "error renaming downloaded server"))?; Ok(target_dir) } fn touch(&self, name: String) -> Result<(), AnyError> { self.state.update(|l| { if let Some(index) = l.iter().position(|s| s == &name) { l.remove(index); } l.insert(0, name); if l.len() <= KEEP_LRU { return; } if let Some(f) = l.last() { let f = self.path.join(f); if !f.exists() || std::fs::remove_dir_all(f).is_ok() { l.pop(); } } })?; Ok(()) } }