use std::path::Path;
#[cfg(not(any(feature = "cli", feature = "files")))]
use anyhow::{Context, bail};
use crate::containment::PathGuard;
use crate::plan::Operation;
use super::{ApplyMode, EditResult};
#[cfg(any(feature = "cli", feature = "files"))]
fn abs_path(path: &Path, guard: Option<&PathGuard>) -> anyhow::Result<std::path::PathBuf> {
super::library_abs_path(path, guard)
}
#[cfg(any(feature = "cli", feature = "files"))]
fn file_write(
op: Operation,
path: &Path,
mode: ApplyMode,
guard: Option<&PathGuard>,
action: &'static str,
) -> anyhow::Result<EditResult> {
let display = path.to_string_lossy();
super::execute_as_edit_result_with_path(
op,
mode,
super::library_project_root(path, guard),
guard,
action,
None,
Some(display.as_ref()),
)
}
#[cfg(not(any(feature = "cli", feature = "files")))]
fn file_write(
op: Operation,
path: &Path,
mode: ApplyMode,
guard: Option<&PathGuard>,
action: &'static str,
) -> anyhow::Result<EditResult> {
let display = path.to_string_lossy();
let abs = match &op {
Operation::FileDelete { .. } => super::library_abs_path_entry(path, guard)?,
_ => super::library_abs_path(path, guard)?,
};
let path = abs.as_path();
match op {
Operation::FileCreate { content, force, .. } => {
let path_str = display.as_ref();
crate::ops::file::reject_whitespace_only_payload(&content, "create")?;
use crate::ops::file::{PathEntryKind, classify_path_entry, path_entry_exists};
match classify_path_entry(path) {
PathEntryKind::RealDirectory => {
return Err(anyhow::Error::new(crate::exit::InvalidInputError {
msg: format!("target is not a file: {}", path.display()),
}));
}
PathEntryKind::Missing | PathEntryKind::RegularFile | PathEntryKind::Special => {}
}
crate::ops::file::ensure_parent_components_are_directories(path)?;
let force = force.unwrap_or(false);
if !force && path_entry_exists(path) {
return Err(anyhow::Error::new(crate::exit::AlreadyExistsError {
msg: format!(
"file already exists: {} (use force to overwrite)",
path.display()
),
}));
}
crate::ops::file::refuse_symlink_destination(path, path_str)
.map_err(anyhow::Error::new)?;
let original = match classify_path_entry(path) {
PathEntryKind::RegularFile => {
match crate::files::load_text_strict(path, &path_str) {
Ok(s) => s,
Err(e) if force && crate::exit::is_load_text_strict_fail(&e) => {
String::new()
}
Err(e) => return Err(e),
}
}
PathEntryKind::Missing | PathEntryKind::Special | PathEntryKind::RealDirectory => {
String::new()
}
};
let policy = crate::write::WritePolicy::default();
let (applied, backup_session) =
super::write_if_apply(path, &content, mode, &policy, guard)?;
{
let mut __e =
super::build_edit_result(&path_str, original, content, applied, action, None);
__e.backup_session = backup_session;
Ok(__e)
}
}
Operation::FileDelete { if_exists, .. } => {
let path_str = display.clone();
if !crate::ops::file::path_entry_exists(path) {
if if_exists {
return Ok(super::build_edit_result(
&path_str,
String::new(),
String::new(),
false,
action,
None,
));
}
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("file not found: {}", path.display()),
)
.into());
}
crate::ops::file::ensure_unlinkable_not_directory(path, display.as_ref())?;
let original = if crate::ops::file::is_regular_file_for_backup(path) {
crate::files::load_text_strict(path, &path_str).unwrap_or_default()
} else {
String::new()
};
let (applied, backup_session) = if mode == ApplyMode::Apply {
super::apply_mutation_at(
path,
mode,
None, super::library_project_root(path, guard),
|backup| backup.save_before_delete(path),
|| {
crate::ops::file::unlink_path_entry(path)
.with_context(|| format!("failed to delete {}", path.display()))
},
)?
} else {
(false, None)
};
{
let mut __e = super::build_edit_result(
&path_str,
original,
String::new(),
applied,
action,
None,
);
__e.backup_session = backup_session;
Ok(__e)
}
}
Operation::FileAppend { ref content, .. } | Operation::FilePrepend { ref content, .. } => {
let is_append = matches!(op, Operation::FileAppend { .. });
let content = content.clone();
let path_str = display.clone();
use crate::ops::file::{PathEntryKind, classify_path_entry, path_entry_exists};
if !path_entry_exists(path) {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("file does not exist: {}", path.display()),
)
.into());
}
if classify_path_entry(path) != PathEntryKind::RegularFile {
return Err(anyhow::Error::new(crate::exit::InvalidInputError {
msg: format!("target is not a file: {}", path.display()),
}));
}
let original = crate::files::load_text_strict(path, &path_str)?;
crate::ops::file::reject_whitespace_only_payload(
&content,
if is_append { "append" } else { "prepend" },
)?;
let combined = if is_append {
crate::ops::file::append_content(&original, &content)
} else {
crate::ops::file::prepend_content(&original, &content)
};
let policy = crate::write::WritePolicy::default();
let (applied, backup_session) =
super::write_if_apply(path, &combined, mode, &policy, guard)?;
{
let mut __e =
super::build_edit_result(&path_str, original, combined, applied, action, None);
__e.backup_session = backup_session;
Ok(__e)
}
}
_ => bail!("unsupported file operation"),
}
}
#[cfg(any(feature = "cli", feature = "files"))]
fn file_write_cross(
op: Operation,
src: &Path,
mode: ApplyMode,
guard: Option<&PathGuard>,
action: &'static str,
dest_path: Option<String>,
) -> anyhow::Result<EditResult> {
let display = src.to_string_lossy();
super::execute_as_edit_result_with_path(
op,
mode,
super::library_project_root(src, guard),
guard,
action,
dest_path,
Some(display.as_ref()),
)
}
#[cfg(not(any(feature = "cli", feature = "files")))]
fn file_write_cross(
_op: Operation,
src: &Path,
mode: ApplyMode,
guard: Option<&PathGuard>,
action: &'static str,
dest_path: Option<String>,
) -> anyhow::Result<EditResult> {
if let Operation::FileRename { to, force, .. } = _op {
let src_abs = super::library_abs_path_entry(src, guard)?;
let dst_abs = super::library_abs_path_entry(Path::new(&to), guard)?;
let src = src_abs.as_path();
let dst = dst_abs.as_path();
crate::ops::file::refuse_non_regular_destination(dst, &to)?;
if !force && crate::ops::file::path_entry_exists(dst) {
return Err(anyhow::Error::new(crate::exit::AlreadyExistsError {
msg: format!(
"destination already exists: {} (use force to overwrite)",
dst.display()
),
}));
}
let original = crate::files::try_read_text_file(src).unwrap_or_default();
let (applied, backup_session) = super::apply_cross_file_mutation(
src,
Some(dst),
mode,
guard,
|backup| {
backup.save_before_write(src)?;
if crate::ops::file::path_entry_exists(dst) && force {
backup.save_before_write(dst)?;
}
Ok(())
},
|| {
std::fs::rename(src, dst).with_context(|| {
format!("failed to rename {} -> {}", src.display(), dst.display())
})
},
)?;
{
let mut __e = super::build_edit_result(
&src.to_string_lossy(),
original.clone(),
original,
applied,
action,
dest_path,
);
__e.backup_session = backup_session;
Ok(__e)
}
} else {
bail!("unsupported cross-file operation")
}
}
pub fn file_create(
path: &Path,
content: &str,
force: bool,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
#[cfg(any(feature = "cli", feature = "files"))]
let abs = abs_path(path, guard)?;
#[cfg(any(feature = "cli", feature = "files"))]
let op_path = super::library_op_path(path, &abs, guard);
#[cfg(not(any(feature = "cli", feature = "files")))]
let op_path = path.to_string_lossy().into_owned();
let op = Operation::FileCreate {
path: op_path,
content: content.into(),
force: Some(force),
};
file_write(op, path, mode, guard, "create")
}
pub fn file_delete(
path: &Path,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
#[cfg(any(feature = "cli", feature = "files"))]
let abs = super::library_abs_path_entry(path, guard)?;
#[cfg(any(feature = "cli", feature = "files"))]
let op_path = super::library_op_path(path, &abs, guard);
#[cfg(not(any(feature = "cli", feature = "files")))]
let op_path = path.to_string_lossy().into_owned();
let op = Operation::FileDelete {
path: op_path,
if_exists: false,
};
file_write(op, path, mode, guard, "delete")
}
pub fn file_rename(
src: &Path,
dst: &Path,
force: bool,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
#[cfg(any(feature = "cli", feature = "files"))]
let src_abs = super::library_abs_path_entry(src, guard)?;
#[cfg(any(feature = "cli", feature = "files"))]
let dst_abs = super::library_abs_path_entry(dst, guard)?;
#[cfg(any(feature = "cli", feature = "files"))]
let from = super::library_op_path(src, &src_abs, guard);
#[cfg(any(feature = "cli", feature = "files"))]
let to = super::library_op_path(dst, &dst_abs, guard);
#[cfg(not(any(feature = "cli", feature = "files")))]
let from = src.to_string_lossy().into_owned();
#[cfg(not(any(feature = "cli", feature = "files")))]
let to = dst.to_string_lossy().into_owned();
let dest_str = Some(dst.to_string_lossy().into_owned());
let op = Operation::FileRename { from, to, force };
file_write_cross(op, src, mode, guard, "rename", dest_str)
}
pub fn file_append(
path: &Path,
content: &str,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
#[cfg(any(feature = "cli", feature = "files"))]
let abs = abs_path(path, guard)?;
#[cfg(any(feature = "cli", feature = "files"))]
let op_path = super::library_op_path(path, &abs, guard);
#[cfg(not(any(feature = "cli", feature = "files")))]
let op_path = path.to_string_lossy().into_owned();
let op = Operation::FileAppend {
path: op_path,
content: content.into(),
};
file_write(op, path, mode, guard, "append")
}
pub fn file_prepend(
path: &Path,
content: &str,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
#[cfg(any(feature = "cli", feature = "files"))]
let abs = abs_path(path, guard)?;
#[cfg(any(feature = "cli", feature = "files"))]
let op_path = super::library_op_path(path, &abs, guard);
#[cfg(not(any(feature = "cli", feature = "files")))]
let op_path = path.to_string_lossy().into_owned();
let op = Operation::FilePrepend {
path: op_path,
content: content.into(),
};
file_write(op, path, mode, guard, "prepend")
}