mountpoint_s3_fs/data_cache/
cache_directory.rs1use sha2::{Digest, Sha256};
10use std::ffi::OsStr;
11use std::fs;
12use std::io;
13use std::os::unix::ffi::OsStrExt as _;
14use std::os::unix::fs::DirBuilderExt;
15use std::path::{Path, PathBuf};
16
17use thiserror::Error;
18
19#[derive(Debug)]
24pub struct ManagedCacheDir {
25 mountpoint_cache_path: PathBuf,
27 managed_cache_path: PathBuf,
29 should_cleanup: bool,
31}
32
33#[derive(Debug, Error)]
34pub enum ManagedCacheDirError {
35 #[error("creation of cache sub-directory failed due to IO error: {0}")]
36 CreationFailure(#[source] io::Error),
37 #[error("cleanup of cache sub-directory failed due to IO error: {0}")]
38 CleanupFailure(#[source] io::Error),
39}
40
41impl ManagedCacheDir {
42 pub fn new_from_parent_with_cache_key(
52 parent_path: impl AsRef<Path>,
53 cache_key: Option<&OsStr>,
54 should_cleanup: bool,
55 ) -> Result<Self, ManagedCacheDirError> {
56 let mountpoint_cache_path = parent_path.as_ref().join("mountpoint-cache");
57 let managed_cache_path = match cache_key {
58 None => mountpoint_cache_path.clone(),
59 Some(cache_key) => mountpoint_cache_path.join(hash_cache_key(cache_key.as_bytes())),
60 };
61 let managed_cache_dir = Self {
62 mountpoint_cache_path,
63 managed_cache_path,
64 should_cleanup,
65 };
66
67 if should_cleanup {
68 managed_cache_dir.remove()?;
69 }
70 Self::create_dir(&managed_cache_dir.mountpoint_cache_path)?;
71 if cache_key.is_some() {
72 Self::create_dir(&managed_cache_dir.managed_cache_path)?;
73 }
74 Ok(managed_cache_dir)
75 }
76
77 fn remove(&self) -> Result<(), ManagedCacheDirError> {
79 tracing::debug!(cache_subdirectory = ?self.mountpoint_cache_path, "removing the cache sub-directory and any contents");
80 if let Err(remove_dir_err) = fs::remove_dir_all(&self.mountpoint_cache_path) {
81 match remove_dir_err.kind() {
82 io::ErrorKind::NotFound => (),
83 _kind => return Err(ManagedCacheDirError::CleanupFailure(remove_dir_err)),
84 }
85 }
86 tracing::trace!(cache_subdirectory = ?self.mountpoint_cache_path, "cache sub-directory removal complete");
87 Ok(())
88 }
89
90 fn create_dir(path: &Path) -> Result<(), ManagedCacheDirError> {
92 let mkdir_result = fs::DirBuilder::new().mode(0o700).create(path);
93 if let Err(mkdir_err) = mkdir_result {
94 match mkdir_err.kind() {
95 io::ErrorKind::AlreadyExists => tracing::debug!(
96 cache_dir = ?path,
97 "cache sub-directory already existed",
98 ),
99 _kind => return Err(ManagedCacheDirError::CreationFailure(mkdir_err)),
100 }
101 }
102
103 Ok(())
104 }
105
106 pub fn as_path(&self) -> &Path {
108 self.managed_cache_path.as_path()
109 }
110
111 pub fn as_path_buf(&self) -> PathBuf {
113 self.managed_cache_path.clone()
114 }
115}
116
117impl Drop for ManagedCacheDir {
118 fn drop(&mut self) {
119 if self.should_cleanup
120 && let Err(err) = self.remove()
121 {
122 tracing::error!(cache_subdirectory = ?self.mountpoint_cache_path, "failed to remove cache sub-directory: {err}");
123 }
124 }
125}
126
127fn hash_cache_key(cache_key: &[u8]) -> String {
129 let hashed_key = Sha256::digest(cache_key);
130 hex::encode(hashed_key)
131}
132
133#[cfg(test)]
134mod tests {
135 use test_case::test_matrix;
136
137 use super::{ManagedCacheDir, hash_cache_key};
138
139 use std::ffi::OsStr;
140 use std::fs;
141 use std::os::unix::ffi::OsStrExt as _;
142 use std::os::unix::fs::{DirBuilderExt, PermissionsExt};
143
144 const EXPECTED_DIR_MODE: u32 = 0o700;
145
146 const SHOULD_CLEANUP: bool = true;
147 const SHOULD_NOT_CLEANUP: bool = !SHOULD_CLEANUP;
148
149 macro_rules! assert_dir_does_not_exist {
150 ($path:expr, $($arg:tt)+) => {
151 let err = fs::metadata($path).expect_err("path should not exist");
152 assert!(
153 matches!(err.kind(), std::io::ErrorKind::NotFound),
154 $($arg)+
155 );
156 };
157 }
158
159 macro_rules! assert_cache_dir_existence_after_cleanup {
160 ($expected:expr, $path:expr) => {
161 let exists = $path.try_exists().unwrap();
162 if $expected {
163 assert_eq!($expected, exists, "expected cache sub-directory to exist");
164 } else {
165 assert_eq!($expected, exists, "expected cache sub-directory to not exist");
166 }
167 };
168 }
169
170 macro_rules! assert_dir_exists_with_permissions {
171 ($expected_path:expr) => {
172 let dir_mode = fs::metadata($expected_path)
173 .expect("path should exist")
174 .permissions()
175 .mode();
176 let dir_mode = dir_mode & 0o777;
177 assert_eq!(
178 dir_mode, EXPECTED_DIR_MODE,
179 "path should have {EXPECTED_DIR_MODE:#o} permission mode but had {dir_mode:#o}",
180 );
181 };
182 }
183
184 #[test_matrix([SHOULD_CLEANUP, SHOULD_NOT_CLEANUP])]
185 fn test_unused(should_cleanup: bool) {
186 let temp_dir = tempfile::tempdir().unwrap();
187 let expected_path = temp_dir.path().join("mountpoint-cache");
188
189 let managed_dir = ManagedCacheDir::new_from_parent_with_cache_key(temp_dir.path(), None, should_cleanup)
190 .expect("creating managed dir should succeed");
191 assert_dir_exists_with_permissions!(&expected_path);
192
193 drop(managed_dir);
194 assert_cache_dir_existence_after_cleanup!(!should_cleanup, &expected_path);
195
196 temp_dir.close().unwrap();
197 }
198
199 #[test_matrix([SHOULD_CLEANUP, SHOULD_NOT_CLEANUP])]
200 fn test_cache_key_unused(should_cleanup: bool) {
201 let temp_dir = tempfile::tempdir().unwrap();
202 let cache_key = OsStr::new("cache_key");
203 let mp_cache_path = temp_dir.path().join("mountpoint-cache");
204 let expected_path = mp_cache_path.join(hash_cache_key(cache_key.as_bytes()));
205
206 let managed_dir =
207 ManagedCacheDir::new_from_parent_with_cache_key(temp_dir.path(), Some(cache_key), should_cleanup)
208 .expect("creating managed dir should succeed");
209 assert_dir_does_not_exist!(
210 &mp_cache_path.join("cache_key"),
211 "raw cache key should not be used in cache dir name",
212 );
213 assert_dir_exists_with_permissions!(&expected_path);
214
215 drop(managed_dir);
216 assert_cache_dir_existence_after_cleanup!(!should_cleanup, &mp_cache_path);
217
218 temp_dir.close().unwrap();
219 }
220
221 #[test_matrix([SHOULD_CLEANUP, SHOULD_NOT_CLEANUP])]
222 fn test_used(should_cleanup: bool) {
223 let temp_dir = tempfile::tempdir().unwrap();
224 let expected_path = temp_dir.path().join("mountpoint-cache");
225
226 let managed_dir = ManagedCacheDir::new_from_parent_with_cache_key(temp_dir.path(), None, should_cleanup)
227 .expect("creating managed dir should succeed");
228 assert_dir_exists_with_permissions!(&expected_path);
229
230 fs::File::create(expected_path.join("file.txt"))
231 .expect("should be able to create file within managed directory");
232 fs::create_dir(expected_path.join("dir")).expect("should be able to create dir within managed directory");
233 fs::File::create(expected_path.join("dir/file.txt"))
234 .expect("should be able to create file within subdirectory");
235
236 drop(managed_dir);
237 assert_cache_dir_existence_after_cleanup!(!should_cleanup, &expected_path);
238
239 temp_dir.close().unwrap();
240 }
241
242 #[test_matrix([SHOULD_CLEANUP, SHOULD_NOT_CLEANUP])]
243 fn test_cache_key_used(should_cleanup: bool) {
244 let temp_dir = tempfile::tempdir().unwrap();
245 let cache_key = OsStr::new("cache_key");
246 let mp_cache_path = temp_dir.path().join("mountpoint-cache");
247 let expected_path = mp_cache_path.join(hash_cache_key(cache_key.as_bytes()));
248
249 let managed_dir =
250 ManagedCacheDir::new_from_parent_with_cache_key(temp_dir.path(), Some(cache_key), should_cleanup)
251 .expect("creating managed dir should succeed");
252 assert_dir_does_not_exist!(
253 &mp_cache_path.join("cache_key"),
254 "raw cache key should not be used in cache dir name",
255 );
256 assert_dir_exists_with_permissions!(&expected_path);
257
258 fs::File::create(expected_path.join("file.txt"))
259 .expect("should be able to create file within managed directory");
260 fs::create_dir(expected_path.join("dir")).expect("should be able to create dir within managed directory");
261 fs::File::create(expected_path.join("dir/file.txt"))
262 .expect("should be able to create file within subdirectory");
263
264 drop(managed_dir);
265 assert_cache_dir_existence_after_cleanup!(!should_cleanup, &expected_path);
266
267 temp_dir.close().unwrap();
268 }
269
270 #[test_matrix([SHOULD_CLEANUP, SHOULD_NOT_CLEANUP])]
271 fn test_already_exists(should_cleanup: bool) {
272 let temp_dir = tempfile::tempdir().unwrap();
273 let expected_path = temp_dir.path().join("mountpoint-cache");
274
275 fs::DirBuilder::new()
276 .recursive(true)
277 .mode(0o775) .create(expected_path.join("dir"))
279 .unwrap();
280 fs::File::create(expected_path.join("dir/file.txt")).unwrap();
281
282 let managed_dir = ManagedCacheDir::new_from_parent_with_cache_key(temp_dir.path(), None, should_cleanup)
283 .expect("creating managed dir should succeed");
284
285 fs::metadata(&expected_path).expect("path should exist");
286
287 let dir_entries = fs::read_dir(&expected_path).unwrap().count();
288 if should_cleanup {
289 assert_eq!(dir_entries, 0, "directory should be empty");
290 assert_dir_exists_with_permissions!(&expected_path);
292 } else {
293 assert_eq!(dir_entries, 1, "directory should have one entry");
294 }
295
296 drop(managed_dir);
297 assert_cache_dir_existence_after_cleanup!(!should_cleanup, &expected_path);
298
299 temp_dir.close().unwrap();
300 }
301}