coven_foundation/files/
observed_file.rs1use super::{FileError, FileReader};
4use std::{
5 fs::Metadata,
6 io,
7 path::{Path, PathBuf},
8 time::SystemTime,
9};
10use tokio::io::AsyncReadExt;
11
12#[derive(Debug)]
14pub struct ObservedFile {
15 path: PathBuf,
16 metadata: Metadata,
17 modified_at: SystemTime,
18}
19
20#[derive(Debug, thiserror::Error)]
22pub enum ObservationError {
23 #[error("file is missing: {}", .0.display())]
25 Missing(PathBuf),
26 #[error("file changed: {}", .0.display())]
28 Changed(PathBuf),
29 #[error(transparent)]
31 File(#[from] FileError),
32}
33
34impl ObservationError {
35 pub(super) fn at(operation: &'static str, path: &Path, error: io::Error) -> Self {
36 if error.kind() == io::ErrorKind::NotFound {
37 Self::Missing(path.to_owned())
38 } else {
39 FileError::at(operation, path, error).into()
40 }
41 }
42}
43
44impl ObservedFile {
45 pub fn path(&self) -> &Path {
47 &self.path
48 }
49 pub fn size(&self) -> u64 {
51 self.metadata.len()
52 }
53 pub fn modified_at(&self) -> SystemTime {
55 self.modified_at
56 }
57
58 pub fn validate(&self) -> Result<(), ObservationError> {
60 let metadata = std::fs::metadata(&self.path)
61 .map_err(|e| ObservationError::at("read original", &self.path, e))?;
62 self.check(&metadata)
63 }
64
65 fn check(&self, metadata: &Metadata) -> Result<(), ObservationError> {
66 let modified = metadata
67 .modified()
68 .map_err(|e| FileError::at("read modification time", &self.path, e))?;
69 let mut same =
70 metadata.is_file() && metadata.len() == self.size() && modified == self.modified_at;
71 #[cfg(unix)]
72 {
73 use std::os::unix::fs::MetadataExt;
74 same &= metadata.dev() == self.metadata.dev()
75 && metadata.ino() == self.metadata.ino()
76 && metadata.ctime() == self.metadata.ctime()
77 && metadata.ctime_nsec() == self.metadata.ctime_nsec();
78 }
79 #[cfg(windows)]
80 {
81 use std::os::windows::fs::MetadataExt;
82 same &= metadata.creation_time() == self.metadata.creation_time();
83 }
84 if same {
85 Ok(())
86 } else {
87 Err(ObservationError::Changed(self.path.clone()))
88 }
89 }
90}
91
92#[derive(Debug)]
97pub struct UserOriginal {
98 path: PathBuf,
99}
100
101impl UserOriginal {
102 pub fn at(path: PathBuf) -> Self {
104 Self { path }
105 }
106
107 pub fn path(&self) -> &Path {
109 &self.path
110 }
111
112 pub async fn observe(
115 &self,
116 consume: impl FnMut(&[u8]),
117 ) -> Result<ObservedFile, ObservationError> {
118 observe_file(&self.path, consume).await
119 }
120
121 pub fn open_reader(
124 &self,
125 size: u64,
126 modified: SystemTime,
127 ) -> Result<FileReader, ObservationError> {
128 FileReader::open_original(&self.path, size, modified)
129 }
130}
131
132pub(super) async fn observe_metadata(path: &Path) -> Result<ObservedFile, ObservationError> {
136 let file = tokio::fs::File::open(path)
137 .await
138 .map_err(|e| ObservationError::at("read original", path, e))?;
139 let observed = observed(path, &file).await?;
140 observed.validate()?;
141 Ok(observed)
142}
143
144async fn observed(path: &Path, file: &tokio::fs::File) -> Result<ObservedFile, ObservationError> {
148 let metadata =
149 file.metadata().await.map_err(|e| ObservationError::at("read original", path, e))?;
150 if !metadata.is_file() {
151 return Err(FileError::at(
152 "read original",
153 path,
154 io::Error::new(io::ErrorKind::InvalidInput, "a regular file is required"),
155 )
156 .into());
157 }
158 let modified_at =
159 metadata.modified().map_err(|e| FileError::at("read modification time", path, e))?;
160 Ok(ObservedFile { path: path.to_owned(), metadata, modified_at })
161}
162
163async fn observe_file(
164 path: &Path,
165 mut consume: impl FnMut(&[u8]),
166) -> Result<ObservedFile, ObservationError> {
167 let mut file = tokio::fs::File::open(path)
168 .await
169 .map_err(|e| ObservationError::at("read original", path, e))?;
170 let observed = observed(path, &file).await?;
171 let mut buffer = vec![0; 64 * 1024];
172 let mut size = 0u64;
173 loop {
174 let read = file
175 .read(&mut buffer)
176 .await
177 .map_err(|e| ObservationError::at("read original", path, e))?;
178 if read == 0 {
179 break;
180 }
181 size += read as u64;
182 consume(&buffer[..read]);
183 }
184 if size != observed.size() {
185 return Err(ObservationError::Changed(path.to_owned()));
186 }
187 observed.check(
188 &file.metadata().await.map_err(|e| ObservationError::at("read original", path, e))?,
189 )?;
190 observed.validate()?;
191 Ok(observed)
192}