use std::{ io::{self, Write}, path::{Path, PathBuf}, sync::Arc, }; use fs_err as fs; use tempfile::{TempDir, tempdir}; /// The main state storage abstraction. /// /// This is appropriate for storing persistent data that is not user-facing, such as managed Python /// installations or tool environments. #[derive(Debug, Clone)] pub struct StateStore { /// The state storage. root: PathBuf, /// A temporary state storage. /// /// Included to ensure that the temporary store exists for the length of the operation, but /// is dropped at the end as appropriate. _temp_dir_drop: Option>, } impl StateStore { /// A persistent state store at `root`. pub fn from_path(root: impl Into) -> Result { Ok(Self { root: root.into(), _temp_dir_drop: None, }) } /// Create a temporary state store. pub fn temp() -> Result { let temp_dir = tempdir()?; Ok(Self { root: temp_dir.path().to_path_buf(), _temp_dir_drop: Some(Arc::new(temp_dir)), }) } /// Return the root of the state store. pub fn root(&self) -> &Path { &self.root } /// Initialize the state store. pub fn init(self) -> Result { let root = &self.root; // Create the state store directory, if it doesn't exist. fs::create_dir_all(root)?; // Add a .gitignore. match fs::OpenOptions::new() .write(true) .create_new(true) .open(root.join(".gitignore")) { Ok(mut file) => file.write_all(b"*")?, Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (), Err(err) => return Err(err), } Ok(Self { root: fs::canonicalize(root)?, ..self }) } /// The folder for a specific cache bucket pub fn bucket(&self, state_bucket: StateBucket) -> PathBuf { self.root.join(state_bucket.to_str()) } /// Prefer, in order: /// /// 1. The specific state directory specified by the user. /// 2. The system-appropriate user-level data directory. /// 3. A `.uv` directory in the current working directory. /// /// Returns an absolute cache dir. pub fn from_settings(state_dir: Option) -> Result { if let Some(state_dir) = state_dir { StateStore::from_path(state_dir) } else if let Some(data_dir) = uv_dirs::legacy_user_state_dir().filter(|dir| dir.exists()) { // If the user has an existing directory at (e.g.) `/Users/user/Library/Application Support/uv`, // respect it for backwards compatibility. Otherwise, prefer the XDG strategy, even on // macOS. StateStore::from_path(data_dir) } else if let Some(data_dir) = uv_dirs::user_state_dir() { StateStore::from_path(data_dir) } else { StateStore::from_path(".uv") } } } /// The different kinds of data in the state store are stored in different bucket, which in our case /// are subdirectories of the state store root. #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] pub enum StateBucket { /// Managed Python installations ManagedPython, /// Installed tools. Tools, } impl StateBucket { fn to_str(self) -> &'static str { match self { Self::ManagedPython => "python", Self::Tools => "tools", } } }