use std::path::PathBuf;
use energyml_core::path;
use energyml_core::value::OwnedValue;
use crate::error::{Error, Result};
use crate::EpcStore;
use crate::array::locate;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Rename {
pub from: String,
pub to: String,
pub objects: Vec<String>,
pub fields: usize,
pub relationships: usize,
pub refusals: Vec<String>,
}
impl Rename {
pub fn is_allowed(&self) -> bool {
self.refusals.is_empty()
}
pub fn touches(&self) -> usize {
self.fields + self.relationships
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileMove {
NotOnDisk,
Absent,
Moved(PathBuf),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Applied {
pub fields: usize,
pub relationships: usize,
pub file: FileMove,
}
pub fn plan(store: &EpcStore, from: &str, to: &str) -> Rename {
let mut out = Rename {
from: from.to_string(),
to: to.to_string(),
..Default::default()
};
if to.trim().is_empty() {
out.refusals.push("the new name is empty".to_string());
}
if to.contains('/') || to.contains('\\') {
out.refusals.push(format!(
"`{to}` is a path; EPC §3.2.3 names an external file by a bare name"
));
}
if from == to {
out.refusals
.push(format!("`{to}` is the name the file already has"));
}
if from.trim().is_empty() {
out.refusals.push("no file was named to rename".to_string());
}
let (objects, unreadable) = store.read_objects(None);
for name in unreadable {
out.refusals
.push(format!("{name} - so this cannot tell whether it names `{from}`"));
}
for (summary, object) in &objects {
let mut fields = 0;
for location in locate(object.as_reflect()) {
if location.proxy_uuid.is_some() {
continue;
}
if names_file(&location.file, from) {
fields += 1;
} else if names_file(&location.file, to) {
out.refusals.push(format!(
"`{to}` is already named by {} ({}), so renaming onto it would \
make two files one",
summary.uri, location.field_path
));
}
}
if fields > 0 {
out.objects.push(summary.uri.clone());
out.fields += fields;
}
}
for (_, file) in store.external_file_map() {
if names_file(&file, from) {
out.relationships += 1;
} else if names_file(&file, to) {
out.refusals.push(format!(
"`{to}` is already the file of an external part reference"
));
}
}
for (_, file) in store.object_external_file_map() {
if names_file(&file, from) {
out.relationships += 1;
}
}
if out.touches() == 0 && out.is_allowed() {
out.refusals
.push(format!("nothing in this package names `{from}`"));
}
out
}
pub fn apply(store: &mut EpcStore, plan: &Rename) -> Result<Applied> {
if !plan.is_allowed() {
return Err(Error::Package(format!(
"this rename was refused: {}",
plan.refusals.join("; ")
)));
}
let mut fields = 0;
for uri in &plan.objects {
let object = store.edit(uri)?;
let targets: Vec<(String, String)> = locate(&*object)
.iter()
.filter(|found| found.proxy_uuid.is_none() && names_file(&found.file, &plan.from))
.map(|found| (found.field_path.clone(), renamed(&found.file, &plan.to)))
.collect();
for (at, value) in targets {
let part = path::object_mut(&mut *object, &at)?;
let field = crate::array::file_field(part.type_info().name);
part.set_field(field, OwnedValue::Str(value))?;
fields += 1;
}
}
let mut relationships = 0;
let due: Vec<(String, String)> = store
.external_file_map()
.into_iter()
.chain(store.object_external_file_map())
.filter(|(_, file)| names_file(file, &plan.from))
.map(|(uuid, file)| (uuid, renamed(&file, &plan.to)))
.collect();
for (uuid, file) in due {
store.register_external_file(&uuid, file);
relationships += 1;
}
let file = move_file(store, plan)?;
Ok(Applied {
fields,
relationships,
file,
})
}
fn move_file(store: &mut EpcStore, plan: &Rename) -> Result<FileMove> {
if store.external_files_ref().is_virtual() {
store.external_files_mut().declare_present(plan.to.clone());
return Ok(FileMove::NotOnDisk);
}
let Ok(source) = store.external_files_ref().resolve(&plan.from) else {
return Ok(FileMove::Absent);
};
if !source.exists() {
return Ok(FileMove::Absent);
}
let destination = store.external_files_ref().destination_for(&plan.to)?;
#[cfg(feature = "hdf5")]
crate::hdf5::close_cached_files();
std::fs::rename(&source, &destination).map_err(|why| {
Error::Package(format!(
"the package now names `{}`, but the file could not be moved from {} to {}: {why} \
- the package has not been saved, so nothing on disk is inconsistent yet",
plan.to,
source.display(),
destination.display()
))
})?;
Ok(FileMove::Moved(destination))
}
fn names_file(declared: &str, wanted: &str) -> bool {
!declared.is_empty() && (declared == wanted || last_segment(declared) == wanted)
}
fn renamed(declared: &str, to: &str) -> String {
match declared.rfind(['/', '\\']) {
Some(at) => format!("{}{to}", &declared[..=at]),
None => to.to_string(),
}
}
fn last_segment(declared: &str) -> &str {
match declared.rfind(['/', '\\']) {
Some(at) => &declared[at + 1..],
None => declared,
}
}