create_output_dir/
lib.rs

1//! This crate provides only one function, `create_output_dir` which creates an excluded from cache
2//! directory atomically with its parents as needed.
3//!
4//! The source code of this crate has been almost verbatim copy-pasted from
5//! [`cargo_util::paths::create_dir_all_excluded_from_backups_atomic`][cargo-util-fn].
6//!
7//! [cargo-util-fn]: https://docs.rs/cargo-util/latest/cargo_util/paths/fn.create_dir_all_excluded_from_backups_atomic.html
8
9use std::ffi::OsStr;
10use std::path::Path;
11use std::{env, fs};
12
13use anyhow::{Context, Result};
14
15/// Creates an excluded from cache directory atomically with its parents as needed.
16///
17/// The atomicity only covers creating the leaf directory and exclusion from cache. Any missing
18/// parent directories will not be created in an atomic manner.
19///
20/// This function is idempotent and in addition to that it won't exclude `path` from cache if it
21/// already exists.
22pub fn create_output_dir(path: &Path) -> Result<()> {
23    if path.is_dir() {
24        return Ok(());
25    }
26
27    let parent = path.parent().unwrap();
28    let base = path.file_name().unwrap();
29    fs::create_dir_all(parent)
30        .with_context(|| format!("failed to create directory `{}`", parent.display()))?;
31
32    // We do this in two steps: first create a temporary directory and exclude it from backups,
33    // then rename it to the desired name.
34    // If we created the directory directly where it should be and then excluded it from backups
35    // we would risk a situation where the application is interrupted right after the directory
36    // creation, but before the exclusion, and the directory would remain non-excluded from backups.
37    //
38    // We need a temporary directory created in `parent` instead of `$TMP`, because only then we
39    // can be easily sure that `fs::rename()` will succeed (the new name needs to be on the same
40    // mount point as the old one).
41    let tempdir = tempfile::Builder::new().prefix(base).tempdir_in(parent)?;
42    exclude_from_backups(tempdir.path());
43    exclude_from_content_indexing(tempdir.path());
44
45    // Previously `fs::create_dir_all()` was used here to create the directory directly and
46    // `fs::create_dir_all()` explicitly treats the directory being created concurrently by another
47    // thread or process as success, hence the check below to follow the existing behavior.
48    // If we get an error at `fs::rename()` and suddenly the directory (which didn't exist a moment
49    // earlier) exists we can infer from it that another application process is doing work here.
50    if let Err(e) = fs::rename(tempdir.path(), path) {
51        if !path.exists() {
52            return Err(e.into());
53        }
54    }
55
56    Ok(())
57}
58
59/// Marks the directory as excluded from archives/backups.
60///
61/// This is recommended to prevent derived/temporary files from bloating backups.
62/// There are two mechanisms used to achieve this right now:
63/// * A dedicated resource property excluding from Time Machine backups on macOS.
64/// * `CACHEDIR.TAG` files supported by various tools in a platform-independent way.
65fn exclude_from_backups(path: &Path) {
66    exclude_from_time_machine(path);
67    let _ = fs::write(
68        path.join("CACHEDIR.TAG"),
69        format!(
70            "Signature: 8a477f597d28d172789f06886806bc55
71# This file is a cache directory tag{}.
72# For information about cache directory tags see https://bford.info/cachedir/
73",
74            match guess_application_name() {
75                None => String::new(),
76                Some(name) => format!(" created by {}", name),
77            }
78        ),
79    );
80    // Similarly to exclude_from_time_machine() we ignore errors here as it's an optional feature.
81}
82
83/// Marks the directory as excluded from content indexing.
84///
85/// This is recommended to prevent the content of derived/temporary files from being indexed.
86/// This is very important for Windows users, as the live content indexing may significantly slow
87/// I/O operations or compilers etc.
88///
89/// This is currently a no-op on non-Windows platforms.
90fn exclude_from_content_indexing(path: &Path) {
91    #[cfg(windows)]
92    {
93        use std::iter::once;
94        use std::os::windows::prelude::OsStrExt;
95        use winapi::um::fileapi::{GetFileAttributesW, SetFileAttributesW};
96        use winapi::um::winnt::FILE_ATTRIBUTE_NOT_CONTENT_INDEXED;
97
98        let path: Vec<u16> = path.as_os_str().encode_wide().chain(once(0)).collect();
99        unsafe {
100            SetFileAttributesW(
101                path.as_ptr(),
102                GetFileAttributesW(path.as_ptr()) | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
103            );
104        }
105    }
106    #[cfg(not(windows))]
107    {
108        let _ = path;
109    }
110}
111
112#[cfg(not(target_os = "macos"))]
113fn exclude_from_time_machine(_: &Path) {}
114
115/// Marks files or directories as excluded from Time Machine on macOS.
116#[cfg(target_os = "macos")]
117fn exclude_from_time_machine(path: &Path) {
118    use core_foundation::base::TCFType;
119    use core_foundation::{number, string, url};
120    use std::ptr;
121
122    // For compatibility with 10.7 a string is used instead of global kCFURLIsExcludedFromBackupKey
123    let is_excluded_key: std::result::Result<string::CFString, _> =
124        "NSURLIsExcludedFromBackupKey".parse();
125    let path = url::CFURL::from_path(path, false);
126    if let (Some(path), Ok(is_excluded_key)) = (path, is_excluded_key) {
127        unsafe {
128            url::CFURLSetResourcePropertyForKey(
129                path.as_concrete_TypeRef(),
130                is_excluded_key.as_concrete_TypeRef(),
131                number::kCFBooleanTrue as *const _,
132                ptr::null_mut(),
133            );
134        }
135    }
136    // Errors are ignored, since it's an optional feature and failure
137    // shouldn't prevent applications from working.
138}
139
140fn guess_application_name() -> Option<String> {
141    let exe = env::current_exe().ok()?;
142    let file_name = if exe.extension() == Some(OsStr::new(env::consts::EXE_EXTENSION)) {
143        exe.file_stem()
144    } else {
145        exe.file_name()
146    }?;
147    Some(file_name.to_string_lossy().into_owned())
148}