use std::fs;
use std::path::Path;
use std::time::Instant;
use anyhow::{Context, Result};
use crate::checksum;
use crate::ndjson_types::PlatformInfo;
use crate::platform;
use super::inplace::{create_backup_in, delete_backup_quietly, write_inplace_path};
use super::rename_path::write_rename_path;
use super::syntax_heuristic::syntax_heuristic_check;
use super::types::{AtomicWriteOptions, WriteResult, WriteStrategy};
pub fn atomic_write(
target: &Path,
content: &[u8],
opts: &AtomicWriteOptions,
workspace: &Path,
) -> Result<WriteResult> {
atomic_write_based(target, content, opts, workspace, None)
}
#[tracing::instrument(skip_all, fields(path = %target.display()))]
pub fn atomic_write_based(
target: &Path,
content: &[u8],
opts: &AtomicWriteOptions,
workspace: &Path,
expected_base: Option<&str>,
) -> Result<WriteResult> {
let mut backup_made: Option<std::path::PathBuf> = None;
match atomic_write_based_inner(
target,
content,
opts,
workspace,
expected_base,
&mut backup_made,
) {
Ok(result) => Ok(result),
Err(error) => match backup_made {
Some(backup_path) => Err(error.context(WriteFailure { backup_path })),
None => Err(error),
},
}
}
#[derive(Debug)]
pub struct WriteFailure {
pub backup_path: std::path::PathBuf,
}
impl std::fmt::Display for WriteFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"a copy of the file stayed on disk at {}",
self.backup_path.display()
)
}
}
fn atomic_write_based_inner(
target: &Path,
content: &[u8],
opts: &AtomicWriteOptions,
workspace: &Path,
expected_base: Option<&str>,
backup_out: &mut Option<std::path::PathBuf>,
) -> Result<WriteResult> {
let start = Instant::now();
let target = crate::path_safety::validate_path(target, workspace)?;
let resolved_durability = opts.durability.resolve(&target);
let mut backup_method_used: Option<&'static str> = None;
let mut backup_skipped: Option<String> = None;
ensure_parent_dir(&target)?;
let lock =
crate::lock::acquire_exclusive(&target, crate::config::tuning().lock_acquire_timeout_ms)?;
if let Some(expected) = expected_base {
let actual = if target.exists() {
Some(checksum::hash_file(&target, u64::MAX)?)
} else {
None
};
if actual.as_deref() != Some(expected) {
return Err(crate::error::AtomwriteError::StateDrift {
path: target,
expected: expected.to_string(),
actual: actual
.unwrap_or_else(|| crate::constants::MISSING_FILE_CHECKSUM.to_string()),
}
.into());
}
}
let sidecar_wanted =
crate::wal::should_create_sidecar(&target, crate::wal::JournalOp::Write, opts.wal_policy)
&& !crate::wal::creation_throttled(opts.wal_policy);
let new_checksum = blake3::hash(content);
let (wal_op_id, mut wal_guard) = if sidecar_wanted {
match crate::wal::journal_started_with_guard(
&target,
crate::wal::JournalOp::Write,
None, new_checksum,
) {
Ok(pair) => pair,
Err(_) => (String::new(), crate::wal::JournalGuard::inert()),
}
} else {
tracing::debug!(
path = %target.display(),
policy = opts.wal_policy.as_str(),
"G119 L1: sidecar suppressed by wal-policy"
);
(String::new(), crate::wal::JournalGuard::inert())
};
let wal_op_id_opt: Option<String> = if wal_op_id.is_empty() {
None
} else {
Some(wal_op_id)
};
let (checksum_before, original_meta) = if target.exists() {
let meta =
fs::metadata(&target).with_context(|| format!("cannot stat {}", target.display()))?;
let hash = checksum::hash_file(&target, u64::MAX)?;
(Some(hash), Some(meta))
} else {
(None, None)
};
#[cfg(unix)]
let hardlink_nlink = if let Some(ref meta) = original_meta {
use std::os::unix::fs::MetadataExt;
let nlink = meta.nlink();
if nlink > 1 { Some(nlink) } else { None }
} else {
None
};
#[cfg(not(unix))]
let hardlink_nlink: Option<u64> = None;
let saved_xattrs = crate::xattr_restore::save_xattrs(&target).unwrap_or_else(|e| {
tracing::warn!(path = %target.display(), error = %e, "xattr save failed; continuing");
Vec::new()
});
let xattr_count = saved_xattrs.len() as u32;
let is_symlink = {
let sm = fs::symlink_metadata(&target);
sm.as_ref().map(fs::Metadata::is_symlink).unwrap_or(false)
};
let strategy = match opts.strategy {
Some(s) => s,
None => {
if hardlink_nlink.is_some_and(|n| n > 1) || is_symlink || !parent_is_writable(&target) {
WriteStrategy::InPlace
} else {
WriteStrategy::Rename
}
}
};
if opts.strict_atomic && matches!(strategy, WriteStrategy::InPlace) {
return Err(crate::error::AtomwriteError::ExdevFallbackDisabled {
path: target,
reason: if is_symlink {
"the target is a symlink, so the write must keep the existing inode"
} else {
"the target has more than one hardlink, so the write must keep the existing inode"
}
.into(),
}
.into());
}
if matches!(strategy, WriteStrategy::InPlace) {
if let Some(n) = hardlink_nlink {
tracing::info!(
path = %target.display(),
nlink = n,
"auto-switched to InPlace to preserve hardlink(s)"
);
} else if is_symlink {
tracing::info!(
path = %target.display(),
"auto-switched to InPlace because target is a symlink"
);
}
}
let (mtime, atime) = if let Some(ref meta) = original_meta {
(
filetime::FileTime::from_last_modification_time(meta),
filetime::FileTime::from_last_access_time(meta),
)
} else {
let now = filetime::FileTime::now();
(now, now)
};
let backup_path = if opts.backup && target.exists() {
match create_backup_in(&target, opts.retention, opts.backup_output_dir.as_deref()) {
Ok(bp) => {
backup_method_used = Some("reflink_or_copy");
*backup_out = Some(bp.clone());
Some(bp)
}
Err(e)
if crate::runtime::find_io_error(&e)
== Some(std::io::ErrorKind::PermissionDenied) =>
{
use std::io::Write as _;
crate::best_effort::best_effort(
writeln!(
std::io::stderr().lock(),
"atomwrite: backup skipped for {}: {}; writing without a backup",
target.display(),
e
),
"report skipped backup on stderr",
);
tracing::warn!(
path = %target.display(),
error = %e,
"backup skipped by directory permission; continuing without a copy"
);
backup_skipped = Some(format!(
"the directory refused the backup file: {e}; the write proceeds without a copy"
));
None
}
Err(e) => {
wal_guard.discard();
return Err(e);
}
}
} else {
None
};
let syntax_errors: u32 = 0;
if opts.syntax_check {
match crate::syntax_check::syntax_check(&target, content) {
Ok(crate::syntax_check::SyntaxCheckResult::Ok) => {}
Ok(crate::syntax_check::SyntaxCheckResult::Skipped { .. }) => {
if let Some(reason) = syntax_heuristic_check(content) {
tracing::warn!(
path = %target.display(),
reason = %reason,
"G72 syntax heuristic (no tree-sitter parser) failed"
);
return Err(crate::error::AtomwriteError::SyntaxError {
path: target.to_path_buf(),
count: 1,
line: None,
line_end: None,
}
.into());
}
}
Ok(crate::syntax_check::SyntaxCheckResult::Errors { count, first }) => {
tracing::warn!(
path = %target.display(),
count = count,
line = first.line,
column = first.column,
kind = %first.kind,
message = %first.message,
"G72 tree-sitter syntax check failed"
);
return Err(crate::error::AtomwriteError::SyntaxError {
path: target.to_path_buf(),
count: count as u32,
line: Some(first.line),
line_end: Some(first.line),
}
.into());
}
Err(e) => {
tracing::warn!(
path = %target.display(),
error = %e,
"G72 tree-sitter check errored; falling back to heuristic"
);
if let Some(_reason) = syntax_heuristic_check(content) {
return Err(crate::error::AtomwriteError::SyntaxError {
path: target.to_path_buf(),
count: 1,
line: None,
line_end: None,
}
.into());
}
}
}
}
if expected_base.is_none() && !lock.target_unchanged() {
wal_guard.discard();
return Err(crate::error::AtomwriteError::StateDrift {
path: target,
expected: "the inode the lock was taken on".to_string(),
actual: "another inode now answers to the same path".to_string(),
}
.into());
}
let (exdev_fallback, rename_method_used) = match strategy {
WriteStrategy::Rename => match write_rename_path(
target.as_path(),
content,
opts.strict_atomic,
resolved_durability,
) {
Ok(done) => done,
Err(e) => {
wal_guard.discard();
return Err(e);
}
},
WriteStrategy::InPlace | WriteStrategy::CopyBack => (
write_inplace_path(target.as_path(), content)?,
strategy.as_str(),
),
};
if !matches!(resolved_durability, crate::platform::Durability::Fast) {
if let Some(parent) = target.parent() {
if let Err(e) = platform::fsync_dir(parent) {
tracing::warn!(
path = %parent.display(),
error = %e,
"fsync_dir after persist failed"
);
}
}
}
if let Some(ref meta) = original_meta {
if let Err(e) = fs::set_permissions(&target, meta.permissions()) {
tracing::warn!(path = %target.display(), error = %e, "permission restore failed");
}
}
let xattr_preserved = crate::xattr_restore::restore_xattrs(&target, &saved_xattrs)
.unwrap_or_else(|e| {
tracing::warn!(path = %target.display(), error = %e, "xattr restore failed");
0
});
if opts.preserve_timestamps && original_meta.is_some() {
if let Err(e) = platform::preserve_timestamps(&target, mtime, atime) {
tracing::warn!(path = %target.display(), error = %e, "timestamp restore failed");
}
}
let checksum = checksum::hash_bytes(content);
if let Some(ref op_id) = wal_op_id_opt {
crate::best_effort::best_effort(
crate::wal::journal_committed(&target, op_id),
"append Committed journal entry",
);
wal_guard.release();
} else {
wal_guard.keep();
}
let backup_path = if let Some(ref bp) = backup_path {
if !opts.keep_backup {
delete_backup_quietly(bp);
None
} else {
Some(bp.clone())
}
} else {
None
};
Ok(WriteResult {
bytes_written: content.len() as u64,
checksum,
checksum_before,
backup_path: backup_path.map(|p| p.display().to_string()),
elapsed_ms: start.elapsed().as_millis() as u64,
platform: PlatformInfo {
fsync: platform::platform_fsync_name(),
dir_fsync: platform::platform_dir_fsync_name(),
durability: Some(resolved_durability.as_str()),
rename_method: Some(rename_method_used),
backup_method: backup_method_used,
},
hardlink_nlink,
write_strategy: strategy.as_str(),
xattr_preserved,
xattr_count,
exdev_fallback,
syntax_errors,
durability: resolved_durability.as_str(),
rename_method: rename_method_used,
backup_method: backup_method_used,
backup_skipped,
})
}
#[cfg(unix)]
fn parent_is_writable(target: &Path) -> bool {
use nix::unistd::{AccessFlags, access};
let parent = target.parent().unwrap_or_else(|| Path::new("."));
access(parent, AccessFlags::W_OK).is_ok()
}
#[cfg(not(unix))]
fn parent_is_writable(_target: &Path) -> bool {
true
}
fn ensure_parent_dir(target: &Path) -> Result<()> {
if let Some(parent) = target.parent() {
if !parent.exists() {
fs::create_dir_all(parent)
.with_context(|| format!("cannot create directories for {}", target.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
crate::best_effort::best_effort(
fs::set_permissions(
parent,
fs::Permissions::from_mode(crate::constants::DIR_PERMISSIONS),
),
"set mode on created parent directory",
);
}
}
}
Ok(())
}