##// END OF EJS Templates
rust: Add a log file rotation utility...
Simon Sapin -
r47341:1f55cd5b default
parent child Browse files
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 .for_file(path)
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.for_file(path)?.path();
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, derive_more::From)]
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) => path.display().fmt(f),
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 given path.
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: example.txt”
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 for_file(self, path: &std::path::Path) -> Result<T, HgError>;
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 for_file(self, path: &std::path::Path) -> Result<T, HgError> {
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: IoErrorContext::File(path.to_owned()),
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(crate) struct Vfs<'a> {
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(crate) fn hg_vfs(&self) -> Vfs<'_> {
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(crate) fn store_vfs(&self) -> Vfs<'_> {
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(crate) fn _working_directory_vfs(&self) -> Vfs<'_> {
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(crate) fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf {
220 pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf {
221 221 self.base.join(relative_path)
222 222 }
223 223
224 pub(crate) fn read(
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).for_file(&path)
229 std::fs::read(&path).when_reading_file(&path)
230 230 }
231 231
232 pub(crate) fn mmap_open(
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).for_file(&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) }.for_file(&path)?;
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 })
253 }
242 254 }
General Comments 0
You need to be logged in to leave comments. Login now