1use crate::protocol::SessionId;
9use std::env;
10use std::fs;
11use std::path::{Path, PathBuf};
12use thiserror::Error;
13
14#[derive(Debug, Error)]
16pub enum SessionDirError {
17 #[error("Failed to create session directory: {0}")]
18 CreateFailed(#[from] std::io::Error),
19
20 #[error("Session directory not found: {0}")]
21 NotFound(PathBuf),
22
23 #[error("Failed to cleanup session directory: {0}")]
24 CleanupFailed(String),
25}
26
27pub type Result<T> = std::result::Result<T, SessionDirError>;
28
29#[derive(Debug, Clone)]
52pub struct SessionDir {
53 path: PathBuf,
55
56 is_tmpfs: bool,
58
59 session_id: SessionId,
61}
62
63impl SessionDir {
64 pub fn new(session_id: &SessionId) -> Result<Self> {
75 if let Ok(dir) = env::var("OXUR_REPL_TEMP_DIR") {
77 return Self::create_in(PathBuf::from(dir), session_id, false);
78 }
79
80 #[cfg(target_os = "linux")]
82 if let Ok(tmpfs_dir) = Self::try_tmpfs(session_id) {
83 return Ok(tmpfs_dir);
84 }
85
86 let temp_dir = env::temp_dir().join("oxur-repl").join(session_id.as_str());
88
89 Self::create_in(temp_dir, session_id, false)
90 }
91
92 #[cfg(target_os = "linux")]
94 fn try_tmpfs(session_id: &SessionId) -> Result<Self> {
95 let tmpfs_path = PathBuf::from("/dev/shm").join("oxur-repl").join(session_id.as_str());
96
97 Self::create_in(tmpfs_path, session_id, true)
98 }
99
100 fn create_in(path: PathBuf, session_id: &SessionId, is_tmpfs: bool) -> Result<Self> {
102 fs::create_dir_all(&path)?;
103
104 Ok(Self { path, is_tmpfs, session_id: session_id.clone() })
105 }
106
107 pub fn path(&self) -> &Path {
109 &self.path
110 }
111
112 pub fn is_tmpfs(&self) -> bool {
114 self.is_tmpfs
115 }
116
117 pub fn session_id(&self) -> &SessionId {
119 &self.session_id
120 }
121
122 pub fn write_source(&self, name: impl AsRef<str>, content: impl AsRef<str>) -> Result<PathBuf> {
129 let path = self.path.join(name.as_ref());
130 fs::write(&path, content.as_ref())?;
131 Ok(path)
132 }
133
134 pub fn create_subdir(&self, name: impl AsRef<str>) -> Result<PathBuf> {
136 let subdir = self.path.join(name.as_ref());
137 fs::create_dir_all(&subdir)?;
138 Ok(subdir)
139 }
140
141 pub fn cleanup(&self) -> Result<()> {
146 if self.path.exists() {
147 fs::remove_dir_all(&self.path).map_err(|e| {
148 SessionDirError::CleanupFailed(format!(
149 "Failed to remove {}: {}",
150 self.path.display(),
151 e
152 ))
153 })?;
154 }
155 Ok(())
156 }
157
158 pub fn list_files(&self) -> Result<Vec<PathBuf>> {
160 let mut files = Vec::new();
161
162 for entry in fs::read_dir(&self.path)? {
163 let entry = entry?;
164 let path = entry.path();
165 if path.is_file() {
166 files.push(path);
167 }
168 }
169
170 Ok(files)
171 }
172
173 pub fn get_stats(&self) -> Result<DirStats> {
178 let mut file_count = 0;
179 let mut total_bytes = 0;
180
181 fn walk_dir(
182 path: &Path,
183 file_count: &mut usize,
184 total_bytes: &mut u64,
185 ) -> std::io::Result<()> {
186 for entry in fs::read_dir(path)? {
187 let entry = entry?;
188 let path = entry.path();
189 let metadata = entry.metadata()?;
190
191 if metadata.is_file() {
192 *file_count += 1;
193 *total_bytes += metadata.len();
194 } else if metadata.is_dir() {
195 walk_dir(&path, file_count, total_bytes)?;
196 }
197 }
198 Ok(())
199 }
200
201 walk_dir(&self.path, &mut file_count, &mut total_bytes)?;
202
203 Ok(DirStats { file_count, total_bytes, is_tmpfs: self.is_tmpfs, path: self.path.clone() })
204 }
205}
206
207#[derive(Debug, Clone)]
209pub struct DirStats {
210 pub file_count: usize,
212
213 pub total_bytes: u64,
215
216 pub is_tmpfs: bool,
218
219 pub path: PathBuf,
221}
222
223impl Drop for SessionDir {
224 fn drop(&mut self) {
225 if let Err(e) = self.cleanup() {
227 eprintln!("Warning: Failed to cleanup session directory: {}", e);
228 }
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 #[serial_test::serial(env)]
238 fn test_session_dir_creation() {
239 let session_id = SessionId::new("test-create");
240 let dir = SessionDir::new(&session_id).expect("Failed to create session dir");
241
242 assert!(dir.path().exists());
243 assert_eq!(dir.session_id(), &session_id);
244 }
245
246 #[test]
247 #[serial_test::serial(env)]
248 fn test_session_dir_write_source() {
249 let session_id = SessionId::new("test-write");
250 let dir = SessionDir::new(&session_id).expect("Failed to create session dir");
251
252 let source_path =
253 dir.write_source("test.rs", "fn main() {}").expect("Failed to write source");
254
255 assert!(source_path.exists());
256 let content = fs::read_to_string(&source_path).expect("Failed to read source");
257 assert_eq!(content, "fn main() {}");
258 }
259
260 #[test]
261 #[serial_test::serial(env)]
262 fn test_session_dir_create_subdir() {
263 let session_id = SessionId::new("test-subdir");
264 let dir = SessionDir::new(&session_id).expect("Failed to create session dir");
265
266 let subdir = dir.create_subdir("artifacts").expect("Failed to create subdir");
267
268 assert!(subdir.exists());
269 assert!(subdir.is_dir());
270 }
271
272 #[test]
273 #[serial_test::serial(env)]
274 fn test_session_dir_list_files() {
275 let session_id = SessionId::new("test-list");
276 let dir = SessionDir::new(&session_id).expect("Failed to create session dir");
277
278 dir.write_source("file1.rs", "// file 1").expect("Failed to write file1");
279 dir.write_source("file2.rs", "// file 2").expect("Failed to write file2");
280
281 let files = dir.list_files().expect("Failed to list files");
282 assert_eq!(files.len(), 2);
283 }
284
285 #[test]
286 #[serial_test::serial(env)]
287 fn test_session_dir_cleanup() {
288 let session_id = SessionId::new("test-cleanup");
289 let dir = SessionDir::new(&session_id).expect("Failed to create session dir");
290 let path = dir.path().to_path_buf();
291
292 dir.write_source("test.rs", "fn main() {}").expect("Failed to write source");
293
294 assert!(path.exists());
295
296 dir.cleanup().expect("Failed to cleanup");
297 assert!(!path.exists());
298 }
299
300 #[test]
301 #[serial_test::serial(env)]
302 fn test_session_dir_auto_cleanup_on_drop() {
303 let session_id = SessionId::new("test-drop");
304 let path = {
305 let dir = SessionDir::new(&session_id).expect("Failed to create session dir");
306 dir.path().to_path_buf()
307 }; std::thread::sleep(std::time::Duration::from_millis(100));
311 assert!(!path.exists());
312 }
313
314 #[test]
315 #[serial_test::serial(env)]
316 fn test_session_dir_respects_env_var() {
317 let test_dir = env::temp_dir().join("oxur-test-custom");
318 env::set_var("OXUR_REPL_TEMP_DIR", &test_dir);
319
320 let session_id = SessionId::new("test-env");
321 let dir = SessionDir::new(&session_id).expect("Failed to create session dir");
322
323 assert!(dir.path().starts_with(&test_dir));
324
325 env::remove_var("OXUR_REPL_TEMP_DIR");
326 }
327
328 #[cfg(target_os = "linux")]
329 #[test]
330 fn test_session_dir_tmpfs_on_linux() {
331 let session_id = SessionId::new("test-tmpfs");
332 let dir = SessionDir::new(&session_id).expect("Failed to create session dir");
333
334 if PathBuf::from("/dev/shm").exists() {
336 assert!(dir.is_tmpfs() || !dir.is_tmpfs()); }
338 }
339}