Skip to main content

metrowrap/
workspace.rs

1// SPDX-FileCopyrightText: © 2026 TTKB, LLC
2// SPDX-License-Identifier: BSD-3-CLAUSE
3use std::path::{Path, PathBuf};
4
5use tempfile::{Builder, TempDir};
6
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub enum TempMode {
9    /// Use /dev/shm if available, else system temp. Always clean up.
10    Normal,
11    /// Use system temp. On failure, keep files and print their location.
12    KeepOnFailure,
13    /// Use /dev/shm if available, else system temp. Always clean up at
14    /// termination regardless of outcome - files are never left orphaned.
15    ShmDebug,
16}
17
18impl TempMode {
19    fn base_dir(&self) -> PathBuf {
20        let shm = Path::new("/dev/shm");
21        match self {
22            Self::Normal | Self::ShmDebug => {
23                if shm.is_dir() {
24                    shm.to_path_buf()
25                } else {
26                    std::env::temp_dir()
27                }
28            }
29            Self::KeepOnFailure => std::env::temp_dir(),
30        }
31    }
32}
33
34pub struct Workspace {
35    dir: TempDir,
36    mode: TempMode,
37}
38
39impl Workspace {
40    pub fn new(mode: TempMode) -> Result<Self, Box<dyn std::error::Error>> {
41        let dir = Builder::new().prefix("mw-").tempdir_in(mode.base_dir())?;
42        Ok(Self { dir, mode })
43    }
44
45    pub fn path(&self) -> &Path {
46        self.dir.path()
47    }
48
49    /// Call when the operation fails. `KeepOnFailure` leaks the directory so
50    /// it survives process exit; all other modes drop and clean up immediately.
51    /// `ShmDebug` guarantees no orphaned files in /dev/shm.
52    pub fn on_failure(self) {
53        if self.mode == TempMode::KeepOnFailure {
54            let path = self.dir.keep();
55            eprintln!("note: temp files retained at {}", path.display());
56        }
57        // Normal / ShmDebug: dir drops here, deleting the workspace.
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn test_workspace_normal_creates_dir() {
67        let ws = Workspace::new(TempMode::Normal).unwrap();
68        assert!(ws.path().is_dir());
69    }
70
71    #[test]
72    fn test_workspace_cleans_up_on_drop() {
73        let path = {
74            let ws = Workspace::new(TempMode::Normal).unwrap();
75            ws.path().to_path_buf()
76        };
77        assert!(!path.exists(), "workspace dir should be deleted on drop");
78    }
79
80    #[test]
81    fn test_workspace_keep_on_failure_leaks() {
82        let path = {
83            let ws = Workspace::new(TempMode::KeepOnFailure).unwrap();
84            let p = ws.path().to_path_buf();
85            ws.on_failure();
86            p
87        };
88        assert!(path.exists(), "KeepOnFailure should retain the dir");
89        std::fs::remove_dir_all(&path).unwrap();
90    }
91
92    #[test]
93    fn test_workspace_shm_debug_cleans_up_on_failure() {
94        let path = {
95            let ws = Workspace::new(TempMode::ShmDebug).unwrap();
96            let p = ws.path().to_path_buf();
97            ws.on_failure();
98            p
99        };
100        assert!(!path.exists(), "ShmDebug should always clean up");
101    }
102}