]>
Commit | Line | Data |
---|---|---|
b4d5787d WB |
1 | //! File system helper utilities. |
2 | ||
b12505da WB |
3 | use std::borrow::{Borrow, BorrowMut}; |
4 | use std::ops::{Deref, DerefMut}; | |
1db41615 | 5 | use std::os::unix::io::{AsRawFd, RawFd}; |
b4d5787d | 6 | |
770a36e5 | 7 | use anyhow::{bail, format_err, Error}; |
b4d5787d WB |
8 | use nix::dir; |
9 | use nix::dir::Dir; | |
3dc1a2d5 DM |
10 | use nix::fcntl::OFlag; |
11 | use nix::sys::stat::Mode; | |
12 | ||
53e1e7ca | 13 | use regex::Regex; |
b4d5787d | 14 | |
3dc1a2d5 DM |
15 | use proxmox::sys::error::SysError; |
16 | ||
770a36e5 | 17 | use crate::borrow::Tied; |
b4d5787d | 18 | |
3dc1a2d5 DM |
19 | pub type DirLockGuard = Dir; |
20 | ||
b12505da WB |
21 | /// This wraps nix::dir::Entry with the parent directory's file descriptor. |
22 | pub struct ReadDirEntry { | |
23 | entry: dir::Entry, | |
24 | parent_fd: RawFd, | |
25 | } | |
26 | ||
27 | impl Into<dir::Entry> for ReadDirEntry { | |
28 | fn into(self) -> dir::Entry { | |
29 | self.entry | |
30 | } | |
31 | } | |
32 | ||
33 | impl Deref for ReadDirEntry { | |
34 | type Target = dir::Entry; | |
35 | ||
36 | fn deref(&self) -> &Self::Target { | |
37 | &self.entry | |
38 | } | |
39 | } | |
40 | ||
41 | impl DerefMut for ReadDirEntry { | |
42 | fn deref_mut(&mut self) -> &mut Self::Target { | |
43 | &mut self.entry | |
44 | } | |
45 | } | |
46 | ||
47 | impl AsRef<dir::Entry> for ReadDirEntry { | |
48 | fn as_ref(&self) -> &dir::Entry { | |
49 | &self.entry | |
50 | } | |
51 | } | |
52 | ||
53 | impl AsMut<dir::Entry> for ReadDirEntry { | |
54 | fn as_mut(&mut self) -> &mut dir::Entry { | |
55 | &mut self.entry | |
56 | } | |
57 | } | |
58 | ||
59 | impl Borrow<dir::Entry> for ReadDirEntry { | |
60 | fn borrow(&self) -> &dir::Entry { | |
61 | &self.entry | |
62 | } | |
63 | } | |
64 | ||
65 | impl BorrowMut<dir::Entry> for ReadDirEntry { | |
66 | fn borrow_mut(&mut self) -> &mut dir::Entry { | |
67 | &mut self.entry | |
68 | } | |
69 | } | |
70 | ||
71 | impl ReadDirEntry { | |
72 | #[inline] | |
73 | pub fn parent_fd(&self) -> RawFd { | |
74 | self.parent_fd | |
75 | } | |
32286b03 WB |
76 | |
77 | pub unsafe fn file_name_utf8_unchecked(&self) -> &str { | |
78 | std::str::from_utf8_unchecked(self.file_name().to_bytes()) | |
79 | } | |
b12505da WB |
80 | } |
81 | ||
b4d5787d | 82 | // Since Tied<T, U> implements Deref to U, a Tied<Dir, Iterator> already implements Iterator. |
f7d4e4b5 | 83 | // This is simply a wrapper with a shorter type name mapping nix::Error to anyhow::Error. |
b4d5787d WB |
84 | /// Wrapper over a pair of `nix::dir::Dir` and `nix::dir::Iter`, returned by `read_subdir()`. |
85 | pub struct ReadDir { | |
dd5495d6 | 86 | iter: Tied<Dir, dyn Iterator<Item = nix::Result<dir::Entry>> + Send>, |
1db41615 | 87 | dir_fd: RawFd, |
b4d5787d WB |
88 | } |
89 | ||
90 | impl Iterator for ReadDir { | |
1db41615 | 91 | type Item = Result<ReadDirEntry, Error>; |
b4d5787d WB |
92 | |
93 | fn next(&mut self) -> Option<Self::Item> { | |
1db41615 WB |
94 | self.iter.next().map(|res| { |
95 | res.map(|entry| ReadDirEntry { entry, parent_fd: self.dir_fd }) | |
fc7f0352 | 96 | .map_err(Error::from) |
1db41615 | 97 | }) |
b4d5787d WB |
98 | } |
99 | } | |
100 | ||
101 | /// Create an iterator over sub directory entries. | |
102 | /// This uses `openat` on `dirfd`, so `path` can be relative to that or an absolute path. | |
c698636a | 103 | pub fn read_subdir<P: ?Sized + nix::NixPath>(dirfd: RawFd, path: &P) -> nix::Result<ReadDir> { |
b4d5787d | 104 | let dir = Dir::openat(dirfd, path, OFlag::O_RDONLY, Mode::empty())?; |
1db41615 | 105 | let fd = dir.as_raw_fd(); |
b4d5787d | 106 | let iter = Tied::new(dir, |dir| { |
dd5495d6 WB |
107 | Box::new(unsafe { (*dir).iter() }) |
108 | as Box<dyn Iterator<Item = nix::Result<dir::Entry>> + Send> | |
b4d5787d | 109 | }); |
1db41615 | 110 | Ok(ReadDir { iter, dir_fd: fd }) |
b4d5787d | 111 | } |
a25f863a WB |
112 | |
113 | /// Scan through a directory with a regular expression. This is simply a shortcut filtering the | |
add5861e | 114 | /// results of `read_subdir`. Non-UTF8 compatible file names are silently ignored. |
a25f863a WB |
115 | pub fn scan_subdir<'a, P: ?Sized + nix::NixPath>( |
116 | dirfd: RawFd, | |
117 | path: &P, | |
118 | regex: &'a regex::Regex, | |
a576e668 | 119 | ) -> Result<impl Iterator<Item = Result<ReadDirEntry, Error>> + 'a, nix::Error> { |
806d7a6a | 120 | Ok(read_subdir(dirfd, path)?.filter_file_name_regex(regex)) |
a25f863a | 121 | } |
23fba9d7 | 122 | |
770a36e5 WB |
123 | /// Scan directory for matching file names with a callback. |
124 | /// | |
125 | /// Scan through all directory entries and call `callback()` function | |
126 | /// if the entry name matches the regular expression. This function | |
127 | /// used unix `openat()`, so you can pass absolute or relative file | |
128 | /// names. This function simply skips non-UTF8 encoded names. | |
129 | pub fn scandir<P, F>( | |
130 | dirfd: RawFd, | |
131 | path: &P, | |
132 | regex: ®ex::Regex, | |
133 | mut callback: F, | |
134 | ) -> Result<(), Error> | |
135 | where | |
136 | F: FnMut(RawFd, &str, nix::dir::Type) -> Result<(), Error>, | |
137 | P: ?Sized + nix::NixPath, | |
138 | { | |
139 | for entry in scan_subdir(dirfd, path, regex)? { | |
140 | let entry = entry?; | |
141 | let file_type = match entry.file_type() { | |
142 | Some(file_type) => file_type, | |
143 | None => bail!("unable to detect file type"), | |
144 | }; | |
145 | ||
146 | callback( | |
147 | entry.parent_fd(), | |
148 | unsafe { entry.file_name_utf8_unchecked() }, | |
149 | file_type, | |
150 | )?; | |
151 | } | |
152 | Ok(()) | |
153 | } | |
154 | ||
155 | ||
23fba9d7 WB |
156 | /// Helper trait to provide a combinators for directory entry iterators. |
157 | pub trait FileIterOps<T, E> | |
158 | where | |
159 | Self: Sized + Iterator<Item = Result<T, E>>, | |
160 | T: Borrow<dir::Entry>, | |
161 | E: Into<Error> + Send + Sync, | |
162 | { | |
163 | /// Filter by file type. This is more convenient than using the `filter` method alone as this | |
164 | /// also includes error handling and handling of files without a type (via an error). | |
165 | fn filter_file_type(self, ty: dir::Type) -> FileTypeFilter<Self, T, E> { | |
166 | FileTypeFilter { inner: self, ty } | |
167 | } | |
53e1e7ca WB |
168 | |
169 | /// Filter by file name. Note that file names which aren't valid utf-8 will be treated as if | |
170 | /// they do not match the pattern. | |
62ee2eb4 | 171 | fn filter_file_name_regex(self, regex: &Regex) -> FileNameRegexFilter<Self, T, E> { |
53e1e7ca WB |
172 | FileNameRegexFilter { inner: self, regex } |
173 | } | |
23fba9d7 WB |
174 | } |
175 | ||
176 | impl<I, T, E> FileIterOps<T, E> for I | |
177 | where | |
178 | I: Iterator<Item = Result<T, E>>, | |
179 | T: Borrow<dir::Entry>, | |
180 | E: Into<Error> + Send + Sync, | |
181 | { | |
182 | } | |
183 | ||
184 | /// This filters files from its inner iterator by a file type. Files with no type produce an error. | |
185 | pub struct FileTypeFilter<I, T, E> | |
186 | where | |
187 | I: Iterator<Item = Result<T, E>>, | |
188 | T: Borrow<dir::Entry>, | |
189 | E: Into<Error> + Send + Sync, | |
190 | { | |
191 | inner: I, | |
192 | ty: nix::dir::Type, | |
193 | } | |
194 | ||
195 | impl<I, T, E> Iterator for FileTypeFilter<I, T, E> | |
196 | where | |
197 | I: Iterator<Item = Result<T, E>>, | |
198 | T: Borrow<dir::Entry>, | |
199 | E: Into<Error> + Send + Sync, | |
200 | { | |
201 | type Item = Result<T, Error>; | |
202 | ||
203 | fn next(&mut self) -> Option<Self::Item> { | |
204 | loop { | |
205 | let item = self.inner.next()?.map_err(|e| e.into()); | |
206 | match item { | |
207 | Ok(ref entry) => match entry.borrow().file_type() { | |
208 | Some(ty) => { | |
209 | if ty == self.ty { | |
210 | return Some(item); | |
211 | } else { | |
212 | continue; | |
213 | } | |
214 | } | |
215 | None => return Some(Err(format_err!("unable to detect file type"))), | |
216 | }, | |
217 | Err(_) => return Some(item), | |
218 | } | |
219 | } | |
220 | } | |
221 | } | |
222 | ||
53e1e7ca WB |
223 | /// This filters files by name via a Regex. Files whose file name aren't valid utf-8 are skipped |
224 | /// silently. | |
225 | pub struct FileNameRegexFilter<'a, I, T, E> | |
226 | where | |
227 | I: Iterator<Item = Result<T, E>>, | |
228 | T: Borrow<dir::Entry>, | |
229 | { | |
230 | inner: I, | |
231 | regex: &'a Regex, | |
232 | } | |
233 | ||
234 | impl<I, T, E> Iterator for FileNameRegexFilter<'_, I, T, E> | |
235 | where | |
236 | I: Iterator<Item = Result<T, E>>, | |
237 | T: Borrow<dir::Entry>, | |
238 | { | |
239 | type Item = Result<T, E>; | |
240 | ||
241 | fn next(&mut self) -> Option<Self::Item> { | |
242 | loop { | |
243 | let item = self.inner.next()?; | |
244 | match item { | |
245 | Ok(ref entry) => { | |
246 | if let Ok(name) = entry.borrow().file_name().to_str() { | |
247 | if self.regex.is_match(name) { | |
248 | return Some(item); | |
249 | } | |
250 | } | |
251 | // file did not match regex or isn't valid utf-8 | |
252 | continue; | |
253 | }, | |
254 | Err(_) => return Some(item), | |
255 | } | |
256 | } | |
257 | } | |
258 | } | |
042babe4 | 259 | |
042babe4 CE |
260 | // /usr/include/linux/fs.h: #define FS_IOC_GETFLAGS _IOR('f', 1, long) |
261 | // read Linux file system attributes (see man chattr) | |
032cd1b8 WB |
262 | nix::ioctl_read!(read_attr_fd, b'f', 1, libc::c_long); |
263 | nix::ioctl_write_ptr!(write_attr_fd, b'f', 2, libc::c_long); | |
042babe4 CE |
264 | |
265 | // /usr/include/linux/msdos_fs.h: #define FAT_IOCTL_GET_ATTRIBUTES _IOR('r', 0x10, __u32) | |
266 | // read FAT file system attributes | |
267 | nix::ioctl_read!(read_fat_attr_fd, b'r', 0x10, u32); | |
032cd1b8 | 268 | nix::ioctl_write_ptr!(write_fat_attr_fd, b'r', 0x11, u32); |
042babe4 CE |
269 | |
270 | // From /usr/include/linux/fs.h | |
271 | // #define FS_IOC_FSGETXATTR _IOR('X', 31, struct fsxattr) | |
272 | // #define FS_IOC_FSSETXATTR _IOW('X', 32, struct fsxattr) | |
273 | nix::ioctl_read!(fs_ioc_fsgetxattr, b'X', 31, FSXAttr); | |
274 | nix::ioctl_write_ptr!(fs_ioc_fssetxattr, b'X', 32, FSXAttr); | |
275 | ||
276 | #[repr(C)] | |
277 | #[derive(Debug)] | |
278 | pub struct FSXAttr { | |
279 | pub fsx_xflags: u32, | |
280 | pub fsx_extsize: u32, | |
281 | pub fsx_nextents: u32, | |
282 | pub fsx_projid: u32, | |
283 | pub fsx_cowextsize: u32, | |
284 | pub fsx_pad: [u8; 8], | |
285 | } | |
286 | ||
287 | impl Default for FSXAttr { | |
288 | fn default() -> Self { | |
289 | FSXAttr { | |
290 | fsx_xflags: 0u32, | |
291 | fsx_extsize: 0u32, | |
292 | fsx_nextents: 0u32, | |
293 | fsx_projid: 0u32, | |
294 | fsx_cowextsize: 0u32, | |
295 | fsx_pad: [0u8; 8], | |
296 | } | |
297 | } | |
298 | } | |
3dc1a2d5 | 299 | |
7d6c4c39 SR |
300 | /// Attempt to acquire a shared flock on the given path, 'what' and |
301 | /// 'would_block_message' are used for error formatting. | |
302 | pub fn lock_dir_noblock_shared( | |
303 | path: &std::path::Path, | |
304 | what: &str, | |
305 | would_block_msg: &str, | |
306 | ) -> Result<DirLockGuard, Error> { | |
307 | do_lock_dir_noblock(path, what, would_block_msg, false) | |
308 | } | |
3dc1a2d5 | 309 | |
7d6c4c39 SR |
310 | /// Attempt to acquire an exclusive flock on the given path, 'what' and |
311 | /// 'would_block_message' are used for error formatting. | |
3dc1a2d5 DM |
312 | pub fn lock_dir_noblock( |
313 | path: &std::path::Path, | |
314 | what: &str, | |
315 | would_block_msg: &str, | |
7d6c4c39 SR |
316 | ) -> Result<DirLockGuard, Error> { |
317 | do_lock_dir_noblock(path, what, would_block_msg, true) | |
318 | } | |
319 | ||
320 | fn do_lock_dir_noblock( | |
321 | path: &std::path::Path, | |
322 | what: &str, | |
323 | would_block_msg: &str, | |
324 | exclusive: bool, | |
3dc1a2d5 DM |
325 | ) -> Result<DirLockGuard, Error> { |
326 | let mut handle = Dir::open(path, OFlag::O_RDONLY, Mode::empty()) | |
327 | .map_err(|err| { | |
328 | format_err!("unable to open {} directory {:?} for locking - {}", what, path, err) | |
329 | })?; | |
330 | ||
331 | // acquire in non-blocking mode, no point in waiting here since other | |
332 | // backups could still take a very long time | |
7d6c4c39 | 333 | proxmox::tools::fs::lock_file(&mut handle, exclusive, Some(std::time::Duration::from_nanos(0))) |
3dc1a2d5 DM |
334 | .map_err(|err| { |
335 | format_err!( | |
336 | "unable to acquire lock on {} directory {:?} - {}", what, path, | |
337 | if err.would_block() { | |
338 | String::from(would_block_msg) | |
339 | } else { | |
340 | err.to_string() | |
341 | } | |
342 | ) | |
343 | })?; | |
344 | ||
345 | Ok(handle) | |
346 | } |