use std::io::Write;
use std::time::Instant;
use anyhow::{Context, Result};
use crate::checksum;
use crate::cli::{GlobalArgs, MoveArgs};
use crate::error::AtomwriteError;
use crate::output::NdjsonWriter;
use crate::platform;
pub fn cmd_move(
args: &MoveArgs,
global: &GlobalArgs,
writer: &mut NdjsonWriter<impl Write>,
) -> Result<()> {
let start = Instant::now();
let workspace = global.resolve_workspace()?;
let source = crate::path_safety::validate_path(&args.source, &workspace)?;
let target = crate::path_safety::validate_path(&args.target, &workspace)?;
if !source.exists() {
return Err(AtomwriteError::NotFound {
path: source.clone(),
}
.into());
}
if target.exists() {
if let (Ok(src_h), Ok(dst_h)) = (
same_file::Handle::from_path(&source),
same_file::Handle::from_path(&target),
) {
if src_h == dst_h {
return Err(AtomwriteError::InvalidInput {
reason: "source and target are the same file".into(),
}
.into());
}
}
}
if target.exists() && !args.force && !args.backup {
return Err(AtomwriteError::InvalidInput {
reason: format!(
"target {} already exists, use --force or --backup",
target.display()
),
}
.into());
}
if args.dry_run {
writer.write_event(&serde_json::json!({
"type": "plan",
"operation": "move",
"source": source.display().to_string(),
"target": target.display().to_string(),
"would_modify": true,
}))?;
return Ok(());
}
if args.backup && target.exists() {
crate::atomic::create_backup(&target, args.retention)?;
}
if let Some(parent) = target.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)
.with_context(|| format!("cannot create dirs for {}", target.display()))?;
}
}
let hash = checksum::hash_file(&source)?;
let bytes = std::fs::metadata(&source)?.len();
match std::fs::rename(&source, &target) {
Ok(()) => {
if let Some(src_parent) = source.parent() {
if let Err(e) = platform::fsync_dir(src_parent) {
tracing::warn!(
"fsync_dir after move failed for {}: {e}",
src_parent.display()
);
}
}
if let Some(tgt_parent) = target.parent() {
if let Err(e) = platform::fsync_dir(tgt_parent) {
tracing::warn!(
"fsync_dir after move failed for {}: {e}",
tgt_parent.display()
);
}
}
writer.write_event(&serde_json::json!({
"type": "moved",
"source": source.display().to_string(),
"target": target.display().to_string(),
"bytes": bytes,
"checksum": hash,
"cross_device": false,
"atomic": true,
"elapsed_ms": start.elapsed().as_millis() as u64,
}))?;
}
Err(e) if e.raw_os_error() == Some(18) => {
let content = crate::file_io::read_file_bytes(&source)?;
crate::atomic::atomic_write(
&target,
&content,
&crate::atomic::AtomicWriteOptions::default(),
&workspace,
)?;
let verify = checksum::hash_file(&target)?;
if verify != hash {
return Err(AtomwriteError::InvalidInput {
reason: format!(
"checksum mismatch after cross-device copy: expected {hash}, got {verify}"
),
}
.into());
}
std::fs::remove_file(&source)
.with_context(|| format!("cannot remove source {}", source.display()))?;
if let Some(parent) = source.parent() {
if let Err(e) = platform::fsync_dir(parent) {
tracing::warn!(
"fsync_dir after cross-device move failed for {}: {e}",
parent.display()
);
}
}
writer.write_event(&serde_json::json!({
"type": "moved",
"source": source.display().to_string(),
"target": target.display().to_string(),
"bytes": bytes,
"checksum": hash,
"cross_device": true,
"atomic": false,
"elapsed_ms": start.elapsed().as_millis() as u64,
}))?;
}
Err(e) => {
return Err(e).with_context(|| {
format!("cannot move {} to {}", source.display(), target.display())
});
}
}
Ok(())
}