Skip to main content

orcs_runtime/
work_dir.rs

1//! Lifecycle-managed work directory.
2//!
3//! [`WorkDir`] owns a directory and guarantees its existence for the
4//! duration of the value's lifetime.
5//!
6//! # Motivation
7//!
8//! Temporary directories require explicit lifecycle management:
9//! **who creates, who owns, and who cleans up**.
10//! Without a dedicated owner, temp directories leak on disk
11//! (no RAII guard) or use predictable names (security risk).
12//!
13//! `WorkDir` centralises this concern so that callers never
14//! interact with `std::env::temp_dir()` or `tempfile::TempDir` directly.
15//!
16//! # Variants
17//!
18//! | Variant | Created by | Drop behaviour |
19//! |---------|-----------|----------------|
20//! | [`Persistent`](WorkDir::Persistent) | User-specified path | Directory is **kept** |
21//! | [`Temporary`](WorkDir::Temporary) | Auto-generated | Directory is **deleted** |
22
23use std::path::{Path, PathBuf};
24
25/// A directory whose existence is guaranteed while this value is alive.
26///
27/// Use [`WorkDir::persistent`] for user-specified paths (no cleanup)
28/// and [`WorkDir::temporary`] for auto-generated directories (cleanup on drop).
29#[derive(Debug)]
30pub enum WorkDir {
31    /// User-specified directory. Not deleted on drop.
32    Persistent(PathBuf),
33    /// Auto-generated temporary directory. Deleted on drop via [`tempfile::TempDir`].
34    Temporary(tempfile::TempDir),
35}
36
37impl WorkDir {
38    /// Create a `WorkDir` backed by a user-specified path.
39    ///
40    /// Creates the directory (and parents) if it does not exist.
41    ///
42    /// # Errors
43    ///
44    /// Returns [`std::io::Error`] if directory creation fails.
45    pub fn persistent(path: PathBuf) -> std::io::Result<Self> {
46        std::fs::create_dir_all(&path)?;
47        Ok(Self::Persistent(path))
48    }
49
50    /// Create a `WorkDir` backed by a temporary directory.
51    ///
52    /// The directory is created with a cryptographically random name
53    /// (via [`tempfile::tempdir`]) and is automatically deleted when
54    /// the `WorkDir` is dropped.
55    ///
56    /// # Errors
57    ///
58    /// Returns [`std::io::Error`] if temporary directory creation fails.
59    pub fn temporary() -> std::io::Result<Self> {
60        let td = tempfile::tempdir()?;
61        Ok(Self::Temporary(td))
62    }
63
64    /// Returns the path to the work directory.
65    ///
66    /// The path is valid for as long as this `WorkDir` value is alive.
67    #[must_use]
68    pub fn path(&self) -> &Path {
69        match self {
70            Self::Persistent(p) => p,
71            Self::Temporary(td) => td.path(),
72        }
73    }
74
75    /// Returns `true` if this is a temporary (auto-cleanup) directory.
76    #[must_use]
77    pub fn is_temporary(&self) -> bool {
78        matches!(self, Self::Temporary(_))
79    }
80}
81
82impl AsRef<Path> for WorkDir {
83    fn as_ref(&self) -> &Path {
84        self.path()
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn temporary_creates_and_cleans_up() {
94        let path;
95        {
96            let wd = WorkDir::temporary().expect("should create temporary WorkDir");
97            path = wd.path().to_path_buf();
98            assert!(
99                path.exists(),
100                "directory should exist while WorkDir is alive"
101            );
102            assert!(wd.is_temporary());
103        }
104        // After drop, directory should be gone
105        assert!(
106            !path.exists(),
107            "directory should be removed after WorkDir is dropped"
108        );
109    }
110
111    #[test]
112    fn persistent_creates_directory() {
113        let td = tempfile::tempdir().expect("should create outer temp dir");
114        let target = td.path().join("my-persistent-dir");
115
116        let wd = WorkDir::persistent(target.clone()).expect("should create persistent WorkDir");
117
118        assert!(target.exists(), "persistent directory should be created");
119        assert!(!wd.is_temporary());
120        assert_eq!(wd.path(), target);
121
122        drop(wd);
123        // Persistent directories are NOT deleted on drop
124        assert!(
125            target.exists(),
126            "persistent directory should survive WorkDir drop"
127        );
128    }
129
130    #[test]
131    fn temporary_path_is_accessible() {
132        let wd = WorkDir::temporary().expect("should create temporary WorkDir");
133        let path = wd.path();
134
135        // Should be able to create files inside the work dir
136        let file = path.join("test.txt");
137        std::fs::write(&file, "hello").expect("should write file inside WorkDir");
138        assert!(file.exists());
139    }
140
141    #[test]
142    fn as_ref_path_matches_path_method() {
143        let wd = WorkDir::temporary().expect("should create temporary WorkDir");
144        let via_path: &Path = wd.path();
145        let via_as_ref: &Path = wd.as_ref();
146        assert_eq!(
147            via_path, via_as_ref,
148            "AsRef<Path> should delegate to path()"
149        );
150    }
151
152    #[test]
153    fn persistent_nested_creation() {
154        let td = tempfile::tempdir().expect("should create outer temp dir");
155        let deep = td.path().join("a").join("b").join("c");
156
157        let wd =
158            WorkDir::persistent(deep.clone()).expect("should create nested persistent WorkDir");
159
160        assert!(deep.exists(), "nested directories should be created");
161        assert_eq!(wd.path(), deep);
162    }
163}