cachedir/
lib.rs

1//! A Rust library to help interacting with cache directories and `CACHEDIR.TAG` files as defined
2//! in [Cache Directory Tagging Specification](https://bford.info/cachedir/).
3//!
4//! The abstract of the spefication should be more than enough to illustrate what we're doing here:
5//!
6//! > Many applications create and manage directories containing cached information about content
7//! > stored elsewhere, such as cached Web content or thumbnail-size versions of images or movies.
8//! > For speed and storage efficiency we would often like to avoid backing up, archiving, or
9//! > otherwise unnecessarily copying such directories around, but it is a pain to identify and
10//! > individually exclude each such directory during data transfer operations. I propose an
11//! > extremely simple convention by which applications can reliably "tag" any cache directories
12//! > they create, for easy identification by backup systems and other data management utilities.
13//! > Data management utilities can then heed or ignore these tags as the user sees fit.
14use std::io::prelude::*;
15use std::{env, fs, io, path};
16
17/// The `CACHEDIR.TAG` file header as defined by the specification.
18pub const HEADER: &[u8; 43] = b"Signature: 8a477f597d28d172789f06886806bc55";
19
20/// Returns `true` if the tag is present at `directory`, `false` otherwise.
21///
22/// This is basically a shortcut for
23///
24/// ```ignore
25/// get_tag_state(directory).map(|state| match state {
26///     TagState::Present => true,
27///     _ => false,
28/// })
29/// ```
30///
31/// See [get_tag_state](fn.get_tag_state.html) for error conditions documentation.
32pub fn is_tagged<P: AsRef<path::Path>>(directory: P) -> io::Result<bool> {
33    get_tag_state(directory).map(|state| matches!(state, TagState::Present))
34}
35
36/// Gets the state of the tag in the specified directory.
37///
38/// Will return an error if:
39///
40/// * The directory can't be accessed for any reason (it doesn't exist, permission error etc.)
41/// * The `CACHEDIR.TAG` in the directory exists but can't be accessed or read from
42pub fn get_tag_state<P: AsRef<path::Path>>(directory: P) -> io::Result<TagState> {
43    let directory = directory.as_ref();
44    match fs::File::open(directory.join("CACHEDIR.TAG")) {
45        Ok(mut cachedir_tag) => {
46            let mut buffer = vec![0; HEADER.len()];
47            let read = cachedir_tag.read(&mut buffer)?;
48            let header_ok = read == HEADER.len() && buffer == HEADER[..];
49            Ok(if header_ok {
50                TagState::Present
51            } else {
52                TagState::WrongHeader
53            })
54        }
55        Err(e) => match e.kind() {
56            io::ErrorKind::NotFound => {
57                if directory.is_dir() {
58                    Ok(TagState::Absent)
59                } else {
60                    Err(e)
61                }
62            }
63            _ => Err(e),
64        },
65    }
66}
67
68/// The state of a `CACHEDIR.TAG` file.
69pub enum TagState {
70    /// The file doesn't exist.
71    Absent,
72    /// The file exists, but doesn't contain the header required by the
73    /// specification.
74    WrongHeader,
75    /// The file exists and contains the correct header.
76    Present,
77}
78
79/// Adds a tag to the specified `directory`.
80///
81/// Will return an error if:
82///
83/// * The `directory` exists and contains a `CACHEDIR.TAG` file, regardless of its content.
84/// * The file can't be created for any reason (the `directory` doesn't exist, permission error,
85///   can't write to the file etc.)
86pub fn add_tag<P: AsRef<path::Path>>(directory: P) -> io::Result<()> {
87    let directory = directory.as_ref();
88    match fs::OpenOptions::new()
89        .write(true)
90        .create_new(true)
91        .open(directory.join("CACHEDIR.TAG"))
92    {
93        Ok(mut cachedir_tag) => cachedir_tag.write_all(HEADER),
94        Err(e) => Err(e),
95    }
96}
97
98/// Ensures the tag exists in `directory`.
99///
100/// This function considers the `CACHEDIR.TAG` file in `directory` existing, regardless of its
101/// content, as a success.
102///
103/// Will return an error if The tag file doesn't exist and can't be created for any reason
104/// (the `directory` doesn't exist, permission error, can't write to the file etc.).
105pub fn ensure_tag<P: AsRef<path::Path>>(directory: P) -> io::Result<()> {
106    match add_tag(directory) {
107        Err(e) => match e.kind() {
108            io::ErrorKind::AlreadyExists => Ok(()),
109            _ => Err(e),
110        },
111        other => other,
112    }
113}
114
115/// Tries to create `directory` with a `CACHEDIR.TAG` file atomically and returns `true` if it
116/// created it or `false` if the directory already exists, regardless of if the `CACHEDIR.TAG`
117/// file exists in it or if it has the correct header.
118///
119/// This function first creates a temporary directory in the same directory where `directory` is
120/// supposed to exist. The temporary directory has a semi-random name based on the `directory` base
121/// name. Then the `CACHEDIR.TAG` file is created in the temporary directory and the temporary
122/// directory is attempted to be renamed to `directory`. This (as opposed to creating the directory
123/// with the final name and creating `CACHEDIR.TAG` file in it) is a way to ensure that the
124/// `directory` is always created with the `CACHEDIR.TAG` file. If we simply created the directory
125/// with the final name the program could be interrupted before `CACHEDIR.TAG` creation and the
126/// `directory` would remain not excluded from backups as this function does not attempt to verify
127/// or change the `CACHEDIR.TAG` file in `directory` if it already exists.
128pub fn mkdir_atomic<P: AsRef<path::Path>>(directory: P) -> io::Result<bool> {
129    let mut directory = directory.as_ref().to_path_buf();
130    if directory.exists() {
131        return Ok(false);
132    }
133
134    if directory.is_relative() {
135        directory = env::current_dir()?.join(directory);
136    }
137
138    let tempdir = tempfile::Builder::new()
139        .prefix(directory.file_name().unwrap())
140        .tempdir_in(directory.parent().unwrap())?;
141    add_tag(tempdir.path())?;
142    match fs::rename(tempdir.path(), &directory) {
143        Ok(()) => Ok(true),
144        Err(e) => {
145            if directory.is_dir() {
146                Ok(false)
147            } else {
148                Err(e)
149            }
150        }
151    }
152}
153
154#[test]
155fn is_tagged_on_nonexistent_directory_is_an_error() {
156    let directory = path::Path::new("this directory does not exist");
157    assert!(!directory.exists());
158    assert!(is_tagged(directory).is_err());
159}
160
161#[test]
162fn empty_directory_is_not_tagged() {
163    assert!(!is_tagged(tempfile::tempdir().unwrap()).unwrap());
164}
165
166#[test]
167fn directory_with_a_tag_with_wrong_content_is_not_tagged() {
168    let directory = tempfile::tempdir().unwrap();
169    let cachedir_tag = directory.path().join("CACHEDIR.TAG");
170
171    fs::write(&cachedir_tag, "").unwrap();
172    assert!(!is_tagged(&directory).unwrap());
173
174    fs::write(&cachedir_tag, &HEADER[..(HEADER.len() - 2)]).unwrap();
175    assert!(!is_tagged(&directory).unwrap());
176}
177
178#[test]
179fn add_tag_is_detected_by_is_tagged() {
180    let directory = tempfile::tempdir().unwrap();
181    add_tag(directory.path()).unwrap();
182    assert!(is_tagged(directory.path()).unwrap());
183}
184
185#[test]
186fn add_tag_errors_when_called_with_nonexistent_directory() {
187    let directory = path::Path::new("this directory does not exist");
188    assert!(!directory.exists());
189    assert!(add_tag(directory).is_err());
190}
191
192#[test]
193fn add_tag_errors_when_tag_already_exists() {
194    let directory = tempfile::tempdir().unwrap();
195    assert!(add_tag(directory.path()).is_ok());
196    assert!(add_tag(directory.path()).is_err());
197}
198
199#[test]
200fn ensure_tag_is_detected_by_is_tagged() {
201    let directory = tempfile::tempdir().unwrap();
202    ensure_tag(directory.path()).unwrap();
203    assert!(is_tagged(directory.path()).unwrap());
204}
205
206#[test]
207fn ensure_tag_errors_when_called_with_nonexistent_directory() {
208    let directory = path::Path::new("this directory does not exist");
209    assert!(!directory.exists());
210    assert!(ensure_tag(directory).is_err());
211    assert!(is_tagged(directory).is_err());
212}
213
214#[test]
215fn ensure_tag_is_idempotent() {
216    let directory = tempfile::tempdir().unwrap();
217    assert!(ensure_tag(directory.path()).is_ok());
218    assert!(is_tagged(directory.path()).unwrap());
219    assert!(ensure_tag(directory.path()).is_ok());
220    assert!(is_tagged(directory.path()).unwrap());
221}
222
223#[test]
224fn mkdir_atomic_works() {
225    use std::thread;
226    let directory = tempfile::tempdir().unwrap();
227    let cache = directory.path().join("cache");
228    let threads = (0..10).map(|_| {
229        let cache = cache.clone();
230        thread::spawn(move || mkdir_atomic(cache))
231    });
232    let results = threads.map(|t| t.join().unwrap().unwrap());
233    let creations: usize = results.map(|created| if created { 1 } else { 0 }).sum();
234    // One and only one actually creates the desired directory...
235    assert_eq!(creations, 1);
236    // ...which is tagged correctly.
237    assert!(is_tagged(cache).unwrap());
238
239    // The mkdir_atomic() calls which didn't actually create the final directory shouldn't leave
240    // behind any garbage.
241    assert_eq!(
242        fs::read_dir(directory.path())
243            .unwrap()
244            .map(|entry| entry.unwrap().file_name())
245            .collect::<Vec<_>>(),
246        ["cache"],
247    );
248}