Show More
@@ -0,0 +1,101 b'' | |||
|
1 | use crate::errors::{HgError, HgResultExt, IoErrorContext, IoResultExt}; | |
|
2 | use crate::repo::Vfs; | |
|
3 | use std::io::Write; | |
|
4 | ||
|
5 | /// An utility to append to a log file with the given name, and optionally | |
|
6 | /// rotate it after it reaches a certain maximum size. | |
|
7 | /// | |
|
8 | /// Rotation works by renaming "example.log" to "example.log.1", after renaming | |
|
9 | /// "example.log.1" to "example.log.2" etc up to the given maximum number of | |
|
10 | /// files. | |
|
11 | pub struct LogFile<'a> { | |
|
12 | vfs: Vfs<'a>, | |
|
13 | name: &'a str, | |
|
14 | max_size: Option<u64>, | |
|
15 | max_files: u32, | |
|
16 | } | |
|
17 | ||
|
18 | impl<'a> LogFile<'a> { | |
|
19 | pub fn new(vfs: Vfs<'a>, name: &'a str) -> Self { | |
|
20 | Self { | |
|
21 | vfs, | |
|
22 | name, | |
|
23 | max_size: None, | |
|
24 | max_files: 0, | |
|
25 | } | |
|
26 | } | |
|
27 | ||
|
28 | /// Rotate before writing to a log file that was already larger than the | |
|
29 | /// given size, in bytes. `None` disables rotation. | |
|
30 | pub fn max_size(mut self, value: Option<u64>) -> Self { | |
|
31 | self.max_size = value; | |
|
32 | self | |
|
33 | } | |
|
34 | ||
|
35 | /// Keep this many rotated files `{name}.1` up to `{name}.{max}`, in | |
|
36 | /// addition to the original `{name}` file. | |
|
37 | pub fn max_files(mut self, value: u32) -> Self { | |
|
38 | self.max_files = value; | |
|
39 | self | |
|
40 | } | |
|
41 | ||
|
42 | /// Append the given `bytes` as-is to the log file, after rotating if | |
|
43 | /// needed. | |
|
44 | /// | |
|
45 | /// No trailing newline is added. Make sure to include one in `bytes` if | |
|
46 | /// desired. | |
|
47 | pub fn write(&self, bytes: &[u8]) -> Result<(), HgError> { | |
|
48 | let path = self.vfs.join(self.name); | |
|
49 | let context = || IoErrorContext::WritingFile(path.clone()); | |
|
50 | let open = || { | |
|
51 | std::fs::OpenOptions::new() | |
|
52 | .create(true) | |
|
53 | .append(true) | |
|
54 | .open(&path) | |
|
55 | .with_context(context) | |
|
56 | }; | |
|
57 | let mut file = open()?; | |
|
58 | if let Some(max_size) = self.max_size { | |
|
59 | if file.metadata().with_context(context)?.len() >= max_size { | |
|
60 | // For example with `max_files == 5`, the first iteration of | |
|
61 | // this loop has `i == 4` and renames `{name}.4` to `{name}.5`. | |
|
62 | // The last iteration renames `{name}.1` to | |
|
63 | // `{name}.2` | |
|
64 | for i in (1..self.max_files).rev() { | |
|
65 | self.vfs | |
|
66 | .rename( | |
|
67 | format!("{}.{}", self.name, i), | |
|
68 | format!("{}.{}", self.name, i + 1), | |
|
69 | ) | |
|
70 | .io_not_found_as_none()?; | |
|
71 | } | |
|
72 | // Then rename `{name}` to `{name}.1`. This is the | |
|
73 | // previously-opened `file`. | |
|
74 | self.vfs | |
|
75 | .rename(self.name, format!("{}.1", self.name)) | |
|
76 | .io_not_found_as_none()?; | |
|
77 | // Finally, create a new `{name}` file and replace our `file` | |
|
78 | // handle. | |
|
79 | file = open()?; | |
|
80 | } | |
|
81 | } | |
|
82 | file.write_all(bytes).with_context(context)?; | |
|
83 | file.sync_all().with_context(context) | |
|
84 | } | |
|
85 | } | |
|
86 | ||
|
87 | #[test] | |
|
88 | fn test_rotation() { | |
|
89 | let temp = tempfile::tempdir().unwrap(); | |
|
90 | let vfs = Vfs { base: temp.path() }; | |
|
91 | let logger = LogFile::new(vfs, "log").max_size(Some(3)).max_files(2); | |
|
92 | logger.write(b"one\n").unwrap(); | |
|
93 | logger.write(b"two\n").unwrap(); | |
|
94 | logger.write(b"3\n").unwrap(); | |
|
95 | logger.write(b"four\n").unwrap(); | |
|
96 | logger.write(b"five\n").unwrap(); | |
|
97 | assert_eq!(vfs.read("log").unwrap(), b"five\n"); | |
|
98 | assert_eq!(vfs.read("log.1").unwrap(), b"3\nfour\n"); | |
|
99 | assert_eq!(vfs.read("log.2").unwrap(), b"two\n"); | |
|
100 | assert!(vfs.read("log.3").io_not_found_as_none().unwrap().is_none()); | |
|
101 | } |
@@ -137,11 +137,11 b' impl Config {' | |||
|
137 | 137 | |
|
138 | 138 | fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> { |
|
139 | 139 | if let Some(entries) = std::fs::read_dir(path) |
|
140 |
. |
|
|
140 | .when_reading_file(path) | |
|
141 | 141 | .io_not_found_as_none()? |
|
142 | 142 | { |
|
143 | 143 | for entry in entries { |
|
144 |
let file_path = entry. |
|
|
144 | let file_path = entry.when_reading_file(path)?.path(); | |
|
145 | 145 | if file_path.extension() == Some(std::ffi::OsStr::new("rc")) { |
|
146 | 146 | self.add_trusted_file(&file_path)? |
|
147 | 147 | } |
@@ -151,8 +151,9 b' impl Config {' | |||
|
151 | 151 | } |
|
152 | 152 | |
|
153 | 153 | fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> { |
|
154 | if let Some(data) = | |
|
155 | std::fs::read(path).for_file(path).io_not_found_as_none()? | |
|
154 | if let Some(data) = std::fs::read(path) | |
|
155 | .when_reading_file(path) | |
|
156 | .io_not_found_as_none()? | |
|
156 | 157 | { |
|
157 | 158 | self.layers.extend(ConfigLayer::parse(path, &data)?) |
|
158 | 159 | } |
@@ -150,7 +150,8 b' impl ConfigLayer {' | |||
|
150 | 150 | // `Path::join` with an absolute argument correctly ignores the |
|
151 | 151 | // base path |
|
152 | 152 | let filename = dir.join(&get_path_from_bytes(&filename_bytes)); |
|
153 | let data = std::fs::read(&filename).for_file(&filename)?; | |
|
153 | let data = | |
|
154 | std::fs::read(&filename).when_reading_file(&filename)?; | |
|
154 | 155 | layers.push(current_layer); |
|
155 | 156 | layers.extend(Self::parse(&filename, &data)?); |
|
156 | 157 | current_layer = Self::new(ConfigOrigin::File(src.to_owned())); |
@@ -41,11 +41,15 b' pub enum HgError {' | |||
|
41 | 41 | } |
|
42 | 42 | |
|
43 | 43 | /// Details about where an I/O error happened |
|
44 |
#[derive(Debug |
|
|
44 | #[derive(Debug)] | |
|
45 | 45 | pub enum IoErrorContext { |
|
46 | /// A filesystem operation for the given file | |
|
47 | #[from] | |
|
48 | File(std::path::PathBuf), | |
|
46 | ReadingFile(std::path::PathBuf), | |
|
47 | WritingFile(std::path::PathBuf), | |
|
48 | RemovingFile(std::path::PathBuf), | |
|
49 | RenamingFile { | |
|
50 | from: std::path::PathBuf, | |
|
51 | to: std::path::PathBuf, | |
|
52 | }, | |
|
49 | 53 | /// `std::env::current_dir` |
|
50 | 54 | CurrentDir, |
|
51 | 55 | /// `std::env::current_exe` |
@@ -109,28 +113,55 b' impl fmt::Display for HgError {' | |||
|
109 | 113 | impl fmt::Display for IoErrorContext { |
|
110 | 114 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
|
111 | 115 | match self { |
|
112 |
IoErrorContext::File(path) => |
|
|
113 | IoErrorContext::CurrentDir => f.write_str("current directory"), | |
|
114 | IoErrorContext::CurrentExe => f.write_str("current executable"), | |
|
116 | IoErrorContext::ReadingFile(path) => { | |
|
117 | write!(f, "when reading {}", path.display()) | |
|
118 | } | |
|
119 | IoErrorContext::WritingFile(path) => { | |
|
120 | write!(f, "when writing {}", path.display()) | |
|
121 | } | |
|
122 | IoErrorContext::RemovingFile(path) => { | |
|
123 | write!(f, "when removing {}", path.display()) | |
|
124 | } | |
|
125 | IoErrorContext::RenamingFile { from, to } => write!( | |
|
126 | f, | |
|
127 | "when renaming {} to {}", | |
|
128 | from.display(), | |
|
129 | to.display() | |
|
130 | ), | |
|
131 | IoErrorContext::CurrentDir => write!(f, "current directory"), | |
|
132 | IoErrorContext::CurrentExe => write!(f, "current executable"), | |
|
115 | 133 | } |
|
116 | 134 | } |
|
117 | 135 | } |
|
118 | 136 | |
|
119 | 137 | pub trait IoResultExt<T> { |
|
120 |
/// Annotate a possible I/O error as related to a file at the |
|
|
138 | /// Annotate a possible I/O error as related to a reading a file at the | |
|
139 | /// given path. | |
|
121 | 140 | /// |
|
122 |
/// This allows printing something like βFile not found |
|
|
123 | /// instead of just βFile not foundβ. | |
|
141 | /// This allows printing something like βFile not found when reading | |
|
142 | /// example.txtβ instead of just βFile not foundβ. | |
|
124 | 143 | /// |
|
125 | 144 | /// Converts a `Result` with `std::io::Error` into one with `HgError`. |
|
126 |
fn |
|
|
145 | fn when_reading_file(self, path: &std::path::Path) -> Result<T, HgError>; | |
|
146 | ||
|
147 | fn with_context( | |
|
148 | self, | |
|
149 | context: impl FnOnce() -> IoErrorContext, | |
|
150 | ) -> Result<T, HgError>; | |
|
127 | 151 | } |
|
128 | 152 | |
|
129 | 153 | impl<T> IoResultExt<T> for std::io::Result<T> { |
|
130 |
fn |
|
|
154 | fn when_reading_file(self, path: &std::path::Path) -> Result<T, HgError> { | |
|
155 | self.with_context(|| IoErrorContext::ReadingFile(path.to_owned())) | |
|
156 | } | |
|
157 | ||
|
158 | fn with_context( | |
|
159 | self, | |
|
160 | context: impl FnOnce() -> IoErrorContext, | |
|
161 | ) -> Result<T, HgError> { | |
|
131 | 162 | self.map_err(|error| HgError::IoError { |
|
132 | 163 | error, |
|
133 |
context: |
|
|
164 | context: context(), | |
|
134 | 165 | }) |
|
135 | 166 | } |
|
136 | 167 | } |
@@ -29,6 +29,7 b' pub mod repo;' | |||
|
29 | 29 | pub mod revlog; |
|
30 | 30 | pub use revlog::*; |
|
31 | 31 | pub mod config; |
|
32 | pub mod logging; | |
|
32 | 33 | pub mod operations; |
|
33 | 34 | pub mod revset; |
|
34 | 35 | pub mod utils; |
@@ -1,5 +1,5 b'' | |||
|
1 | 1 | use crate::config::{Config, ConfigError, ConfigParseError}; |
|
2 | use crate::errors::{HgError, IoResultExt}; | |
|
2 | use crate::errors::{HgError, IoErrorContext, IoResultExt}; | |
|
3 | 3 | use crate::requirements; |
|
4 | 4 | use crate::utils::current_dir; |
|
5 | 5 | use crate::utils::files::get_path_from_bytes; |
@@ -38,8 +38,8 b' impl From<ConfigError> for RepoError {' | |||
|
38 | 38 | |
|
39 | 39 | /// Filesystem access abstraction for the contents of a given "base" diretory |
|
40 | 40 | #[derive(Clone, Copy)] |
|
41 |
pub |
|
|
42 | base: &'a Path, | |
|
41 | pub struct Vfs<'a> { | |
|
42 | pub(crate) base: &'a Path, | |
|
43 | 43 | } |
|
44 | 44 | |
|
45 | 45 | impl Repo { |
@@ -196,12 +196,12 b' impl Repo {' | |||
|
196 | 196 | |
|
197 | 197 | /// For accessing repository files (in `.hg`), except for the store |
|
198 | 198 | /// (`.hg/store`). |
|
199 |
pub |
|
|
199 | pub fn hg_vfs(&self) -> Vfs<'_> { | |
|
200 | 200 | Vfs { base: &self.dot_hg } |
|
201 | 201 | } |
|
202 | 202 | |
|
203 | 203 | /// For accessing repository store files (in `.hg/store`) |
|
204 |
pub |
|
|
204 | pub fn store_vfs(&self) -> Vfs<'_> { | |
|
205 | 205 | Vfs { base: &self.store } |
|
206 | 206 | } |
|
207 | 207 | |
@@ -209,7 +209,7 b' impl Repo {' | |||
|
209 | 209 | |
|
210 | 210 | // The undescore prefix silences the "never used" warning. Remove before |
|
211 | 211 | // using. |
|
212 |
pub |
|
|
212 | pub fn _working_directory_vfs(&self) -> Vfs<'_> { | |
|
213 | 213 | Vfs { |
|
214 | 214 | base: &self.working_directory, |
|
215 | 215 | } |
@@ -217,26 +217,38 b' impl Repo {' | |||
|
217 | 217 | } |
|
218 | 218 | |
|
219 | 219 | impl Vfs<'_> { |
|
220 |
pub |
|
|
220 | pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf { | |
|
221 | 221 | self.base.join(relative_path) |
|
222 | 222 | } |
|
223 | 223 | |
|
224 |
pub |
|
|
224 | pub fn read( | |
|
225 | 225 | &self, |
|
226 | 226 | relative_path: impl AsRef<Path>, |
|
227 | 227 | ) -> Result<Vec<u8>, HgError> { |
|
228 | 228 | let path = self.join(relative_path); |
|
229 |
std::fs::read(&path). |
|
|
229 | std::fs::read(&path).when_reading_file(&path) | |
|
230 | 230 | } |
|
231 | 231 | |
|
232 |
pub |
|
|
232 | pub fn mmap_open( | |
|
233 | 233 | &self, |
|
234 | 234 | relative_path: impl AsRef<Path>, |
|
235 | 235 | ) -> Result<Mmap, HgError> { |
|
236 | 236 | let path = self.base.join(relative_path); |
|
237 |
let file = std::fs::File::open(&path). |
|
|
237 | let file = std::fs::File::open(&path).when_reading_file(&path)?; | |
|
238 | 238 | // TODO: what are the safety requirements here? |
|
239 |
let mmap = unsafe { MmapOptions::new().map(&file) } |
|
|
239 | let mmap = unsafe { MmapOptions::new().map(&file) } | |
|
240 | .when_reading_file(&path)?; | |
|
240 | 241 | Ok(mmap) |
|
241 | 242 | } |
|
243 | ||
|
244 | pub fn rename( | |
|
245 | &self, | |
|
246 | relative_from: impl AsRef<Path>, | |
|
247 | relative_to: impl AsRef<Path>, | |
|
248 | ) -> Result<(), HgError> { | |
|
249 | let from = self.join(relative_from); | |
|
250 | let to = self.join(relative_to); | |
|
251 | std::fs::rename(&from, &to) | |
|
252 | .with_context(|| IoErrorContext::RenamingFile { from, to }) | |
|
242 | 253 | } |
|
254 | } |
General Comments 0
You need to be logged in to leave comments.
Login now