uv_state/
lib.rs

1use std::{
2    io::{self, Write},
3    path::{Path, PathBuf},
4    sync::Arc,
5};
6
7use fs_err as fs;
8use tempfile::{TempDir, tempdir};
9
10/// The main state storage abstraction.
11///
12/// This is appropriate for storing persistent data that is not user-facing, such as managed Python
13/// installations or tool environments.
14#[derive(Debug, Clone)]
15pub struct StateStore {
16    /// The state storage.
17    root: PathBuf,
18    /// A temporary state storage.
19    ///
20    /// Included to ensure that the temporary store exists for the length of the operation, but
21    /// is dropped at the end as appropriate.
22    _temp_dir_drop: Option<Arc<TempDir>>,
23}
24
25impl StateStore {
26    /// A persistent state store at `root`.
27    pub fn from_path(root: impl Into<PathBuf>) -> Result<Self, io::Error> {
28        Ok(Self {
29            root: root.into(),
30            _temp_dir_drop: None,
31        })
32    }
33
34    /// Create a temporary state store.
35    pub fn temp() -> Result<Self, io::Error> {
36        let temp_dir = tempdir()?;
37        Ok(Self {
38            root: temp_dir.path().to_path_buf(),
39            _temp_dir_drop: Some(Arc::new(temp_dir)),
40        })
41    }
42
43    /// Return the root of the state store.
44    pub fn root(&self) -> &Path {
45        &self.root
46    }
47
48    /// Initialize the state store.
49    pub fn init(self) -> Result<Self, io::Error> {
50        let root = &self.root;
51
52        // Create the state store directory, if it doesn't exist.
53        fs::create_dir_all(root)?;
54
55        // Add a .gitignore.
56        match fs::OpenOptions::new()
57            .write(true)
58            .create_new(true)
59            .open(root.join(".gitignore"))
60        {
61            Ok(mut file) => file.write_all(b"*")?,
62            Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
63            Err(err) => return Err(err),
64        }
65
66        Ok(Self {
67            root: fs::canonicalize(root)?,
68            ..self
69        })
70    }
71
72    /// The folder for a specific cache bucket
73    pub fn bucket(&self, state_bucket: StateBucket) -> PathBuf {
74        self.root.join(state_bucket.to_str())
75    }
76
77    /// Prefer, in order:
78    ///
79    /// 1. The specific state directory specified by the user.
80    /// 2. The system-appropriate user-level data directory.
81    /// 3. A `.uv` directory in the current working directory.
82    ///
83    /// Returns an absolute cache dir.
84    pub fn from_settings(state_dir: Option<PathBuf>) -> Result<Self, io::Error> {
85        if let Some(state_dir) = state_dir {
86            Self::from_path(state_dir)
87        } else if let Some(data_dir) = uv_dirs::legacy_user_state_dir().filter(|dir| dir.exists()) {
88            // If the user has an existing directory at (e.g.) `/Users/user/Library/Application Support/uv`,
89            // respect it for backwards compatibility. Otherwise, prefer the XDG strategy, even on
90            // macOS.
91            Self::from_path(data_dir)
92        } else if let Some(data_dir) = uv_dirs::user_state_dir() {
93            Self::from_path(data_dir)
94        } else {
95            Self::from_path(".uv")
96        }
97    }
98}
99
100/// The different kinds of data in the state store are stored in different bucket, which in our case
101/// are subdirectories of the state store root.
102#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
103pub enum StateBucket {
104    /// Managed Python installations
105    ManagedPython,
106    /// Installed tools.
107    Tools,
108    /// Credentials.
109    Credentials,
110}
111
112impl StateBucket {
113    fn to_str(self) -> &'static str {
114        match self {
115            Self::ManagedPython => "python",
116            Self::Tools => "tools",
117            Self::Credentials => "credentials",
118        }
119    }
120}