1 //! Extended utilities for working with files and filesystems in Rust.
2 
3 #![doc(html_root_url = "https://docs.rs/fs2/0.4.3")]
4 
5 #![cfg_attr(test, feature(test))]
6 
7 #[cfg(windows)]
8 extern crate winapi;
9 
10 #[cfg(unix)]
11 mod unix;
12 #[cfg(unix)]
13 use unix as sys;
14 
15 #[cfg(windows)]
16 mod windows;
17 #[cfg(windows)]
18 use windows as sys;
19 
20 use std::fs::File;
21 use std::io::{Error, Result};
22 use std::path::Path;
23 
24 /// Extension trait for `std::fs::File` which provides allocation, duplication and locking methods.
25 ///
26 /// ## Notes on File Locks
27 ///
28 /// This library provides whole-file locks in both shared (read) and exclusive
29 /// (read-write) varieties.
30 ///
31 /// File locks are a cross-platform hazard since the file lock APIs exposed by
32 /// operating system kernels vary in subtle and not-so-subtle ways.
33 ///
34 /// The API exposed by this library can be safely used across platforms as long
35 /// as the following rules are followed:
36 ///
37 ///   * Multiple locks should not be created on an individual `File` instance
38 ///     concurrently.
39 ///   * Duplicated files should not be locked without great care.
40 ///   * Files to be locked should be opened with at least read or write
41 ///     permissions.
42 ///   * File locks may only be relied upon to be advisory.
43 ///
44 /// See the tests in `lib.rs` for cross-platform lock behavior that may be
45 /// relied upon; see the tests in `unix.rs` and `windows.rs` for examples of
46 /// platform-specific behavior. File locks are implemented with
47 /// [`flock(2)`](http://man7.org/linux/man-pages/man2/flock.2.html) on Unix and
48 /// [`LockFile`](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365202(v=vs.85).aspx)
49 /// on Windows.
50 pub trait FileExt {
51 
52     /// Returns a duplicate instance of the file.
53     ///
54     /// The returned file will share the same file position as the original
55     /// file.
56     ///
57     /// If using rustc version 1.9 or later, prefer using `File::try_clone` to this.
58     ///
59     /// # Notes
60     ///
61     /// This is implemented with
62     /// [`dup(2)`](http://man7.org/linux/man-pages/man2/dup.2.html) on Unix and
63     /// [`DuplicateHandle`](https://msdn.microsoft.com/en-us/library/windows/desktop/ms724251(v=vs.85).aspx)
64     /// on Windows.
duplicate(&self) -> Result<File>65     fn duplicate(&self) -> Result<File>;
66 
67     /// Returns the amount of physical space allocated for a file.
allocated_size(&self) -> Result<u64>68     fn allocated_size(&self) -> Result<u64>;
69 
70     /// Ensures that at least `len` bytes of disk space are allocated for the
71     /// file, and the file size is at least `len` bytes. After a successful call
72     /// to `allocate`, subsequent writes to the file within the specified length
73     /// are guaranteed not to fail because of lack of disk space.
allocate(&self, len: u64) -> Result<()>74     fn allocate(&self, len: u64) -> Result<()>;
75 
76     /// Locks the file for shared usage, blocking if the file is currently
77     /// locked exclusively.
lock_shared(&self) -> Result<()>78     fn lock_shared(&self) -> Result<()>;
79 
80     /// Locks the file for exclusive usage, blocking if the file is currently
81     /// locked.
lock_exclusive(&self) -> Result<()>82     fn lock_exclusive(&self) -> Result<()>;
83 
84     /// Locks the file for shared usage, or returns a an error if the file is
85     /// currently locked (see `lock_contended_error`).
try_lock_shared(&self) -> Result<()>86     fn try_lock_shared(&self) -> Result<()>;
87 
88     /// Locks the file for shared usage, or returns a an error if the file is
89     /// currently locked (see `lock_contended_error`).
try_lock_exclusive(&self) -> Result<()>90     fn try_lock_exclusive(&self) -> Result<()>;
91 
92     /// Unlocks the file.
unlock(&self) -> Result<()>93     fn unlock(&self) -> Result<()>;
94 }
95 
96 impl FileExt for File {
duplicate(&self) -> Result<File>97     fn duplicate(&self) -> Result<File> {
98         sys::duplicate(self)
99     }
allocated_size(&self) -> Result<u64>100     fn allocated_size(&self) -> Result<u64> {
101         sys::allocated_size(self)
102     }
allocate(&self, len: u64) -> Result<()>103     fn allocate(&self, len: u64) -> Result<()> {
104         sys::allocate(self, len)
105     }
lock_shared(&self) -> Result<()>106     fn lock_shared(&self) -> Result<()> {
107         sys::lock_shared(self)
108     }
lock_exclusive(&self) -> Result<()>109     fn lock_exclusive(&self) -> Result<()> {
110         sys::lock_exclusive(self)
111     }
try_lock_shared(&self) -> Result<()>112     fn try_lock_shared(&self) -> Result<()> {
113         sys::try_lock_shared(self)
114     }
try_lock_exclusive(&self) -> Result<()>115     fn try_lock_exclusive(&self) -> Result<()> {
116         sys::try_lock_exclusive(self)
117     }
unlock(&self) -> Result<()>118     fn unlock(&self) -> Result<()> {
119         sys::unlock(self)
120     }
121 }
122 
123 /// Returns the error that a call to a try lock method on a contended file will
124 /// return.
lock_contended_error() -> Error125 pub fn lock_contended_error() -> Error {
126     sys::lock_error()
127 }
128 
129 /// `FsStats` contains some common stats about a file system.
130 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
131 pub struct FsStats {
132     free_space: u64,
133     available_space: u64,
134     total_space: u64,
135     allocation_granularity: u64,
136 }
137 
138 impl FsStats {
139     /// Returns the number of free bytes in the file system containing the provided
140     /// path.
free_space(&self) -> u64141     pub fn free_space(&self) -> u64 {
142         self.free_space
143     }
144 
145     /// Returns the available space in bytes to non-priveleged users in the file
146     /// system containing the provided path.
available_space(&self) -> u64147     pub fn available_space(&self) -> u64 {
148         self.available_space
149     }
150 
151     /// Returns the total space in bytes in the file system containing the provided
152     /// path.
total_space(&self) -> u64153     pub fn total_space(&self) -> u64 {
154         self.total_space
155     }
156 
157     /// Returns the filesystem's disk space allocation granularity in bytes.
158     /// The provided path may be for any file in the filesystem.
159     ///
160     /// On Posix, this is equivalent to the filesystem's block size.
161     /// On Windows, this is equivalent to the filesystem's cluster size.
allocation_granularity(&self) -> u64162     pub fn allocation_granularity(&self) -> u64 {
163         self.allocation_granularity
164     }
165 }
166 
167 /// Get the stats of the file system containing the provided path.
statvfs<P>(path: P) -> Result<FsStats> where P: AsRef<Path>168 pub fn statvfs<P>(path: P) -> Result<FsStats> where P: AsRef<Path> {
169     sys::statvfs(path.as_ref())
170 }
171 
172 /// Returns the number of free bytes in the file system containing the provided
173 /// path.
free_space<P>(path: P) -> Result<u64> where P: AsRef<Path>174 pub fn free_space<P>(path: P) -> Result<u64> where P: AsRef<Path> {
175     statvfs(path).map(|stat| stat.free_space)
176 }
177 
178 /// Returns the available space in bytes to non-priveleged users in the file
179 /// system containing the provided path.
available_space<P>(path: P) -> Result<u64> where P: AsRef<Path>180 pub fn available_space<P>(path: P) -> Result<u64> where P: AsRef<Path> {
181     statvfs(path).map(|stat| stat.available_space)
182 }
183 
184 /// Returns the total space in bytes in the file system containing the provided
185 /// path.
total_space<P>(path: P) -> Result<u64> where P: AsRef<Path>186 pub fn total_space<P>(path: P) -> Result<u64> where P: AsRef<Path> {
187     statvfs(path).map(|stat| stat.total_space)
188 }
189 
190 /// Returns the filesystem's disk space allocation granularity in bytes.
191 /// The provided path may be for any file in the filesystem.
192 ///
193 /// On Posix, this is equivalent to the filesystem's block size.
194 /// On Windows, this is equivalent to the filesystem's cluster size.
allocation_granularity<P>(path: P) -> Result<u64> where P: AsRef<Path>195 pub fn allocation_granularity<P>(path: P) -> Result<u64> where P: AsRef<Path> {
196     statvfs(path).map(|stat| stat.allocation_granularity)
197 }
198 
199 #[cfg(test)]
200 mod test {
201 
202     extern crate tempdir;
203     extern crate test;
204 
205     use std::fs;
206     use super::*;
207     use std::io::{Read, Seek, SeekFrom, Write};
208 
209     /// Tests file duplication.
210     #[test]
duplicate()211     fn duplicate() {
212         let tempdir = tempdir::TempDir::new("fs2").unwrap();
213         let path = tempdir.path().join("fs2");
214         let mut file1 =
215             fs::OpenOptions::new().read(true).write(true).create(true).open(&path).unwrap();
216         let mut file2 = file1.duplicate().unwrap();
217 
218         // Write into the first file and then drop it.
219         file1.write_all(b"foo").unwrap();
220         drop(file1);
221 
222         let mut buf = vec![];
223 
224         // Read from the second file; since the position is shared it will already be at EOF.
225         file2.read_to_end(&mut buf).unwrap();
226         assert_eq!(0, buf.len());
227 
228         // Rewind and read.
229         file2.seek(SeekFrom::Start(0)).unwrap();
230         file2.read_to_end(&mut buf).unwrap();
231         assert_eq!(&buf, &b"foo");
232     }
233 
234     /// Tests shared file lock operations.
235     #[test]
lock_shared()236     fn lock_shared() {
237         let tempdir = tempdir::TempDir::new("fs2").unwrap();
238         let path = tempdir.path().join("fs2");
239         let file1 = fs::OpenOptions::new().read(true).write(true).create(true).open(&path).unwrap();
240         let file2 = fs::OpenOptions::new().read(true).write(true).create(true).open(&path).unwrap();
241         let file3 = fs::OpenOptions::new().read(true).write(true).create(true).open(&path).unwrap();
242 
243         // Concurrent shared access is OK, but not shared and exclusive.
244         file1.lock_shared().unwrap();
245         file2.lock_shared().unwrap();
246         assert_eq!(file3.try_lock_exclusive().unwrap_err().kind(),
247                    lock_contended_error().kind());
248         file1.unlock().unwrap();
249         assert_eq!(file3.try_lock_exclusive().unwrap_err().kind(),
250                    lock_contended_error().kind());
251 
252         // Once all shared file locks are dropped, an exclusive lock may be created;
253         file2.unlock().unwrap();
254         file3.lock_exclusive().unwrap();
255     }
256 
257     /// Tests exclusive file lock operations.
258     #[test]
lock_exclusive()259     fn lock_exclusive() {
260         let tempdir = tempdir::TempDir::new("fs2").unwrap();
261         let path = tempdir.path().join("fs2");
262         let file1 = fs::OpenOptions::new().read(true).write(true).create(true).open(&path).unwrap();
263         let file2 = fs::OpenOptions::new().read(true).write(true).create(true).open(&path).unwrap();
264 
265         // No other access is possible once an exclusive lock is created.
266         file1.lock_exclusive().unwrap();
267         assert_eq!(file2.try_lock_exclusive().unwrap_err().kind(),
268                    lock_contended_error().kind());
269         assert_eq!(file2.try_lock_shared().unwrap_err().kind(),
270                    lock_contended_error().kind());
271 
272         // Once the exclusive lock is dropped, the second file is able to create a lock.
273         file1.unlock().unwrap();
274         file2.lock_exclusive().unwrap();
275     }
276 
277     /// Tests that a lock is released after the file that owns it is dropped.
278     #[test]
lock_cleanup()279     fn lock_cleanup() {
280         let tempdir = tempdir::TempDir::new("fs2").unwrap();
281         let path = tempdir.path().join("fs2");
282         let file1 = fs::OpenOptions::new().read(true).write(true).create(true).open(&path).unwrap();
283         let file2 = fs::OpenOptions::new().read(true).write(true).create(true).open(&path).unwrap();
284 
285         file1.lock_exclusive().unwrap();
286         assert_eq!(file2.try_lock_shared().unwrap_err().kind(),
287                    lock_contended_error().kind());
288 
289         // Drop file1; the lock should be released.
290         drop(file1);
291         file2.lock_shared().unwrap();
292     }
293 
294     /// Tests file allocation.
295     #[test]
allocate()296     fn allocate() {
297         let tempdir = tempdir::TempDir::new("fs2").unwrap();
298         let path = tempdir.path().join("fs2");
299         let file = fs::OpenOptions::new().write(true).create(true).open(&path).unwrap();
300         let blksize = allocation_granularity(&path).unwrap();
301 
302         // New files are created with no allocated size.
303         assert_eq!(0, file.allocated_size().unwrap());
304         assert_eq!(0, file.metadata().unwrap().len());
305 
306         // Allocate space for the file, checking that the allocated size steps
307         // up by block size, and the file length matches the allocated size.
308 
309         file.allocate(2 * blksize - 1).unwrap();
310         assert_eq!(2 * blksize, file.allocated_size().unwrap());
311         assert_eq!(2 * blksize - 1, file.metadata().unwrap().len());
312 
313         // Truncate the file, checking that the allocated size steps down by
314         // block size.
315 
316         file.set_len(blksize + 1).unwrap();
317         assert_eq!(2 * blksize, file.allocated_size().unwrap());
318         assert_eq!(blksize + 1, file.metadata().unwrap().len());
319     }
320 
321     /// Checks filesystem space methods.
322     #[test]
filesystem_space()323     fn filesystem_space() {
324         let tempdir = tempdir::TempDir::new("fs2").unwrap();
325         let total_space = total_space(&tempdir.path()).unwrap();
326         let free_space = free_space(&tempdir.path()).unwrap();
327         let available_space = available_space(&tempdir.path()).unwrap();
328 
329         assert!(total_space > free_space);
330         assert!(total_space > available_space);
331         assert!(available_space <= free_space);
332     }
333 
334     /// Benchmarks creating and removing a file. This is a baseline benchmark
335     /// for comparing against the truncate and allocate benchmarks.
336     #[bench]
bench_file_create(b: &mut test::Bencher)337     fn bench_file_create(b: &mut test::Bencher) {
338         let tempdir = tempdir::TempDir::new("fs2").unwrap();
339         let path = tempdir.path().join("file");
340 
341         b.iter(|| {
342             fs::OpenOptions::new()
343                             .read(true)
344                             .write(true)
345                             .create(true)
346                             .open(&path)
347                             .unwrap();
348             fs::remove_file(&path).unwrap();
349         });
350     }
351 
352     /// Benchmarks creating a file, truncating it to 32MiB, and deleting it.
353     #[bench]
bench_file_truncate(b: &mut test::Bencher)354     fn bench_file_truncate(b: &mut test::Bencher) {
355         let size = 32 * 1024 * 1024;
356         let tempdir = tempdir::TempDir::new("fs2").unwrap();
357         let path = tempdir.path().join("file");
358 
359         b.iter(|| {
360             let file = fs::OpenOptions::new()
361                                        .read(true)
362                                        .write(true)
363                                        .create(true)
364                                        .open(&path)
365                                        .unwrap();
366             file.set_len(size).unwrap();
367             fs::remove_file(&path).unwrap();
368         });
369     }
370 
371     /// Benchmarks creating a file, allocating 32MiB for it, and deleting it.
372     #[bench]
bench_file_allocate(b: &mut test::Bencher)373     fn bench_file_allocate(b: &mut test::Bencher) {
374         let size = 32 * 1024 * 1024;
375         let tempdir = tempdir::TempDir::new("fs2").unwrap();
376         let path = tempdir.path().join("file");
377 
378         b.iter(|| {
379             let file = fs::OpenOptions::new()
380                                        .read(true)
381                                        .write(true)
382                                        .create(true)
383                                        .open(&path)
384                                        .unwrap();
385             file.allocate(size).unwrap();
386             fs::remove_file(&path).unwrap();
387         });
388     }
389 
390     /// Benchmarks creating a file, allocating 32MiB for it, and deleting it.
391     #[bench]
bench_allocated_size(b: &mut test::Bencher)392     fn bench_allocated_size(b: &mut test::Bencher) {
393         let size = 32 * 1024 * 1024;
394         let tempdir = tempdir::TempDir::new("fs2").unwrap();
395         let path = tempdir.path().join("file");
396         let file = fs::OpenOptions::new()
397                                    .read(true)
398                                    .write(true)
399                                    .create(true)
400                                    .open(&path)
401                                    .unwrap();
402         file.allocate(size).unwrap();
403 
404         b.iter(|| {
405             file.allocated_size().unwrap();
406         });
407     }
408 
409     /// Benchmarks duplicating a file descriptor or handle.
410     #[bench]
bench_duplicate(b: &mut test::Bencher)411     fn bench_duplicate(b: &mut test::Bencher) {
412         let tempdir = tempdir::TempDir::new("fs2").unwrap();
413         let path = tempdir.path().join("fs2");
414         let file = fs::OpenOptions::new().read(true).write(true).create(true).open(&path).unwrap();
415 
416         b.iter(|| test::black_box(file.duplicate().unwrap()));
417     }
418 
419     /// Benchmarks locking and unlocking a file lock.
420     #[bench]
bench_lock_unlock(b: &mut test::Bencher)421     fn bench_lock_unlock(b: &mut test::Bencher) {
422         let tempdir = tempdir::TempDir::new("fs2").unwrap();
423         let path = tempdir.path().join("fs2");
424         let file = fs::OpenOptions::new().read(true).write(true).create(true).open(&path).unwrap();
425 
426         b.iter(|| {
427             file.lock_exclusive().unwrap();
428             file.unlock().unwrap();
429         });
430     }
431 
432     /// Benchmarks the free space method.
433     #[bench]
bench_free_space(b: &mut test::Bencher)434     fn bench_free_space(b: &mut test::Bencher) {
435         let tempdir = tempdir::TempDir::new("fs2").unwrap();
436         b.iter(|| {
437             test::black_box(free_space(&tempdir.path()).unwrap());
438         });
439     }
440 
441     /// Benchmarks the available space method.
442     #[bench]
bench_available_space(b: &mut test::Bencher)443     fn bench_available_space(b: &mut test::Bencher) {
444         let tempdir = tempdir::TempDir::new("fs2").unwrap();
445         b.iter(|| {
446             test::black_box(available_space(&tempdir.path()).unwrap());
447         });
448     }
449 
450     /// Benchmarks the total space method.
451     #[bench]
bench_total_space(b: &mut test::Bencher)452     fn bench_total_space(b: &mut test::Bencher) {
453         let tempdir = tempdir::TempDir::new("fs2").unwrap();
454         b.iter(|| {
455             test::black_box(total_space(&tempdir.path()).unwrap());
456         });
457     }
458 }
459