use std::fs::{self, OpenOptions};
use std::io::{ErrorKind, Write};
use std::path::{Path, PathBuf};
use super::ops::FileError;
const TRIES: u32 = 1_000;
#[must_use]
pub(super) fn home_trash() -> Option<PathBuf> {
if cfg!(windows) || cfg!(target_os = "macos") {
return None;
}
crate::storage::data_dir("Trash")
}
pub(super) fn move_to_trash(trash: &Path, path: &Path) -> Result<String, FileError> {
let (files, info) = (trash.join("files"), trash.join("info"));
for folder in [&files, &info] {
fs::create_dir_all(folder).map_err(|_| FileError::NoTrash)?;
}
let name = path.file_name().ok_or(FileError::NoTrash)?.to_string_lossy().into_owned();
let (taken, note) = reserve(&info, &name)?;
if let Err(problem) = write_info(¬e, path) {
let _ = fs::remove_file(¬e);
return Err(problem);
}
match fs::rename(path, files.join(&taken)) {
Ok(()) => Ok(taken),
Err(problem) => {
let _ = fs::remove_file(¬e);
Err(if problem.kind() == ErrorKind::CrossesDevices { FileError::NoTrash } else { problem.into() })
}
}
}
fn reserve(info: &Path, name: &str) -> Result<(String, PathBuf), FileError> {
for attempt in 0..TRIES {
let taken = if attempt == 0 { name.to_owned() } else { format!("{name}.{attempt}") };
let note = info.join(format!("{taken}.trashinfo"));
match OpenOptions::new().write(true).create_new(true).open(¬e) {
Ok(_) => return Ok((taken, note)),
Err(problem) if problem.kind() == ErrorKind::AlreadyExists => {}
Err(problem) => return Err(problem.into()),
}
}
Err(FileError::NoTrash)
}
fn write_info(note: &Path, path: &Path) -> Result<(), FileError> {
let now = crate::date::DateTime::now_local();
let date = now.date;
let stamp = format!("{:04}-{:02}-{:02}T{}", date.year(), date.month(), date.day(), now.time);
let text = format!("[Trash Info]\nPath={}\nDeletionDate={stamp}\n", encode_path(path));
let mut file = OpenOptions::new().write(true).truncate(true).open(note)?;
file.write_all(text.as_bytes())?;
file.flush()?;
Ok(())
}
fn encode_path(path: &Path) -> String {
use std::os::unix::ffi::OsStrExt;
let mut encoded = String::new();
for byte in path.as_os_str().as_bytes() {
let plain = byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~' | b'/');
if plain {
encoded.push(char::from(*byte));
} else {
encoded.push_str(&format!("%{byte:02X}"));
}
}
encoded
}