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}