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 | fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> { |
|
138 | fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> { | |
139 | if let Some(entries) = std::fs::read_dir(path) |
|
139 | if let Some(entries) = std::fs::read_dir(path) | |
140 |
. |
|
140 | .when_reading_file(path) | |
141 | .io_not_found_as_none()? |
|
141 | .io_not_found_as_none()? | |
142 | { |
|
142 | { | |
143 | for entry in entries { |
|
143 | for entry in entries { | |
144 |
let file_path = entry. |
|
144 | let file_path = entry.when_reading_file(path)?.path(); | |
145 | if file_path.extension() == Some(std::ffi::OsStr::new("rc")) { |
|
145 | if file_path.extension() == Some(std::ffi::OsStr::new("rc")) { | |
146 | self.add_trusted_file(&file_path)? |
|
146 | self.add_trusted_file(&file_path)? | |
147 | } |
|
147 | } | |
@@ -151,8 +151,9 b' impl Config {' | |||||
151 | } |
|
151 | } | |
152 |
|
152 | |||
153 | fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> { |
|
153 | fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> { | |
154 | if let Some(data) = |
|
154 | if let Some(data) = std::fs::read(path) | |
155 | std::fs::read(path).for_file(path).io_not_found_as_none()? |
|
155 | .when_reading_file(path) | |
|
156 | .io_not_found_as_none()? | |||
156 | { |
|
157 | { | |
157 | self.layers.extend(ConfigLayer::parse(path, &data)?) |
|
158 | self.layers.extend(ConfigLayer::parse(path, &data)?) | |
158 | } |
|
159 | } |
@@ -150,7 +150,8 b' impl ConfigLayer {' | |||||
150 | // `Path::join` with an absolute argument correctly ignores the |
|
150 | // `Path::join` with an absolute argument correctly ignores the | |
151 | // base path |
|
151 | // base path | |
152 | let filename = dir.join(&get_path_from_bytes(&filename_bytes)); |
|
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 | layers.push(current_layer); |
|
155 | layers.push(current_layer); | |
155 | layers.extend(Self::parse(&filename, &data)?); |
|
156 | layers.extend(Self::parse(&filename, &data)?); | |
156 | current_layer = Self::new(ConfigOrigin::File(src.to_owned())); |
|
157 | current_layer = Self::new(ConfigOrigin::File(src.to_owned())); |
@@ -41,11 +41,15 b' pub enum HgError {' | |||||
41 | } |
|
41 | } | |
42 |
|
42 | |||
43 | /// Details about where an I/O error happened |
|
43 | /// Details about where an I/O error happened | |
44 |
#[derive(Debug |
|
44 | #[derive(Debug)] | |
45 | pub enum IoErrorContext { |
|
45 | pub enum IoErrorContext { | |
46 | /// A filesystem operation for the given file |
|
46 | ReadingFile(std::path::PathBuf), | |
47 | #[from] |
|
47 | WritingFile(std::path::PathBuf), | |
48 | File(std::path::PathBuf), |
|
48 | RemovingFile(std::path::PathBuf), | |
|
49 | RenamingFile { | |||
|
50 | from: std::path::PathBuf, | |||
|
51 | to: std::path::PathBuf, | |||
|
52 | }, | |||
49 | /// `std::env::current_dir` |
|
53 | /// `std::env::current_dir` | |
50 | CurrentDir, |
|
54 | CurrentDir, | |
51 | /// `std::env::current_exe` |
|
55 | /// `std::env::current_exe` | |
@@ -109,28 +113,55 b' impl fmt::Display for HgError {' | |||||
109 | impl fmt::Display for IoErrorContext { |
|
113 | impl fmt::Display for IoErrorContext { | |
110 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
|
114 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
111 | match self { |
|
115 | match self { | |
112 |
IoErrorContext::File(path) => |
|
116 | IoErrorContext::ReadingFile(path) => { | |
113 | IoErrorContext::CurrentDir => f.write_str("current directory"), |
|
117 | write!(f, "when reading {}", path.display()) | |
114 | IoErrorContext::CurrentExe => f.write_str("current executable"), |
|
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 | pub trait IoResultExt<T> { |
|
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 |
|
141 | /// This allows printing something like βFile not found when reading | |
123 | /// instead of just βFile not foundβ. |
|
142 | /// example.txtβ instead of just βFile not foundβ. | |
124 | /// |
|
143 | /// | |
125 | /// Converts a `Result` with `std::io::Error` into one with `HgError`. |
|
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 | impl<T> IoResultExt<T> for std::io::Result<T> { |
|
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 | self.map_err(|error| HgError::IoError { |
|
162 | self.map_err(|error| HgError::IoError { | |
132 | error, |
|
163 | error, | |
133 |
context: |
|
164 | context: context(), | |
134 | }) |
|
165 | }) | |
135 | } |
|
166 | } | |
136 | } |
|
167 | } |
@@ -29,6 +29,7 b' pub mod repo;' | |||||
29 | pub mod revlog; |
|
29 | pub mod revlog; | |
30 | pub use revlog::*; |
|
30 | pub use revlog::*; | |
31 | pub mod config; |
|
31 | pub mod config; | |
|
32 | pub mod logging; | |||
32 | pub mod operations; |
|
33 | pub mod operations; | |
33 | pub mod revset; |
|
34 | pub mod revset; | |
34 | pub mod utils; |
|
35 | pub mod utils; |
@@ -1,5 +1,5 b'' | |||||
1 | use crate::config::{Config, ConfigError, ConfigParseError}; |
|
1 | use crate::config::{Config, ConfigError, ConfigParseError}; | |
2 | use crate::errors::{HgError, IoResultExt}; |
|
2 | use crate::errors::{HgError, IoErrorContext, IoResultExt}; | |
3 | use crate::requirements; |
|
3 | use crate::requirements; | |
4 | use crate::utils::current_dir; |
|
4 | use crate::utils::current_dir; | |
5 | use crate::utils::files::get_path_from_bytes; |
|
5 | use crate::utils::files::get_path_from_bytes; | |
@@ -38,8 +38,8 b' impl From<ConfigError> for RepoError {' | |||||
38 |
|
38 | |||
39 | /// Filesystem access abstraction for the contents of a given "base" diretory |
|
39 | /// Filesystem access abstraction for the contents of a given "base" diretory | |
40 | #[derive(Clone, Copy)] |
|
40 | #[derive(Clone, Copy)] | |
41 |
pub |
|
41 | pub struct Vfs<'a> { | |
42 | base: &'a Path, |
|
42 | pub(crate) base: &'a Path, | |
43 | } |
|
43 | } | |
44 |
|
44 | |||
45 | impl Repo { |
|
45 | impl Repo { | |
@@ -196,12 +196,12 b' impl Repo {' | |||||
196 |
|
196 | |||
197 | /// For accessing repository files (in `.hg`), except for the store |
|
197 | /// For accessing repository files (in `.hg`), except for the store | |
198 | /// (`.hg/store`). |
|
198 | /// (`.hg/store`). | |
199 |
pub |
|
199 | pub fn hg_vfs(&self) -> Vfs<'_> { | |
200 | Vfs { base: &self.dot_hg } |
|
200 | Vfs { base: &self.dot_hg } | |
201 | } |
|
201 | } | |
202 |
|
202 | |||
203 | /// For accessing repository store files (in `.hg/store`) |
|
203 | /// For accessing repository store files (in `.hg/store`) | |
204 |
pub |
|
204 | pub fn store_vfs(&self) -> Vfs<'_> { | |
205 | Vfs { base: &self.store } |
|
205 | Vfs { base: &self.store } | |
206 | } |
|
206 | } | |
207 |
|
207 | |||
@@ -209,7 +209,7 b' impl Repo {' | |||||
209 |
|
209 | |||
210 | // The undescore prefix silences the "never used" warning. Remove before |
|
210 | // The undescore prefix silences the "never used" warning. Remove before | |
211 | // using. |
|
211 | // using. | |
212 |
pub |
|
212 | pub fn _working_directory_vfs(&self) -> Vfs<'_> { | |
213 | Vfs { |
|
213 | Vfs { | |
214 | base: &self.working_directory, |
|
214 | base: &self.working_directory, | |
215 | } |
|
215 | } | |
@@ -217,26 +217,38 b' impl Repo {' | |||||
217 | } |
|
217 | } | |
218 |
|
218 | |||
219 | impl Vfs<'_> { |
|
219 | impl Vfs<'_> { | |
220 |
pub |
|
220 | pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf { | |
221 | self.base.join(relative_path) |
|
221 | self.base.join(relative_path) | |
222 | } |
|
222 | } | |
223 |
|
223 | |||
224 |
pub |
|
224 | pub fn read( | |
225 | &self, |
|
225 | &self, | |
226 | relative_path: impl AsRef<Path>, |
|
226 | relative_path: impl AsRef<Path>, | |
227 | ) -> Result<Vec<u8>, HgError> { |
|
227 | ) -> Result<Vec<u8>, HgError> { | |
228 | let path = self.join(relative_path); |
|
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 | &self, |
|
233 | &self, | |
234 | relative_path: impl AsRef<Path>, |
|
234 | relative_path: impl AsRef<Path>, | |
235 | ) -> Result<Mmap, HgError> { |
|
235 | ) -> Result<Mmap, HgError> { | |
236 | let path = self.base.join(relative_path); |
|
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 | // TODO: what are the safety requirements here? |
|
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 | Ok(mmap) |
|
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 }) | |||
|
253 | } | |||
242 | } |
|
254 | } |
General Comments 0
You need to be logged in to leave comments.
Login now