Skip to main content

xianyun_deploy/registrycache/
refs.rs

1//! Per-profile ref counting for the host-side registry cache.
2//!
3//! Ported from `internal/registrycache/refs.go`.
4
5use std::fs::{self, OpenOptions};
6use std::io;
7
8use thiserror::Error;
9
10use super::paths::{self, PathError};
11
12/// Errors from ref-marker file operations.
13#[derive(Debug, Error)]
14pub enum RefsError {
15    /// Failed resolving a registry-cache path.
16    #[error(transparent)]
17    Path(#[from] PathError),
18    /// Failed creating the refs directory.
19    #[error("creating refs dir: {0}")]
20    CreateDir(io::Error),
21    /// Failed creating a ref marker file.
22    #[error("creating ref {0}: {1}")]
23    CreateRef(String, io::Error),
24    /// Failed removing a ref marker file.
25    #[error("removing ref {0}: {1}")]
26    RemoveRef(String, io::Error),
27    /// Failed reading the refs directory.
28    #[error("reading refs dir: {0}")]
29    ReadDir(io::Error),
30}
31
32/// Ensures a ref marker file exists for the given profile. Idempotent:
33/// re-adding an existing ref is a no-op.
34pub fn add_ref(profile: &str) -> Result<(), RefsError> {
35    let refs_dir = paths::refs_dir()?;
36    fs::create_dir_all(&refs_dir).map_err(RefsError::CreateDir)?;
37
38    let path = paths::ref_path(profile)?;
39    OpenOptions::new()
40        .create(true)
41        .truncate(false)
42        .write(true)
43        .open(&path)
44        .map_err(|e| RefsError::CreateRef(profile.to_string(), e))?;
45
46    Ok(())
47}
48
49/// Deletes the ref marker file for the given profile. Missing-file is
50/// not an error (idempotent).
51pub fn remove_ref(profile: &str) -> Result<(), RefsError> {
52    let path = paths::ref_path(profile)?;
53
54    match fs::remove_file(&path) {
55        Ok(()) => Ok(()),
56        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
57        Err(e) => Err(RefsError::RemoveRef(profile.to_string(), e)),
58    }
59}
60
61/// Returns the names (profile names) of all current refs, sorted.
62/// Returns an empty vec when the refs directory does not exist.
63pub fn list_refs() -> Result<Vec<String>, RefsError> {
64    let refs_dir = paths::refs_dir()?;
65
66    let entries = match fs::read_dir(&refs_dir) {
67        Ok(entries) => entries,
68        Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
69        Err(e) => return Err(RefsError::ReadDir(e)),
70    };
71
72    let mut out = Vec::new();
73    for entry in entries {
74        let entry = entry.map_err(RefsError::ReadDir)?;
75        let file_type = entry.file_type().map_err(RefsError::ReadDir)?;
76        if file_type.is_file() {
77            out.push(entry.file_name().to_string_lossy().into_owned());
78        }
79    }
80
81    out.sort();
82
83    Ok(out)
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use crate::registrycache::test_support::with_home;
90
91    // Ported from internal/registrycache/refs_test.go:12 (TestAddRefCreatesFile)
92    #[test]
93    fn test_add_ref_creates_file() {
94        with_home(|_home| {
95            add_ref("alpha").unwrap();
96
97            let path = paths::ref_path("alpha").unwrap();
98            assert!(path.exists());
99        });
100    }
101
102    // Ported from internal/registrycache/refs_test.go:23 (TestAddRefIdempotent)
103    #[test]
104    fn test_add_ref_idempotent() {
105        with_home(|_home| {
106            add_ref("alpha").unwrap();
107            add_ref("alpha").unwrap();
108
109            let refs = list_refs().unwrap();
110            assert_eq!(refs, vec!["alpha".to_string()]);
111        });
112    }
113
114    // Ported from internal/registrycache/refs_test.go:35 (TestRemoveRefRemovesFile)
115    #[test]
116    fn test_remove_ref_removes_file() {
117        with_home(|_home| {
118            add_ref("alpha").unwrap();
119            remove_ref("alpha").unwrap();
120
121            let path = paths::ref_path("alpha").unwrap();
122            assert!(!path.exists());
123        });
124    }
125
126    // Ported from internal/registrycache/refs_test.go:45 (TestRemoveRefMissingIsNoop)
127    #[test]
128    fn test_remove_ref_missing_is_noop() {
129        with_home(|_home| {
130            remove_ref("alpha").unwrap();
131        });
132    }
133
134    // Ported from internal/registrycache/refs_test.go:53 (TestListRefsEmpty)
135    #[test]
136    fn test_list_refs_empty() {
137        with_home(|_home| {
138            let refs = list_refs().unwrap();
139            assert!(refs.is_empty());
140        });
141    }
142
143    // Ported from internal/registrycache/refs_test.go:61 (TestListRefsMultipleSorted)
144    #[test]
145    fn test_list_refs_multiple_sorted() {
146        with_home(|_home| {
147            add_ref("charlie").unwrap();
148            add_ref("alpha").unwrap();
149            add_ref("bravo").unwrap();
150
151            let refs = list_refs().unwrap();
152            assert_eq!(
153                refs,
154                vec![
155                    "alpha".to_string(),
156                    "bravo".to_string(),
157                    "charlie".to_string()
158                ]
159            );
160        });
161    }
162
163    // Ported from internal/registrycache/refs_test.go:75 (TestRefsDirIsCreated)
164    #[test]
165    fn test_refs_dir_is_created() {
166        with_home(|_home| {
167            add_ref("alpha").unwrap();
168
169            let refs_dir = paths::refs_dir().unwrap();
170            assert!(refs_dir.is_dir());
171
172            let parent = refs_dir.parent().unwrap();
173            assert!(parent.is_dir());
174        });
175    }
176}