persist_if_changed/
lib.rs

1//! A tiny utility library to avoid touching the filesystem if the content has not changed.
2//!
3//! This is useful to avoid triggering unnecessary rebuilds in systems that look at
4//! the modification time (`mtime`) as part of their file fingerprint (e.g. `cargo`).
5use fs_err::File;
6use sha2::Digest;
7use std::io::ErrorKind;
8use std::{
9    io::{Read, Write},
10    path::Path,
11};
12
13/// Only persist the content if it differs from the one already on disk.
14/// It if the file does not exist, it will be created.
15///
16/// This is useful to avoid unnecessary rebuilds, since `cargo` takes into account
17/// the modification time of the files when determining if they have changed or not.
18#[tracing::instrument(skip_all, level=tracing::Level::TRACE)]
19pub fn persist_if_changed(path: &Path, content: &[u8]) -> Result<(), anyhow::Error> {
20    let has_changed = has_changed_file2buffer(path, content).unwrap_or(true);
21    if !has_changed {
22        return Ok(());
23    }
24    let mut file = fs_err::OpenOptions::new()
25        .write(true)
26        .truncate(true)
27        .create(true)
28        .open(path)?;
29    file.write_all(content)?;
30    Ok(())
31}
32
33/// Only copy the file if its contents differ from the contents stored at
34/// the destination path.
35#[tracing::instrument(skip_all, level=tracing::Level::TRACE)]
36pub fn copy_if_changed(from: &Path, to: &Path) -> Result<(), anyhow::Error> {
37    let has_changed = has_changed_file2file(from, to).unwrap_or(true);
38    if !has_changed {
39        return Ok(());
40    }
41    fs_err::copy(from, to)?;
42    Ok(())
43}
44
45/// Returns `true` if the file contents are different, `false` otherwise.
46///
47/// It returns an error if we could not determine the outcome due to a
48/// failure in any of the intermediate operations.
49#[tracing::instrument(skip_all, level=tracing::Level::TRACE)]
50pub fn has_changed_file2file(from: &Path, to: &Path) -> Result<bool, anyhow::Error> {
51    let from_file = File::open(from);
52    let to_file = File::open(to);
53    let (from_file, to_file) = match (from_file, to_file) {
54        (Ok(from_file), Ok(to_file)) => (from_file, to_file),
55        (Ok(_), Err(e)) | (Err(e), Ok(_)) => {
56            if e.kind() == ErrorKind::NotFound {
57                return Ok(true);
58            }
59            return Err(e.into());
60        }
61        (Err(e1), Err(e2)) => {
62            if e1.kind() == ErrorKind::NotFound && e2.kind() == ErrorKind::NotFound {
63                return Ok(false);
64            }
65            return Err(e1.into());
66        }
67    };
68
69    // Cheaper check first: if the file size is not the same,
70    // we can skip computing the checksum.
71    let from_metadata = from_file.metadata()?;
72    let to_metadata = to_file.metadata()?;
73    if from_metadata.len() != to_metadata.len() {
74        return Ok(true);
75    }
76
77    let from_checksum = compute_file_checksum(from_file)?;
78    let to_checksum = compute_file_checksum(to_file)?;
79    Ok(from_checksum != to_checksum)
80}
81
82/// Returns `true` if the file contents are different, `false` otherwise.
83///
84/// It returns an error if we could not determine the outcome due to a
85/// failure in any of the intermediate operations.
86#[tracing::instrument(skip_all, level=tracing::Level::TRACE)]
87pub fn has_changed_file2buffer(path: &Path, contents: &[u8]) -> Result<bool, anyhow::Error> {
88    let file = match File::open(path) {
89        Ok(f) => f,
90        Err(e) => {
91            if e.kind() == ErrorKind::NotFound {
92                return Ok(true);
93            }
94            return Err(e.into());
95        }
96    };
97    // Cheaper check first: if the file size is not the same,
98    // we can skip computing the checksum.
99    let metadata = file.metadata()?;
100    if metadata.len() != contents.len() as u64 {
101        return Ok(true);
102    }
103    let file_checksum = compute_file_checksum(file)?;
104    let buffer_checksum = compute_buffer_checksum(contents);
105    Ok(file_checksum != buffer_checksum)
106}
107
108/// Compute the checksum of a file, if it exists.
109#[tracing::instrument(skip_all, level=tracing::Level::TRACE)]
110fn compute_file_checksum(file: fs_err::File) -> std::io::Result<String> {
111    let mut hasher = sha2::Sha256::new();
112
113    let mut reader = std::io::BufReader::new(file);
114    let mut buffer = [0; 8192]; // Buffer size (adjust as needed)
115
116    loop {
117        let bytes_read = reader.read(&mut buffer)?;
118        if bytes_read == 0 {
119            break;
120        }
121        hasher.update(&buffer[..bytes_read]);
122    }
123
124    let result = hasher.finalize();
125    Ok(format!("{:x}", result))
126}
127
128/// Compute the checksum of an in-memory bytes buffer.
129#[tracing::instrument(skip_all, level=tracing::Level::TRACE)]
130fn compute_buffer_checksum(buffer: &[u8]) -> String {
131    let mut hasher = sha2::Sha256::new();
132    hasher.update(buffer);
133    let result = hasher.finalize();
134    format!("{:x}", result)
135}