mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
Some checks are pending
Cachix / Publish Flake (push) Waiting to run
Check / clippy (push) Waiting to run
Check / rustfmt (push) Waiting to run
Check / stylua (push) Waiting to run
Draft / build-unix (gcc-aarch64-linux-gnu, ubuntu-latest, aarch64-unknown-linux-gnu) (push) Waiting to run
Draft / build-unix (gcc-i686-linux-gnu, ubuntu-latest, i686-unknown-linux-gnu) (push) Waiting to run
Draft / build-unix (gcc-riscv64-linux-gnu, ubuntu-latest, riscv64gc-unknown-linux-gnu) (push) Waiting to run
Draft / build-unix (gcc-sparc64-linux-gnu, ubuntu-latest, sparc64-unknown-linux-gnu) (push) Waiting to run
Draft / build-unix (macos-latest, aarch64-apple-darwin) (push) Waiting to run
Draft / build-unix (macos-latest, x86_64-apple-darwin) (push) Waiting to run
Draft / build-unix (ubuntu-latest, x86_64-unknown-linux-gnu) (push) Waiting to run
Draft / build-windows (windows-latest, aarch64-pc-windows-msvc) (push) Waiting to run
Draft / build-windows (windows-latest, x86_64-pc-windows-msvc) (push) Waiting to run
Draft / build-musl (aarch64-unknown-linux-musl) (push) Waiting to run
Draft / build-musl (x86_64-unknown-linux-musl) (push) Waiting to run
Draft / build-snap (amd64, ubuntu-latest) (push) Waiting to run
Draft / build-snap (arm64, ubuntu-24.04-arm) (push) Waiting to run
Draft / snap (push) Blocked by required conditions
Draft / draft (push) Blocked by required conditions
Draft / nightly (push) Blocked by required conditions
Test / test (macos-latest) (push) Waiting to run
Test / test (ubuntu-latest) (push) Waiting to run
Test / test (windows-latest) (push) Waiting to run
188 lines
5.4 KiB
Rust
188 lines
5.4 KiB
Rust
use std::{ffi::{OsStr, OsString}, fs, io, path::{Component, Path, PathBuf}};
|
|
|
|
use ds_parser::Value;
|
|
use hashbrown::HashMap;
|
|
use yazi_macro::ok_or_not_found;
|
|
|
|
use super::{TrashCha, TrashEntry, TrashNode, TrashNodes};
|
|
use crate::{cha::Cha, file::File};
|
|
|
|
pub struct Trash;
|
|
|
|
impl Trash {
|
|
pub fn new() -> io::Result<Self> { Ok(Self) }
|
|
|
|
pub fn list(&self, node: Option<&TrashNode>) -> io::Result<Vec<TrashEntry>> {
|
|
let it = match fs::read_dir(self.resolve(node)?) {
|
|
Ok(it) => it,
|
|
Err(e) if e.kind() == io::ErrorKind::NotFound && node.is_none() => return Ok(vec![]),
|
|
Err(e) => return Err(e),
|
|
};
|
|
|
|
it.map(|entry| {
|
|
let entry = entry?;
|
|
let name = entry.file_name();
|
|
let path = entry.path();
|
|
let key = if node.is_some() { name.clone() } else { path.as_os_str().to_owned() };
|
|
TrashEntry::new(path, name, key)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub fn entry(&self, node: &TrashNode) -> io::Result<TrashEntry> {
|
|
let path = self.resolve(Some(node))?;
|
|
let name = path
|
|
.file_name()
|
|
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid trash item path"))?
|
|
.to_owned();
|
|
TrashEntry::new(path, name, node.key.clone())
|
|
}
|
|
|
|
pub fn metadata(&self, node: &TrashNode, follow: bool) -> io::Result<Cha> {
|
|
let path = self.resolve(Some(node))?;
|
|
Cha::from_trash(&path, path.file_name().unwrap_or_default(), follow)
|
|
}
|
|
|
|
pub(super) fn revalidate(
|
|
&self,
|
|
node: Option<&TrashNode>,
|
|
current: &File,
|
|
) -> io::Result<Option<File>> {
|
|
let latest = if let Some(node) = node {
|
|
self.entry(node)?.into_file(¤t.url)
|
|
} else {
|
|
let path = self.resolve(None)?;
|
|
let cha = match fs::symlink_metadata(&path) {
|
|
Ok(meta) => Cha::new(path.file_name().unwrap_or_default(), meta),
|
|
Err(e) if e.kind() == io::ErrorKind::NotFound => Cha::from_mold(true),
|
|
Err(e) => return Err(e),
|
|
};
|
|
File { cha, ..current.clone() }
|
|
};
|
|
|
|
let changed = !latest.cha.hits(current.cha)
|
|
|| latest.extra.link_to() != current.extra.link_to()
|
|
|| latest.extra.backing() != current.extra.backing();
|
|
|
|
Ok(changed.then_some(latest))
|
|
}
|
|
|
|
pub fn remove_file(&self, node: &TrashNode) -> io::Result<()> {
|
|
fs::remove_file(self.resolve(Some(node))?)
|
|
}
|
|
|
|
pub fn remove_dir(&self, node: &TrashNode) -> io::Result<()> {
|
|
fs::remove_dir(self.resolve(Some(node))?)
|
|
}
|
|
|
|
pub fn restore(&self, nodes: TrashNodes) -> io::Result<()> {
|
|
let root = self.resolve(None)?;
|
|
let locations = OriginalLocation::parse(&fs::read(root.join(".DS_Store"))?)?;
|
|
|
|
for node in nodes {
|
|
let name = Path::new(&node.top).file_name().unwrap_or_default();
|
|
let location = locations.get(name).ok_or_else(|| {
|
|
io::Error::new(
|
|
io::ErrorKind::NotFound,
|
|
format!("original location is unavailable for trash item: {:?}", node.top),
|
|
)
|
|
})?;
|
|
|
|
let from = self.resolve(Some(&node))?;
|
|
let to = location.join(&node)?;
|
|
|
|
let is_dir = fs::symlink_metadata(&from)?.is_dir();
|
|
if let Some(parent) = to.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
|
|
match if is_dir { fs::create_dir(&to) } else { fs::File::create_new(&to).map(|_| ()) } {
|
|
Ok(()) => fs::rename(from, to),
|
|
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Err(io::Error::new(
|
|
io::ErrorKind::AlreadyExists,
|
|
format!("restore target already exists: {to:?}"),
|
|
)),
|
|
Err(e) => Err(e),
|
|
}?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn empty(&self) -> io::Result<()> {
|
|
let root = self.resolve(None)?;
|
|
for entry in ok_or_not_found!(fs::read_dir(root), return Ok(())) {
|
|
let entry = entry?;
|
|
if entry.file_type()?.is_dir() {
|
|
fs::remove_dir_all(entry.path())?;
|
|
} else {
|
|
fs::remove_file(entry.path())?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn resolve(&self, node: Option<&TrashNode>) -> io::Result<PathBuf> {
|
|
if let Some(node) = node {
|
|
let top = Path::new(&node.top);
|
|
return Ok(if node.rel.as_os_str().is_empty() { top.into() } else { top.join(&node.rel) });
|
|
}
|
|
|
|
Ok(
|
|
dirs::home_dir()
|
|
.filter(|p| p.is_absolute())
|
|
.ok_or_else(|| {
|
|
io::Error::other("cannot determine home directory for trash root resolution")
|
|
})?
|
|
.join(".Trash"),
|
|
)
|
|
}
|
|
}
|
|
|
|
// --- OriginalLocation
|
|
#[derive(Default)]
|
|
struct OriginalLocation {
|
|
parent: Option<PathBuf>,
|
|
name: Option<OsString>,
|
|
}
|
|
|
|
impl OriginalLocation {
|
|
fn parse(bytes: &[u8]) -> io::Result<HashMap<OsString, OriginalLocation>> {
|
|
let store = ds_parser::parse(bytes).map_err(io::Error::other)?;
|
|
let mut locations = HashMap::<OsString, Self>::new();
|
|
|
|
for record in store.records {
|
|
let Value::Ustr(value) = record.value else { continue };
|
|
if value.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
let location = locations.entry_ref(OsStr::new(&record.name)).or_default();
|
|
match &record.field.fourcc().bytes() {
|
|
b"ptbL" => location.parent = Some(value.into()),
|
|
b"ptbN" => location.name = Some(value.into()),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
Ok(locations)
|
|
}
|
|
|
|
fn join(&self, node: &TrashNode) -> io::Result<PathBuf> {
|
|
let parent = self.parent.as_deref().ok_or_else(|| {
|
|
io::Error::new(io::ErrorKind::InvalidData, "trash item has no put-back location")
|
|
})?;
|
|
|
|
let name = self.name.as_deref().ok_or_else(|| {
|
|
io::Error::new(io::ErrorKind::InvalidData, "trash item has no put-back name")
|
|
})?;
|
|
|
|
let mut components = Path::new(name).components();
|
|
if !matches!(components.next(), Some(Component::Normal(_))) || components.next().is_some() {
|
|
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid trash put-back name"));
|
|
}
|
|
|
|
let top_path = Path::new("/").join(parent).join(name);
|
|
Ok(if node.rel.as_os_str().is_empty() { top_path } else { top_path.join(&node.rel) })
|
|
}
|
|
}
|