use crate::child_environment::environment_value;
use crate::diagnostic_log::debug;
use crate::private_storage::{create_private_directory, create_private_directory_all};
#[cfg(unix)]
use crate::private_storage::{PRIVATE_DIRECTORY_MODE, protect_path};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
const TAG: &str = "cli-git";
pub const REPOSITORY_SCRATCH_NAME: &str = "cli-git-scratch";
pub const SCRATCH_STALE_AFTER: Duration = Duration::from_secs(24 * 60 * 60);
const REFRESHES_PER_STALE_AGE: u64 = 24;
pub const SCRATCH_REFRESH_EVERY: Duration = Duration::from_secs(SCRATCH_STALE_AFTER.as_secs() / REFRESHES_PER_STALE_AGE);
pub fn renew(path: &Path) {
let renewed: std::io::Result<()> = std::fs::File::open(path).and_then(|opened| { return opened.set_modified(SystemTime::now()); });
if let Err(error) = renewed {
debug(TAG, format!("could not renew {}: {error}", path.display()).as_str());
}
}
pub fn state_directory(environment: &[(OsString, OsString)]) -> Option<PathBuf> {
if let Some(state_home) = environment_value(environment, "XDG_STATE_HOME") {
let state_path: PathBuf = PathBuf::from(state_home);
if state_path.is_absolute() {
return Some(state_path.join("cli-git"));
}
}
let home: OsString = environment_value(environment, "HOME")?;
if home.is_empty() {
return None;
}
return Some(PathBuf::from(home).join(".local").join("state").join("cli-git"));
}
pub fn user_scratch_path(environment: &[(OsString, OsString)]) -> Result<PathBuf, String> {
let Some(state) = state_directory(environment) else {
return Err(String::from(
"cli-git keeps its temporary files in a private directory under XDG_STATE_HOME (or HOME/.local/state) and \
never in the system temporary directory, and this command's environment has neither an absolute \
XDG_STATE_HOME nor a non-empty HOME; set one of them",
));
};
return Ok(state.join("scratch"));
}
pub fn prepare_user_scratch(environment: &[(OsString, OsString)]) -> Result<PathBuf, String> {
let given: PathBuf = user_scratch_path(environment)?;
let directory: PathBuf = std::path::absolute(&given).unwrap_or_else(|_| { return given.clone(); });
if let Err(error) = create_private_directory_all(directory.as_path()) {
return Err(format!(
"cli-git could not create its private scratch directory {}: {error}",
directory.display()
));
}
if let Err(error) = assert_private_directory(directory.as_path()) {
return Err(format!(
"cli-git will not use {} for temporary files: {error}",
directory.display()
));
}
sweep_stale(directory.as_path(), SystemTime::now(), SCRATCH_STALE_AFTER);
return Ok(directory);
}
pub fn scratch_file(environment: &[(OsString, OsString)]) -> std::io::Result<std::fs::File> {
let directory: PathBuf = prepare_user_scratch(environment).map_err(std::io::Error::other)?;
return tempfile::tempfile_in(directory);
}
pub fn prepare_repository_scratch(common_dir: &Path) -> std::io::Result<PathBuf> {
let directory: PathBuf = common_dir.join(REPOSITORY_SCRATCH_NAME);
match create_private_directory(directory.as_path()) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(error) => return Err(error),
}
assert_private_directory(directory.as_path())?;
sweep_stale(directory.as_path(), SystemTime::now(), SCRATCH_STALE_AFTER);
return Ok(directory);
}
#[cfg(unix)]
fn assert_private_directory(path: &Path) -> std::io::Result<()> {
use std::os::unix::fs::MetadataExt;
let metadata: std::fs::Metadata = std::fs::symlink_metadata(path)?;
if !metadata.is_dir() {
return Err(std::io::Error::other("it is a link or a file, not a directory"));
}
let owner: u32 = unsafe { libc::geteuid() };
if metadata.uid() != owner {
return Err(std::io::Error::other("it belongs to another account"));
}
if metadata.mode() & 0o077 != 0 {
protect_path(path, PRIVATE_DIRECTORY_MODE)?;
}
return Ok(());
}
#[cfg(not(unix))]
fn assert_private_directory(path: &Path) -> std::io::Result<()> {
if !std::fs::symlink_metadata(path)?.is_dir() {
return Err(std::io::Error::other("it is a link or a file, not a directory"));
}
return Ok(());
}
pub fn sweep_stale(directory: &Path, now: SystemTime, max_age: Duration) -> usize {
let entries: std::fs::ReadDir = match std::fs::read_dir(directory) {
Ok(found) => found,
Err(error) => {
debug(TAG, format!("could not list {} for stale entries: {error}", directory.display()).as_str());
return 0;
}
};
let mut removed: usize = 0;
for listed in entries {
let entry: std::fs::DirEntry = match listed {
Ok(found) => found,
Err(error) => {
debug(TAG, format!("could not read an entry of {}: {error}", directory.display()).as_str());
continue;
}
};
let path: PathBuf = entry.path();
let metadata: std::fs::Metadata = match std::fs::symlink_metadata(&path) {
Ok(found) => found,
Err(error) => {
debug(TAG, format!("could not inspect {}: {error}", path.display()).as_str());
continue;
}
};
let age: Option<Duration> = metadata
.modified()
.ok()
.and_then(|modified| { return now.duration_since(modified).ok(); });
if !age.is_some_and(|found| { return found > max_age; }) {
continue;
}
let outcome: std::io::Result<()> = if metadata.is_dir() {
std::fs::remove_dir_all(&path)
} else {
std::fs::remove_file(&path)
};
match outcome {
Ok(()) => removed += 1,
Err(error) => debug(TAG, format!("could not remove the stale {}: {error}", path.display()).as_str()),
}
}
return removed;
}
#[cfg(test)]
#[path = "scratch_location_tests.rs"]
mod tests;