dodot_lib/fs/mod.rs
1mod os;
2
3pub use os::OsFs;
4
5use std::path::{Path, PathBuf};
6
7use crate::Result;
8
9/// Metadata about a filesystem entry.
10#[derive(Debug, Clone)]
11pub struct FsMetadata {
12 pub is_file: bool,
13 pub is_dir: bool,
14 pub is_symlink: bool,
15 pub len: u64,
16 /// Unix permission mode (e.g. `0o755`).
17 pub mode: u32,
18}
19
20/// A single directory entry returned by [`Fs::read_dir`].
21#[derive(Debug, Clone)]
22pub struct DirEntry {
23 pub path: PathBuf,
24 pub name: String,
25 pub is_dir: bool,
26 pub is_file: bool,
27 pub is_symlink: bool,
28}
29
30/// Filesystem abstraction.
31///
32/// All dodot code accesses the filesystem through this trait so that:
33/// - Tests can use isolated temp directories with a real implementation
34/// - Every `io::Error` is wrapped with the path that caused it
35///
36/// Use `&dyn Fs` (trait objects) throughout the codebase. The operations
37/// are I/O-bound so dynamic dispatch costs nothing meaningful, and generics
38/// would infect every type signature.
39pub trait Fs: Send + Sync {
40 /// Returns metadata for the path, following symlinks.
41 fn stat(&self, path: &Path) -> Result<FsMetadata>;
42
43 /// Returns metadata for the path without following symlinks.
44 fn lstat(&self, path: &Path) -> Result<FsMetadata>;
45
46 /// Opens the file for reading in a streaming fashion.
47 ///
48 /// Errors that occur while opening the file are returned through this
49 /// method's [`Result`] and include path context. Once opened, the
50 /// returned reader is a raw [`std::io::Read`], so any later `read()`
51 /// errors are reported as plain [`std::io::Error`] values and are not
52 /// automatically wrapped with the path.
53 fn open_read(&self, path: &Path) -> Result<Box<dyn std::io::Read + Send + Sync>>;
54
55 /// Reads the entire file into bytes.
56 fn read_file(&self, path: &Path) -> Result<Vec<u8>>;
57
58 /// Reads the entire file as a UTF-8 string.
59 fn read_to_string(&self, path: &Path) -> Result<String>;
60
61 /// Writes `contents` to `path`, creating or truncating the file.
62 fn write_file(&self, path: &Path, contents: &[u8]) -> Result<()>;
63
64 /// Writes `contents` to `path`, creating or truncating the file
65 /// **with `mode` applied at creation time** (not via a follow-up
66 /// `chmod`). Used by whole-file secret preprocessors so the
67 /// rendered plaintext never lives at the umask-default mode,
68 /// even briefly — closing the race window between
69 /// `write_file` (lands at e.g. 0644 on a typical 022 umask)
70 /// and `set_permissions` (tightens to 0600). See
71 /// `secrets.lex` §4.3 + the Phase S3 chmod-race review on
72 /// PR #130.
73 ///
74 /// Default impl falls back to `write_file` then
75 /// `set_permissions` so existing `Fs` implementations stay
76 /// correct (semantics-preserving) without forcing an upgrade;
77 /// the production `OsFs` overrides to use `OpenOptions::mode`
78 /// for the atomic version.
79 fn write_file_with_mode(&self, path: &Path, contents: &[u8], mode: u32) -> Result<()> {
80 self.write_file(path, contents)?;
81 self.set_permissions(path, mode)
82 }
83
84 /// Creates `path` and all parent directories.
85 fn mkdir_all(&self, path: &Path) -> Result<()>;
86
87 /// Creates a symbolic link at `link` pointing to `original`.
88 fn symlink(&self, original: &Path, link: &Path) -> Result<()>;
89
90 /// Reads the target of a symbolic link.
91 fn readlink(&self, path: &Path) -> Result<PathBuf>;
92
93 /// Removes a file or symlink (not a directory).
94 fn remove_file(&self, path: &Path) -> Result<()>;
95
96 /// Removes a directory and all of its contents.
97 fn remove_dir_all(&self, path: &Path) -> Result<()>;
98
99 /// Returns `true` if `path` exists (follows symlinks).
100 fn exists(&self, path: &Path) -> bool;
101
102 /// Returns `true` if `path` is a symlink (does not follow).
103 fn is_symlink(&self, path: &Path) -> bool;
104
105 /// Returns `true` if `path` is a directory (follows symlinks).
106 fn is_dir(&self, path: &Path) -> bool;
107
108 /// Lists entries in a directory, sorted by name.
109 fn read_dir(&self, path: &Path) -> Result<Vec<DirEntry>>;
110
111 /// Renames (moves) `from` to `to`.
112 fn rename(&self, from: &Path, to: &Path) -> Result<()>;
113
114 /// Copies a file from `from` to `to`.
115 fn copy_file(&self, from: &Path, to: &Path) -> Result<()>;
116
117 /// Sets file permissions (Unix mode).
118 fn set_permissions(&self, path: &Path, mode: u32) -> Result<()>;
119
120 /// Returns the modification time of `path` (follows symlinks).
121 /// Used by `dodot refresh` to compare deployed-side mtimes against
122 /// source-side mtimes when deciding whether to touch the source.
123 ///
124 /// **Default implementation panics.** Override in `Fs` impls that
125 /// need mtime support (currently `OsFs`). Provided as a default
126 /// so adding mtime operations doesn't break any existing in-tree
127 /// or downstream `Fs` impl.
128 fn modified(&self, _path: &Path) -> Result<std::time::SystemTime> {
129 unimplemented!("Fs::modified is only implemented by OsFs")
130 }
131
132 /// Sets the modification time of `path` to `time`. Used by
133 /// `dodot refresh` to copy the deployed file's mtime onto the
134 /// template source so git's stat-cache invalidates and the next
135 /// `git status` re-reads the file (invoking the clean filter on
136 /// repos that have it installed).
137 ///
138 /// **Default implementation panics.** Override in `Fs` impls that
139 /// need mtime support (currently `OsFs`).
140 fn set_modified(&self, _path: &Path, _time: std::time::SystemTime) -> Result<()> {
141 unimplemented!("Fs::set_modified is only implemented by OsFs")
142 }
143}