mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
feat: allow restoring trashed items recursively (#4159)
Some checks failed
Cachix / Publish Flake (push) Has been cancelled
Check / clippy (push) Has been cancelled
Check / rustfmt (push) Has been cancelled
Check / stylua (push) Has been cancelled
Draft / build-unix (gcc-aarch64-linux-gnu, ubuntu-latest, aarch64-unknown-linux-gnu) (push) Has been cancelled
Draft / build-unix (gcc-i686-linux-gnu, ubuntu-latest, i686-unknown-linux-gnu) (push) Has been cancelled
Draft / build-unix (gcc-riscv64-linux-gnu, ubuntu-latest, riscv64gc-unknown-linux-gnu) (push) Has been cancelled
Draft / build-unix (gcc-sparc64-linux-gnu, ubuntu-latest, sparc64-unknown-linux-gnu) (push) Has been cancelled
Draft / build-unix (macos-latest, aarch64-apple-darwin) (push) Has been cancelled
Draft / build-unix (macos-latest, x86_64-apple-darwin) (push) Has been cancelled
Draft / build-unix (ubuntu-latest, x86_64-unknown-linux-gnu) (push) Has been cancelled
Draft / build-windows (windows-latest, aarch64-pc-windows-msvc) (push) Has been cancelled
Draft / build-windows (windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
Draft / build-musl (aarch64-unknown-linux-musl) (push) Has been cancelled
Draft / build-musl (x86_64-unknown-linux-musl) (push) Has been cancelled
Draft / build-snap (amd64, ubuntu-latest) (push) Has been cancelled
Draft / build-snap (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Test / test (macos-latest) (push) Has been cancelled
Test / test (ubuntu-latest) (push) Has been cancelled
Test / test (windows-latest) (push) Has been cancelled
Draft / snap (push) Has been cancelled
Draft / draft (push) Has been cancelled
Draft / nightly (push) Has been cancelled
Some checks failed
Cachix / Publish Flake (push) Has been cancelled
Check / clippy (push) Has been cancelled
Check / rustfmt (push) Has been cancelled
Check / stylua (push) Has been cancelled
Draft / build-unix (gcc-aarch64-linux-gnu, ubuntu-latest, aarch64-unknown-linux-gnu) (push) Has been cancelled
Draft / build-unix (gcc-i686-linux-gnu, ubuntu-latest, i686-unknown-linux-gnu) (push) Has been cancelled
Draft / build-unix (gcc-riscv64-linux-gnu, ubuntu-latest, riscv64gc-unknown-linux-gnu) (push) Has been cancelled
Draft / build-unix (gcc-sparc64-linux-gnu, ubuntu-latest, sparc64-unknown-linux-gnu) (push) Has been cancelled
Draft / build-unix (macos-latest, aarch64-apple-darwin) (push) Has been cancelled
Draft / build-unix (macos-latest, x86_64-apple-darwin) (push) Has been cancelled
Draft / build-unix (ubuntu-latest, x86_64-unknown-linux-gnu) (push) Has been cancelled
Draft / build-windows (windows-latest, aarch64-pc-windows-msvc) (push) Has been cancelled
Draft / build-windows (windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
Draft / build-musl (aarch64-unknown-linux-musl) (push) Has been cancelled
Draft / build-musl (x86_64-unknown-linux-musl) (push) Has been cancelled
Draft / build-snap (amd64, ubuntu-latest) (push) Has been cancelled
Draft / build-snap (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Test / test (macos-latest) (push) Has been cancelled
Test / test (ubuntu-latest) (push) Has been cancelled
Test / test (windows-latest) (push) Has been cancelled
Draft / snap (push) Has been cancelled
Draft / draft (push) Has been cancelled
Draft / nightly (push) Has been cancelled
This commit is contained in:
parent
58f1013348
commit
2c3f174eb5
58 changed files with 1651 additions and 1062 deletions
45
AGENTS.md
Normal file
45
AGENTS.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# AGENTS.md
|
||||
|
||||
## Rules
|
||||
|
||||
- Applies repo-wide. Keep changes scoped; do not create issues or pull requests, or post comments.
|
||||
|
||||
## Project
|
||||
|
||||
- Rust 2024 Cargo workspace containing all `yazi-*` crates; default members are `yazi-fm` (`yazi`) and `yazi-cli` (`ya`).
|
||||
|
||||
## Style
|
||||
|
||||
- Follow nearby code and use idiomatic Rust and Lua. Rust uses `snake_case` for modules, functions, and fields and `PascalCase` for types, traits, and variants. Lua uses PascalCase component tables, `local M` plugin modules, `snake_case` methods/locals, and `_name` private fields.
|
||||
- Preserve established terms and type families: `Url`/`UrlBuf`/`UrlCow`, `PathDyn`/`PathBufDyn`/`PathCow`, `*Ref`, `*Arc`, `*Opt`, `*State`, `*Job`, `*Prog`, `File`, `Folder`, `Tab`, `Mgr`, and `Task`. Use `Url` for logical locations and `Path` for filesystem paths.
|
||||
- Reuse established plugin and event names (`fetch`, `preload`, `peek`, `seek`, `spot`, `entry`, `setup`, `yank`, `hover`, and `select`) across Rust, Lua, and configuration.
|
||||
- Use Rust prefixes (`as_`, `to_`, `into_`, `try_`, `is_`, `has_`) according to their usual semantics; prefer descriptive names.
|
||||
- Name variables, modules, methods, and other symbols simply, elegantly, and expressively. Be creative while keeping names clear, consistent with established terminology, and idiomatic.
|
||||
- When passing arguments, use the parameter's conversion traits directly (such as `Into<_>` or `AsRef<_>`); avoid eager conversions like `.to_string()`, `.to_owned()`, and `.as_ref()` unless ownership, type inference, or semantics require them.
|
||||
- Prefer general-purpose traits and conversion APIs already provided by the codebase or its dependencies over manual construction or adapter closures; for example, use `into_lua()` where applicable.
|
||||
|
||||
## Code Changes
|
||||
|
||||
- Search and reuse first. For new features, extend existing infrastructure or data structures with general, reusable capabilities when that keeps the final code concise.
|
||||
- For refactors, inspect the whole target module and its callers first. Look for duplicated work, redundant I/O, underpowered return values, one-use wrappers, and reusable cross-platform abstractions; implement high-confidence, behavior-preserving simplifications while preserving error, fallback, and platform semantics.
|
||||
- Keep diffs minimal and avoid unrelated refactors. Prefer clear code over custom patterns or comments; comment only behavior the code cannot explain.
|
||||
- Put reusable code in the lowest suitable shared layer; avoid unnecessary dependencies and allocations. Prefer borrowed values and existing wrappers.
|
||||
- Use stable Rust APIs; nightly is formatting-only. Use only `pub`, `pub(super)`, and `pub(crate)`—never `pub(in ...)`.
|
||||
- Keep async I/O non-blocking, preserve platform/fork behavior, and follow existing error boundaries with `?`.
|
||||
- For renames or refactors, update all related variables, functions, parameters, modules, methods, types, derived types, exports, tests, configuration keys, documentation, and Lua bindings.
|
||||
- Do not add or modify tests unless requested.
|
||||
|
||||
## Validation
|
||||
|
||||
- Prefer targeted debug checks; use multiple `-p` flags for affected crates before the whole workspace.
|
||||
|
||||
```sh
|
||||
cargo check -p <package>
|
||||
cargo test -p <package>
|
||||
cargo clippy -p <package>
|
||||
find . -name '*.rs' -not -path './target/*' -exec rustfmt +nightly --check {} +
|
||||
stylua --color always --check .
|
||||
```
|
||||
|
||||
- Use `cargo check` instead of `cargo build` unless artifacts are needed. Do not use `--release` unless requested; use `scripts/build.sh <target>` for release or cross-target packaging.
|
||||
- Run relevant existing tests when needed, then inspect `git diff` and verify that only intended files changed.
|
||||
1
CLAUDE.md
Symbolic link
1
CLAUDE.md
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
AGENTS.md
|
||||
151
Cargo.lock
generated
151
Cargo.lock
generated
|
|
@ -145,7 +145,7 @@ version = "1.1.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -156,7 +156,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
|||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -319,6 +319,12 @@ version = "0.22.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.3"
|
||||
|
|
@ -573,9 +579,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8"
|
||||
checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
|
|
@ -636,9 +642,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.2"
|
||||
version = "4.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011"
|
||||
checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
|
|
@ -687,14 +693,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.1"
|
||||
version = "4.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
|
||||
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -921,15 +927,16 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "5.0.0-rc.1"
|
||||
version = "5.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c906a87e53a36ff795d72e06e8162a83c5436e3ea89e942a9cb9fc083f0a384f"
|
||||
checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"curve25519-dalek-derive",
|
||||
"digest",
|
||||
"fiat-crypto",
|
||||
"rand_core 0.10.1",
|
||||
"rustc_version",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
|
|
@ -1076,7 +1083,7 @@ dependencies = [
|
|||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1122,9 +1129,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ed25519-dalek"
|
||||
version = "3.0.0-rc.1"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1685663e23882cd8517dcbcb1c23a6ebff4433c22dfb681d760219b62cd1b849"
|
||||
checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"ed25519",
|
||||
|
|
@ -1138,9 +1145,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.16.0"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
||||
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
|
||||
|
||||
[[package]]
|
||||
name = "elliptic-curve"
|
||||
|
|
@ -1226,7 +1233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1660,7 +1667,7 @@ dependencies = [
|
|||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.62.2",
|
||||
"windows-core 0.56.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1690,7 +1697,7 @@ dependencies = [
|
|||
"exr",
|
||||
"gif",
|
||||
"image-webp",
|
||||
"moxcms",
|
||||
"moxcms 0.8.1",
|
||||
"num-traits",
|
||||
"png",
|
||||
"qoi",
|
||||
|
|
@ -1931,9 +1938,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
|
|
@ -2099,7 +2106,7 @@ dependencies = [
|
|||
"module-lattice",
|
||||
"pkcs8",
|
||||
"rand_core 0.10.1",
|
||||
"sha3",
|
||||
"sha3 0.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2170,6 +2177,16 @@ dependencies = [
|
|||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aee701c0ffba6cc991c0fefda5ff5fca1bd2f2f4a928ffba9f825b9afceedbc3"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "new_debug_unreachable"
|
||||
version = "1.0.6"
|
||||
|
|
@ -2245,7 +2262,7 @@ version = "0.50.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2436,9 +2453,9 @@ checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
|
|||
|
||||
[[package]]
|
||||
name = "p256"
|
||||
version = "0.14.0-rc.15"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6bb40a5099e2c38a09dd29321a7a7f045f165a54317679c7cdfb0cbaf8f6b1e"
|
||||
checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a"
|
||||
dependencies = [
|
||||
"ecdsa",
|
||||
"elliptic-curve",
|
||||
|
|
@ -2449,9 +2466,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "p384"
|
||||
version = "0.14.0-rc.15"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "492f329d7eb11d22dadc988626b9ea1f503b4ab043a8b1e4e2cc4ae45dabdd70"
|
||||
checksum = "d17b851e6b3e378ab4ecb07fa2ed23f4d15f075735f8fec9fa1e7bdce5f8301f"
|
||||
dependencies = [
|
||||
"ecdsa",
|
||||
"elliptic-curve",
|
||||
|
|
@ -2463,9 +2480,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "p521"
|
||||
version = "0.14.0-rc.15"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff42e4ace5424e3b6d7cb82514be89866b85af87015f80341e37dcc21a66ce6e"
|
||||
checksum = "4ad64cc32c2dc466317c12ee5853e61f159f9eab1fe7efade0395dc2e7b43449"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"ecdsa",
|
||||
|
|
@ -2662,7 +2679,7 @@ version = "1.10.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"indexmap 2.14.0",
|
||||
"quick-xml",
|
||||
"serde",
|
||||
|
|
@ -3114,7 +3131,7 @@ checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.2",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3197,9 +3214,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "russh"
|
||||
version = "0.62.2"
|
||||
version = "0.62.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca6c3c1dd0a9ad3a9915a2f6e907d158f9a7e9ac32ddc772b003faafc027d9a8"
|
||||
checksum = "b8b67b5a0d8068c89dcbe9d95df986af7a851d1f3c604525274c37468e60464f"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"bitflags",
|
||||
|
|
@ -3253,7 +3270,7 @@ dependencies = [
|
|||
"sec1",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"sha3",
|
||||
"sha3 0.12.0",
|
||||
"signature",
|
||||
"spki",
|
||||
"ssh-encoding",
|
||||
|
|
@ -3321,7 +3338,7 @@ dependencies = [
|
|||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3463,7 +3480,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.2",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3494,7 +3511,7 @@ version = "3.21.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bs58",
|
||||
"chrono",
|
||||
"hex",
|
||||
|
|
@ -3562,6 +3579,17 @@ dependencies = [
|
|||
"keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha3"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"keccak",
|
||||
"sponge-cursor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
|
|
@ -3659,7 +3687,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3672,6 +3700,12 @@ dependencies = [
|
|||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sponge-cursor"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620"
|
||||
|
||||
[[package]]
|
||||
name = "ssh-cipher"
|
||||
version = "0.3.0"
|
||||
|
|
@ -3795,9 +3829,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.2"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3"
|
||||
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -3837,10 +3871,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.3",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3880,7 +3914,7 @@ checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.2",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3975,9 +4009,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.0"
|
||||
version = "1.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee"
|
||||
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
|
|
@ -4003,9 +4037,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.18"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
|
||||
checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
|
|
@ -4014,9 +4048,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
version = "0.7.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||
checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
|
|
@ -4162,7 +4196,7 @@ version = "2.1.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9"
|
||||
dependencies = [
|
||||
"rand 0.10.2",
|
||||
"rand 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4191,7 +4225,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
|||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4434,7 +4468,7 @@ version = "0.1.11"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4850,10 +4884,10 @@ version = "26.5.6"
|
|||
dependencies = [
|
||||
"ansi-to-tui",
|
||||
"anyhow",
|
||||
"base64",
|
||||
"base64 0.23.0",
|
||||
"image",
|
||||
"inventory",
|
||||
"moxcms",
|
||||
"moxcms 0.9.0",
|
||||
"palette",
|
||||
"quantette",
|
||||
"ratatui-core",
|
||||
|
|
@ -4956,7 +4990,7 @@ version = "26.5.6"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.2",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5134,6 +5168,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"arc-swap",
|
||||
"bitflags",
|
||||
"cfg_aliases",
|
||||
"core-foundation-sys",
|
||||
"data-encoding",
|
||||
"dirs",
|
||||
|
|
@ -5146,6 +5181,7 @@ dependencies = [
|
|||
"mlua",
|
||||
"objc2",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"rand 0.10.2",
|
||||
"regex",
|
||||
"scopeguard",
|
||||
|
|
@ -5368,7 +5404,6 @@ version = "26.5.6"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"base64",
|
||||
"hashbrown 0.17.1",
|
||||
"mlua",
|
||||
"percent-encoding",
|
||||
|
|
@ -5389,7 +5424,7 @@ name = "yazi-term"
|
|||
version = "26.5.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"base64 0.23.0",
|
||||
"bitflags",
|
||||
"compact_str 0.10.0",
|
||||
"futures",
|
||||
|
|
@ -5411,7 +5446,7 @@ dependencies = [
|
|||
name = "yazi-tty"
|
||||
version = "26.5.9"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.23.0",
|
||||
"bitflags",
|
||||
"libc",
|
||||
"mlua",
|
||||
|
|
|
|||
16
Cargo.toml
16
Cargo.toml
|
|
@ -38,16 +38,16 @@ debug = false
|
|||
ansi-to-tui = "8.0.1"
|
||||
anyhow = "1.0.104"
|
||||
arc-swap = { version = "1.9.2", features = [ "serde" ] }
|
||||
base64 = "0.22.1"
|
||||
base64 = "0.23.0"
|
||||
bitflags = { version = "2.13.1", features = [ "serde" ] }
|
||||
chrono = "0.4.45"
|
||||
clap = { version = "4.6.2", features = [ "derive" ] }
|
||||
clap = { version = "4.6.4", features = [ "derive" ] }
|
||||
compact_str = { version = "0.10.0", features = [ "serde" ] }
|
||||
core-foundation-sys = "0.8.7"
|
||||
data-encoding = "2.11.0"
|
||||
dirs = "6.0.0"
|
||||
dyn-clone = "1.0.20"
|
||||
either = { version = "1.16.0" }
|
||||
either = { version = "1.17.0" }
|
||||
foldhash = "0.2.0"
|
||||
futures = "0.3.33"
|
||||
globset = "0.4.19"
|
||||
|
|
@ -55,7 +55,7 @@ hashbrown = { version = "0.17.1", features = [ "serde" ] }
|
|||
image = { version = "0.25.10", default-features = false, features = [ "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp" ] }
|
||||
indexmap = { version = "2.14.0", features = [ "serde" ] }
|
||||
inventory = "0.3.24"
|
||||
libc = "0.2.186"
|
||||
libc = "0.2.189"
|
||||
lru = "0.18.1"
|
||||
mlua = { version = "0.12.0", features = [ "anyhow", "async", "error-send", "lua55", "macros", "serde" ] }
|
||||
objc2 = "0.6.4"
|
||||
|
|
@ -67,7 +67,7 @@ rand = { version = "0.10.2", default-features = false, features =
|
|||
ratatui-core = { version = "0.1.2", default-features = false, features = [ "std", "layout-cache", "serde", "underline-color" ] }
|
||||
ratatui-widgets = { version = "0.3.2", default-features = false, features = [ "std", "unstable-rendered-line-info" ] }
|
||||
regex = "1.13.1"
|
||||
russh = { version = "0.62.2", default-features = false, features = [ "ring", "rsa" ] }
|
||||
russh = { version = "0.62.4", default-features = false, features = [ "ring", "rsa" ] }
|
||||
scopeguard = "1.2.0"
|
||||
serde = { version = "1.0.229", features = [ "derive" ] }
|
||||
serde_json = "1.0.151"
|
||||
|
|
@ -75,9 +75,9 @@ serde_with = "3.21.0"
|
|||
strum = { version = "0.28.0", features = [ "derive" ] }
|
||||
syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
|
||||
thiserror = "2.0.19"
|
||||
tokio = { version = "1.53.0", features = [ "full" ] }
|
||||
tokio-stream = "0.1.18"
|
||||
tokio-util = "0.7.18"
|
||||
tokio = { version = "1.53.1", features = [ "full" ] }
|
||||
tokio-stream = "0.1.19"
|
||||
tokio-util = "0.7.19"
|
||||
toml = { version = "1.1.3" }
|
||||
tracing = { version = "0.1.44", features = [ "max_level_debug", "release_max_level_debug" ] }
|
||||
twox-hash = { version = "2.1.3", default-features = false, features = [ "std", "random", "xxhash3_128" ] }
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"flagWords":[],"version":"0.2","language":"en","words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize","TOCTOU","fellback","watchee","Textlike","sstr","pointee","writef","wakeup","nonblocking","sigwinch","timespec","termios","tcgetattr","tcsetattr","tcgetwinsize","rustix","codepoint","codepoints","Raterm","mimetypes","Mimelist","renderables","redrawer","Padable"]}
|
||||
{"language":"en","version":"0.2","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize","TOCTOU","fellback","watchee","Textlike","sstr","pointee","writef","wakeup","nonblocking","sigwinch","timespec","termios","tcgetattr","tcsetattr","tcgetwinsize","rustix","codepoint","codepoints","Raterm","mimetypes","Mimelist","renderables","redrawer","Padable","ents"]}
|
||||
|
|
@ -28,7 +28,7 @@ anyhow = { workspace = true }
|
|||
base64 = { workspace = true }
|
||||
image = { workspace = true }
|
||||
inventory = { workspace = true }
|
||||
moxcms = "0.8.1"
|
||||
moxcms = "0.9.0"
|
||||
palette = { version = "0.7.6", default-features = false }
|
||||
quantette = { version = "0.6.0", default-features = false }
|
||||
ratatui-core = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -19,4 +19,4 @@ proc-macro = true
|
|||
# External dependencies
|
||||
proc-macro2 = "1"
|
||||
quote = "1.0.47"
|
||||
syn = { version = "3.0.2", features = [ "full" ] }
|
||||
syn = { version = "3.0.3", features = [ "full" ] }
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ rust-version.workspace = true
|
|||
[lints]
|
||||
workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
cfg_aliases = "0.2.2"
|
||||
|
||||
[dependencies]
|
||||
yazi-binding = { path = "../yazi-binding", version = "26.5.6" }
|
||||
yazi-ffi = { path = "../yazi-ffi", version = "26.5.6" }
|
||||
|
|
@ -45,8 +48,11 @@ typed-path = { workspace = true }
|
|||
[target."cfg(unix)".dependencies]
|
||||
uzers = { workspace = true }
|
||||
|
||||
[target.'cfg(all(unix, not(target_os = "macos"), not(target_os = "android"), not(target_os = "ios")))'.dependencies]
|
||||
percent-encoding = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.62.2", features = [ "Win32_Foundation", "Win32_Storage_EnhancedStorage", "Win32_System_Com_StructuredStorage", "Win32_System_SystemServices", "Win32_UI_Shell_PropertiesSystem" ] }
|
||||
windows = { version = "0.62.2", features = [ "Win32_Foundation", "Win32_Storage_EnhancedStorage", "Win32_System_Com_StructuredStorage", "Win32_System_SystemServices", "Win32_System_Variant", "Win32_UI_Shell_PropertiesSystem" ] }
|
||||
windows-sys = { version = "0.61.2", features = [ "Win32_Storage_FileSystem" ] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
|
|
|
|||
13
yazi-fs/build.rs
Normal file
13
yazi-fs/build.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
fn main() {
|
||||
cfg_aliases::cfg_aliases! {
|
||||
trash_unsupported: {
|
||||
any(target_os = "android", target_os = "ios")
|
||||
},
|
||||
trash_unix: {
|
||||
all(unix, not(trash_unsupported))
|
||||
},
|
||||
trash_freedesktop: {
|
||||
all(unix, not(target_os = "macos"), not(trash_unsupported))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@ impl Cha {
|
|||
where
|
||||
T: AsStrand,
|
||||
{
|
||||
Self::from_bare(&meta).attach(ChaKind::hidden(name, &meta))
|
||||
Self::from_bare(&meta).attach(ChaKind::hidden(name, &meta) | ChaKind::reparse(&meta))
|
||||
}
|
||||
|
||||
pub fn from_dummy<U>(_url: U, r#type: Option<ChaType>) -> Self
|
||||
|
|
@ -138,8 +138,8 @@ impl Cha {
|
|||
return self;
|
||||
}
|
||||
|
||||
let retain = self.kind & (ChaKind::HIDDEN | ChaKind::SYSTEM) | ChaKind::FOLLOW;
|
||||
followed.unwrap_or(self).attach(retain)
|
||||
let retain = self.kind & (ChaKind::HIDDEN | ChaKind::SYSTEM | ChaKind::REPARSE);
|
||||
followed.unwrap_or(self).attach(retain | ChaKind::FOLLOW)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -165,6 +165,12 @@ impl Cha {
|
|||
#[inline]
|
||||
pub const fn is_dummy(self) -> bool { self.kind.contains(ChaKind::DUMMY) }
|
||||
|
||||
#[inline]
|
||||
pub const fn is_reparse(self) -> bool { self.kind.contains(ChaKind::REPARSE) }
|
||||
|
||||
#[inline]
|
||||
pub fn is_indirect(self) -> bool { self.is_link() || self.is_reparse() }
|
||||
|
||||
pub fn atime_dur(self) -> anyhow::Result<Duration> {
|
||||
if let Some(atime) = self.atime {
|
||||
Ok(atime.duration_since(UNIX_EPOCH)?)
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ use yazi_shared::strand::AsStrand;
|
|||
bitflags! {
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
|
||||
pub struct ChaKind: u8 {
|
||||
const FOLLOW = 0b0000_0001;
|
||||
const HIDDEN = 0b0000_0010;
|
||||
const SYSTEM = 0b0000_0100;
|
||||
const DUMMY = 0b0000_1000;
|
||||
const FOLLOW = 0b0000_0001;
|
||||
const HIDDEN = 0b0000_0010;
|
||||
const SYSTEM = 0b0000_0100;
|
||||
const DUMMY = 0b0000_1000;
|
||||
const REPARSE = 0b0001_0000;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -43,4 +44,20 @@ impl ChaKind {
|
|||
|
||||
me
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn reparse(_meta: &Metadata) -> Self {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
|
||||
|
||||
if _meta.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
|
||||
return Self::REPARSE;
|
||||
}
|
||||
}
|
||||
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ impl UserData for Cha {
|
|||
fields.add_field_method_get("is_link", |_, me| Ok(me.is_link()));
|
||||
fields.add_field_method_get("is_orphan", |_, me| Ok(me.is_orphan()));
|
||||
fields.add_field_method_get("is_dummy", |_, me| Ok(me.is_dummy()));
|
||||
fields.add_field_method_get("is_indirect", |_, me| Ok(me.is_indirect()));
|
||||
fields.add_field_method_get("is_block", |_, me| Ok(me.is_block()));
|
||||
fields.add_field_method_get("is_char", |_, me| Ok(me.is_char()));
|
||||
fields.add_field_method_get("is_fifo", |_, me| Ok(me.is_fifo()));
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ impl SizeCalculator {
|
|||
let p = path.to_owned();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let cha = Cha::new(p.file_name().unwrap_or_default(), std::fs::symlink_metadata(&p)?);
|
||||
if !cha.is_dir() {
|
||||
if !cha.is_dir() || cha.is_indirect() {
|
||||
return Ok(Self::Idle((VecDeque::new(), Some(cha.len)), cha));
|
||||
}
|
||||
|
||||
|
|
@ -101,11 +101,27 @@ impl SizeCalculator {
|
|||
|
||||
let Ok(ent) = next else { continue };
|
||||
let Ok(ft) = ent.file_type() else { continue };
|
||||
if ft.is_dir() {
|
||||
buf.push_back(Either::Left(ent.path()));
|
||||
} else if let Ok(meta) = ent.metadata() {
|
||||
size += meta.len();
|
||||
|
||||
// If the entry is not a directory
|
||||
if !ft.is_dir() {
|
||||
size += ent.metadata().map_or(0, |meta| meta.len());
|
||||
continue;
|
||||
}
|
||||
|
||||
// The entry is a directory, but it may be a reparse point
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let Ok(cha) = ent.metadata().map(|meta| Cha::new(ent.file_name(), meta)) else {
|
||||
continue;
|
||||
};
|
||||
if !cha.is_dir() || cha.is_indirect() {
|
||||
size += cha.len;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Now, we can safely assume the entry is a regular directory
|
||||
buf.push_back(Either::Left(ent.path()));
|
||||
}
|
||||
Some(size)
|
||||
}
|
||||
|
|
|
|||
126
yazi-fs/src/engine/local/clear.rs
Normal file
126
yazi-fs/src/engine/local/clear.rs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
use std::{io, path::Path};
|
||||
|
||||
#[cfg(unix)]
|
||||
pub(super) fn remove_dir_clean_impl(path: &Path) -> io::Result<()> {
|
||||
use std::{fs::OpenOptions, os::unix::fs::OpenOptionsExt};
|
||||
|
||||
use libc::{ELOOP, ENOENT, ENOTDIR, O_DIRECTORY, O_NOFOLLOW};
|
||||
|
||||
let dir = match OpenOptions::new().read(true).custom_flags(O_DIRECTORY | O_NOFOLLOW).open(path) {
|
||||
Ok(dir) => dir,
|
||||
Err(e) if e.raw_os_error().is_some_and(|e| matches!(e, ENOENT | ENOTDIR | ELOOP)) => {
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
clear_no_follow(dir);
|
||||
std::fs::remove_dir(path)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn clear_no_follow(dir: std::fs::File) {
|
||||
use std::{ffi::CStr, os::fd::{AsRawFd, FromRawFd, IntoRawFd}};
|
||||
|
||||
use libc::{AT_REMOVEDIR, DT_DIR, DT_UNKNOWN, O_CLOEXEC, O_DIRECTORY, O_NOFOLLOW, O_RDONLY};
|
||||
|
||||
struct Dropper(*mut libc::DIR);
|
||||
|
||||
impl Drop for Dropper {
|
||||
fn drop(&mut self) { _ = unsafe { libc::closedir(self.0) }; }
|
||||
}
|
||||
|
||||
let stream = unsafe { libc::fdopendir(dir.as_raw_fd()) };
|
||||
if stream.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = dir.into_raw_fd();
|
||||
let stream = Dropper(stream);
|
||||
let fd = unsafe { libc::dirfd(stream.0) };
|
||||
|
||||
loop {
|
||||
let entry = unsafe { libc::readdir(stream.0) };
|
||||
if entry.is_null() {
|
||||
break;
|
||||
}
|
||||
|
||||
let name = unsafe { CStr::from_ptr((*entry).d_name.as_ptr()) };
|
||||
if matches!(name.to_bytes(), b"." | b"..") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ty = unsafe { (*entry).d_type };
|
||||
if ty != DT_DIR && ty != DT_UNKNOWN {
|
||||
continue;
|
||||
}
|
||||
|
||||
let child =
|
||||
unsafe { libc::openat(fd, name.as_ptr(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC) };
|
||||
if child < 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
clear_no_follow(unsafe { std::fs::File::from_raw_fd(child) });
|
||||
_ = unsafe { libc::unlinkat(fd, name.as_ptr(), AT_REMOVEDIR) };
|
||||
}
|
||||
}
|
||||
|
||||
// --- Windows
|
||||
#[cfg(windows)]
|
||||
pub(super) fn remove_dir_clean_impl(path: &Path) -> io::Result<()> {
|
||||
match clear_no_reparse(path) {
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
|
||||
result => result,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn clear_no_reparse(path: &Path) -> io::Result<()> {
|
||||
use std::{fs::OpenOptions, os::windows::fs::{MetadataExt, OpenOptionsExt}};
|
||||
|
||||
use windows_sys::Win32::Storage::FileSystem::{DELETE, FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_READ_ATTRIBUTES, FILE_SHARE_READ, FILE_SHARE_WRITE};
|
||||
|
||||
let dir = OpenOptions::new()
|
||||
.access_mode(DELETE | FILE_READ_ATTRIBUTES)
|
||||
.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
|
||||
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
|
||||
.open(path)?;
|
||||
|
||||
let attrs = dir.metadata()?.file_attributes();
|
||||
if attrs & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT) != FILE_ATTRIBUTE_DIRECTORY {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Ok(entries) = std::fs::read_dir(path) {
|
||||
for entry in entries.flatten() {
|
||||
if entry.file_type().is_ok_and(|t| t.is_dir()) {
|
||||
clear_no_reparse(&entry.path()).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
remove_dir(&dir)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn remove_dir(dir: &std::fs::File) -> io::Result<()> {
|
||||
use std::{mem, os::windows::io::AsRawHandle};
|
||||
|
||||
use windows_sys::Win32::{Foundation::HANDLE, Storage::FileSystem::{FILE_DISPOSITION_INFO, FileDispositionInfo, SetFileInformationByHandle}};
|
||||
|
||||
let info = FILE_DISPOSITION_INFO { DeleteFile: true };
|
||||
if unsafe {
|
||||
SetFileInformationByHandle(
|
||||
dir.as_raw_handle() as HANDLE,
|
||||
FileDispositionInfo,
|
||||
&info as *const FILE_DISPOSITION_INFO as _,
|
||||
mem::size_of::<FILE_DISPOSITION_INFO>() as u32,
|
||||
)
|
||||
} == 0
|
||||
{
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -119,6 +119,12 @@ impl<'a> Engine for Local<'a> {
|
|||
#[inline]
|
||||
async fn remove_dir_all(&self) -> io::Result<()> { tokio::fs::remove_dir_all(self.path).await }
|
||||
|
||||
#[inline]
|
||||
async fn remove_dir_clean(&self) -> io::Result<()> {
|
||||
let path = self.path.to_owned();
|
||||
tokio::task::spawn_blocking(move || super::remove_dir_clean_impl(&path)).await?
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn remove_file(&self) -> io::Result<()> { tokio::fs::remove_file(self.path).await }
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
yazi_macro::mod_flat!(absolute calculator casefold copier dir_entry demand identical local read_dir);
|
||||
yazi_macro::mod_flat!(absolute calculator casefold clear copier dir_entry demand identical local read_dir);
|
||||
|
|
|
|||
|
|
@ -128,9 +128,11 @@ pub trait Engine: Sized {
|
|||
{
|
||||
let mut it = ok_or_not_found!(P::new(url).await?.read_dir().await, return Ok(()));
|
||||
while let Some(child) = it.next().await? {
|
||||
let ft = ok_or_not_found!(child.file_type().await, continue);
|
||||
let result = if ft.is_dir() {
|
||||
let cha = ok_or_not_found!(child.metadata().await, continue);
|
||||
let result = if cha.is_dir() && !cha.is_indirect() {
|
||||
Box::pin(remove_dir_all_impl::<P>(child.url().as_url())).await
|
||||
} else if cha.is_dir() {
|
||||
P::new(child.url().as_url()).await?.remove_dir().await
|
||||
} else {
|
||||
P::new(child.url().as_url()).await?.remove_file().await
|
||||
};
|
||||
|
|
@ -143,10 +145,12 @@ pub trait Engine: Sized {
|
|||
|
||||
async move {
|
||||
let cha = ok_or_not_found!(self.symlink_metadata().await, return Ok(()));
|
||||
if cha.is_link() {
|
||||
self.remove_file().await
|
||||
} else {
|
||||
if !cha.is_indirect() {
|
||||
remove_dir_all_impl::<Self>(self.url()).await
|
||||
} else if cha.is_dir() {
|
||||
self.remove_dir().await
|
||||
} else {
|
||||
self.remove_file().await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -159,18 +163,23 @@ pub trait Engine: Sized {
|
|||
let mut result = Ok(());
|
||||
|
||||
while let Some((dir, visited)) = stack.pop() {
|
||||
let Ok(engine) = Self::new(dir.as_url()).await else {
|
||||
continue;
|
||||
};
|
||||
let Ok(engine) = Self::new(dir.as_url()).await else { continue };
|
||||
|
||||
if visited {
|
||||
result = engine.remove_dir().await;
|
||||
} else if let Ok(mut it) = engine.read_dir().await {
|
||||
stack.push((dir, true));
|
||||
while let Ok(Some(ent)) = it.next().await {
|
||||
if ent.file_type().await.is_ok_and(|t| t.is_dir()) {
|
||||
stack.push((ent.url(), false));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if !engine.symlink_metadata().await.is_ok_and(|c| c.is_dir() && !c.is_indirect()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(mut it) = engine.read_dir().await else { continue };
|
||||
stack.push((dir, true));
|
||||
|
||||
while let Ok(Some(ent)) = it.next().await {
|
||||
if ent.metadata().await.is_ok_and(|c| c.is_dir() && !c.is_indirect()) {
|
||||
stack.push((ent.url(), false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
yazi-fs/src/trash/common.rs
Normal file
18
yazi-fs/src/trash/common.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#[cfg(trash_unix)]
|
||||
pub(super) fn restore_item(from: &std::path::Path, to: &std::path::Path) -> std::io::Result<()> {
|
||||
use std::{fs, io};
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
38
yazi-fs/src/trash/entries.rs
Normal file
38
yazi-fs/src/trash/entries.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
use std::vec;
|
||||
|
||||
use mlua::{FromLua, Lua, Value};
|
||||
|
||||
use super::{TrashEntry, TrashId};
|
||||
|
||||
pub(crate) struct TrashEntries(Vec<TrashEntry>);
|
||||
|
||||
impl TrashEntries {
|
||||
fn new(mut entries: Vec<TrashEntry>) -> Self {
|
||||
entries.sort_unstable_by_key(|entry| entry.rel().components().count());
|
||||
|
||||
let mut seen = Vec::<TrashId>::with_capacity(entries.len());
|
||||
entries.retain(|entry| {
|
||||
if seen.iter().any(|id| id.top() == entry.top() && entry.rel().starts_with(id.rel())) {
|
||||
false
|
||||
} else {
|
||||
seen.push(entry.id.clone());
|
||||
true
|
||||
}
|
||||
});
|
||||
Self(entries)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromLua for TrashEntries {
|
||||
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
|
||||
let entries = Vec::<TrashEntry>::from_lua(value, lua)?;
|
||||
Ok(Self::new(entries))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for TrashEntries {
|
||||
type IntoIter = vec::IntoIter<TrashEntry>;
|
||||
type Item = TrashEntry;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter { self.0.into_iter() }
|
||||
}
|
||||
|
|
@ -1,31 +1,80 @@
|
|||
use std::{ffi::OsString, path::PathBuf};
|
||||
use std::{ffi::OsStr, ops::Deref, path::{Path, PathBuf}};
|
||||
#[cfg(trash_unix)]
|
||||
use std::{ffi::OsString, io};
|
||||
|
||||
use mlua::{IntoLua, Lua, Value};
|
||||
use mlua::{AnyUserData, FromLua, Lua, UserData, UserDataFields, Value};
|
||||
use yazi_shared::path::PathBufDyn;
|
||||
use yazi_shim::mlua::UserDataFieldsExt;
|
||||
|
||||
use super::TrashId;
|
||||
use crate::cha::Cha;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TrashEntry {
|
||||
pub name: OsString,
|
||||
pub key: OsString,
|
||||
pub cha: Cha,
|
||||
pub link_to: Option<PathBuf>,
|
||||
pub backing: PathBuf,
|
||||
pub(crate) struct TrashEntry {
|
||||
pub(super) id: TrashId,
|
||||
pub(super) cha: Cha,
|
||||
pub(super) lcha: Cha,
|
||||
pub(super) original: Option<PathBuf>,
|
||||
pub(super) link_to: Option<PathBuf>,
|
||||
pub(super) backing: PathBuf,
|
||||
}
|
||||
|
||||
impl Deref for TrashEntry {
|
||||
type Target = TrashId;
|
||||
|
||||
fn deref(&self) -> &Self::Target { &self.id }
|
||||
}
|
||||
|
||||
impl TrashEntry {
|
||||
#[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))]
|
||||
pub(super) fn new(path: PathBuf, name: OsString, key: OsString) -> std::io::Result<Self> {
|
||||
#[cfg(trash_unix)]
|
||||
pub(super) fn new<B>(id: TrashId, backing: B, original: Option<PathBuf>) -> io::Result<Self>
|
||||
where
|
||||
B: Into<PathBuf>,
|
||||
{
|
||||
use super::TrashCha;
|
||||
let backing = backing.into();
|
||||
|
||||
let cha = Cha::from_trash(&path, &name, true)?;
|
||||
let link_to = if cha.is_link() { std::fs::read_link(&path).ok() } else { None };
|
||||
let name = id
|
||||
.rel()
|
||||
.file_name()
|
||||
.or_else(|| original.as_deref().and_then(Path::file_name))
|
||||
.or_else(|| backing.file_name())
|
||||
.filter(|name| !name.is_empty())
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid trash item path"))?;
|
||||
|
||||
Ok(Self { name, key, cha, link_to, backing: path })
|
||||
let (lcha, cha) = Cha::from_trash(&backing, name)?;
|
||||
let link_to = if lcha.is_link() { std::fs::read_link(&backing).ok() } else { None };
|
||||
|
||||
Ok(Self { id, cha, lcha, original, link_to, backing })
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))]
|
||||
#[cfg(trash_unix)]
|
||||
pub(super) fn top<T, B>(top: T, backing: B, original: Option<PathBuf>) -> io::Result<Self>
|
||||
where
|
||||
T: Into<PathBuf>,
|
||||
B: Into<PathBuf>,
|
||||
{
|
||||
Self::new(TrashId::new(top, PathBuf::new())?, backing, original)
|
||||
}
|
||||
|
||||
#[cfg(trash_unix)]
|
||||
pub(super) fn child(&self, name: OsString) -> io::Result<Self> {
|
||||
let original = self.original.as_ref().map(|original| original.join(&name));
|
||||
Self::new(self.id.child(&name)?, self.backing.join(&name), original)
|
||||
}
|
||||
|
||||
pub(super) fn key(&self) -> &OsStr { self.rel().file_name().unwrap_or(self.top().as_os_str()) }
|
||||
|
||||
pub(super) fn name(&self) -> &OsStr {
|
||||
self
|
||||
.rel()
|
||||
.file_name()
|
||||
.or_else(|| self.original.as_deref().and_then(Path::file_name))
|
||||
.or_else(|| self.backing.file_name())
|
||||
.expect("trash entry must have a name")
|
||||
}
|
||||
|
||||
#[cfg(trash_unix)]
|
||||
pub(super) fn into_file(self, url: impl Into<yazi_shared::url::UrlBuf>) -> crate::file::File {
|
||||
use crate::file::{File, FileExtra};
|
||||
|
||||
|
|
@ -37,17 +86,23 @@ impl TrashEntry {
|
|||
}
|
||||
}
|
||||
|
||||
impl IntoLua for TrashEntry {
|
||||
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
|
||||
let Self { name, key, cha, link_to, backing } = self;
|
||||
lua
|
||||
.create_table_from([
|
||||
("name", lua.create_external_string(name.into_encoded_bytes())?.into_lua(lua)?),
|
||||
("key", lua.create_external_string(key.into_encoded_bytes())?.into_lua(lua)?),
|
||||
("cha", cha.into_lua(lua)?),
|
||||
("link_to", link_to.map(PathBufDyn::Os).into_lua(lua)?),
|
||||
("backing", PathBufDyn::Os(backing).into_lua(lua)?),
|
||||
])?
|
||||
.into_lua(lua)
|
||||
impl FromLua for TrashEntry {
|
||||
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
|
||||
AnyUserData::from_lua(value, lua)?.take()
|
||||
}
|
||||
}
|
||||
|
||||
impl UserData for TrashEntry {
|
||||
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
|
||||
fields.add_cached_field("key", |lua, me| lua.create_string(me.key().as_encoded_bytes()));
|
||||
fields.add_cached_field("top", |lua, me| {
|
||||
lua.create_string(me.top().as_os_str().as_encoded_bytes())
|
||||
});
|
||||
fields.add_cached_field("rel", |_, me| Ok(PathBufDyn::from(me.rel())));
|
||||
fields.add_cached_field("name", |lua, me| lua.create_string(me.name().as_encoded_bytes()));
|
||||
fields.add_cached_field("cha", |_, me| Ok(me.cha));
|
||||
fields.add_cached_field("original", |_, me| Ok(me.original.clone().map(PathBufDyn::Os)));
|
||||
fields.add_cached_field("link_to", |_, me| Ok(me.link_to.clone().map(PathBufDyn::Os)));
|
||||
fields.add_cached_field("backing", |_, me| Ok(PathBufDyn::Os(me.backing.clone())));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,171 +0,0 @@
|
|||
use std::{ffi::OsStr, fs, hash::Hash, io, path::{Path, PathBuf}};
|
||||
|
||||
use hashbrown::HashMap;
|
||||
use trash::{TrashItem, os_limited};
|
||||
use yazi_macro::ok_or_not_found;
|
||||
use yazi_shim::Twox128;
|
||||
|
||||
use super::{TrashCha, TrashEntry, TrashNode, TrashNodes};
|
||||
use crate::{cha::{Cha, ChaSig}, 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 Some(node) = node else {
|
||||
return os_limited::list()
|
||||
.map_err(io::Error::other)?
|
||||
.into_iter()
|
||||
.map(|item| TrashEntry::new(Self::path(&item.id)?, item.name, item.id))
|
||||
.collect();
|
||||
};
|
||||
|
||||
fs::read_dir(self.resolve(node)?)?
|
||||
.map(|entry| {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name();
|
||||
let path = entry.path();
|
||||
TrashEntry::new(path, name.clone(), name)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn entry(&self, node: &TrashNode) -> io::Result<TrashEntry> {
|
||||
let path = self.resolve(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(node)?;
|
||||
if let Some(name) = node.rel.file_name() {
|
||||
Cha::from_trash(&path, name, follow)
|
||||
} else {
|
||||
let item = self.top_item(&node.top)?;
|
||||
Cha::from_trash(&path, &item.name, 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 mut roots: Vec<_> = os_limited::trash_folders().unwrap_or_default().into_iter().collect();
|
||||
roots.sort_unstable();
|
||||
|
||||
let mut h = Twox128::default();
|
||||
for root in roots {
|
||||
let meta = ok_or_not_found!(fs::metadata(root.join("info")), continue);
|
||||
let cha = Cha::new(root.file_name().unwrap_or_default(), meta);
|
||||
|
||||
root.hash(&mut h);
|
||||
ChaSig(cha).hash(&mut h);
|
||||
}
|
||||
|
||||
let hash = h.finish_128();
|
||||
File {
|
||||
cha: Cha { len: hash as u64 ^ (hash >> 64) as u64, ..Cha::from_mold(true) },
|
||||
..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<()> {
|
||||
if node.rel.as_os_str().is_empty() {
|
||||
os_limited::purge_all([self.top_item(&node.top)?]).map_err(io::Error::other)
|
||||
} else {
|
||||
fs::remove_file(self.resolve(node)?)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_dir(&self, node: &TrashNode) -> io::Result<()> {
|
||||
if node.rel.as_os_str().is_empty() {
|
||||
os_limited::purge_all([self.top_item(&node.top)?]).map_err(io::Error::other)
|
||||
} else {
|
||||
fs::remove_dir(self.resolve(node)?)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn restore(&self, nodes: TrashNodes) -> io::Result<()> {
|
||||
let mut tops = Vec::new();
|
||||
let items: HashMap<_, _> = os_limited::list()
|
||||
.map_err(io::Error::other)?
|
||||
.into_iter()
|
||||
.map(|item| (item.id.clone(), item))
|
||||
.collect();
|
||||
|
||||
for node in nodes {
|
||||
let item = items
|
||||
.get(&node.top)
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "trash item no longer exists"))?;
|
||||
if node.rel.as_os_str().is_empty() {
|
||||
tops.push(item.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
let from = self.resolve(&node)?;
|
||||
let to = item.original_path().join(node.rel);
|
||||
|
||||
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),
|
||||
}?;
|
||||
}
|
||||
|
||||
os_limited::restore_all(tops).map_err(io::Error::other)
|
||||
}
|
||||
|
||||
pub fn empty(&self) -> io::Result<()> {
|
||||
os_limited::purge_all(os_limited::list().map_err(io::Error::other)?).map_err(io::Error::other)
|
||||
}
|
||||
|
||||
fn resolve(&self, node: &TrashNode) -> io::Result<PathBuf> {
|
||||
let path = Self::path(&node.top)?;
|
||||
Ok(if node.rel.as_os_str().is_empty() { path } else { path.join(&node.rel) })
|
||||
}
|
||||
|
||||
fn top_item(&self, key: &OsStr) -> io::Result<TrashItem> {
|
||||
os_limited::list()
|
||||
.map_err(io::Error::other)?
|
||||
.into_iter()
|
||||
.find(|item| item.id == key)
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "trash item no longer exists"))
|
||||
}
|
||||
|
||||
fn path(key: &OsStr) -> io::Result<PathBuf> {
|
||||
let info = Path::new(key); // Path of the trash info file, e.g. "~/.local/share/Trash/info/filename.txt.trashinfo"
|
||||
let parent = info
|
||||
.parent()
|
||||
.and_then(|parent| parent.parent())
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid trash item path"))?;
|
||||
let stem = info
|
||||
.file_stem()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid trash item path"))?;
|
||||
|
||||
Ok(parent.join("files").join(stem))
|
||||
}
|
||||
}
|
||||
1
yazi-fs/src/trash/freedesktop/mod.rs
Normal file
1
yazi-fs/src/trash/freedesktop/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
yazi_macro::mod_flat!(trash trash_info);
|
||||
150
yazi-fs/src/trash/freedesktop/trash.rs
Normal file
150
yazi-fs/src/trash/freedesktop/trash.rs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
use std::{fs::{self}, hash::Hash, io, path::Path};
|
||||
|
||||
use trash::os_limited;
|
||||
use yazi_macro::ok_or_not_found;
|
||||
use yazi_shim::Twox128;
|
||||
|
||||
use super::{super::{TrashCha, TrashEntries, TrashEntry, TrashId, restore_item}, TrashInfo};
|
||||
use crate::{cha::{Cha, ChaSig}, file::File};
|
||||
|
||||
pub struct Trash;
|
||||
|
||||
impl Trash {
|
||||
pub(crate) fn new() -> io::Result<Self> { Ok(Self) }
|
||||
|
||||
pub(crate) fn list(&self, entry: Option<&TrashEntry>) -> io::Result<Vec<TrashEntry>> {
|
||||
let Some(entry) = entry else {
|
||||
return self.tops();
|
||||
};
|
||||
|
||||
// TODO
|
||||
if !entry.lcha.is_dir() {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "trash item is not a directory"));
|
||||
}
|
||||
|
||||
fs::read_dir(&entry.backing)?
|
||||
.map(|dirent| {
|
||||
let dirent = dirent?;
|
||||
entry.child(dirent.file_name())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn entry(&self, id: &TrashId) -> io::Result<TrashEntry> {
|
||||
let info = TrashInfo::parse(id.top())?;
|
||||
if !os_limited::trash_folders()
|
||||
.map_err(io::Error::other)?
|
||||
.iter()
|
||||
.any(|folder| folder == &info.root)
|
||||
{
|
||||
return Err(io::Error::new(io::ErrorKind::NotFound, "trash item outside of trash folders"));
|
||||
}
|
||||
|
||||
if id.has_rel() && !fs::symlink_metadata(&info.backing)?.file_type().is_dir() {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "trash item is not a directory"));
|
||||
}
|
||||
|
||||
let backing = info.backing.join(id.rel());
|
||||
TrashEntry::new(id.clone(), backing, Some(info.original.join(id.rel())))
|
||||
}
|
||||
|
||||
pub(crate) fn metadata(&self, entry: &TrashEntry, follow: bool) -> io::Result<Cha> {
|
||||
Ok(if follow { entry.cha } else { entry.lcha })
|
||||
}
|
||||
|
||||
pub(crate) fn revalidate(
|
||||
&self,
|
||||
entry: Option<&TrashEntry>,
|
||||
current: &File,
|
||||
) -> io::Result<Option<File>> {
|
||||
let latest = if let Some(entry) = entry {
|
||||
entry.clone().into_file(¤t.url)
|
||||
} else {
|
||||
let mut roots: Vec<_> = os_limited::trash_folders().unwrap_or_default().into_iter().collect();
|
||||
roots.sort_unstable();
|
||||
|
||||
let mut h = Twox128::default();
|
||||
for root in roots {
|
||||
let meta = ok_or_not_found!(fs::metadata(root.join("info")), continue);
|
||||
let cha = Cha::new(root.file_name().unwrap_or_default(), meta);
|
||||
|
||||
root.hash(&mut h);
|
||||
ChaSig(cha).hash(&mut h);
|
||||
}
|
||||
|
||||
let hash = h.finish_128();
|
||||
File {
|
||||
cha: Cha { len: hash as u64 ^ (hash >> 64) as u64, ..Cha::from_mold(true) },
|
||||
..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(crate) fn remove_file(&self, entry: &TrashEntry) -> io::Result<()> {
|
||||
fs::remove_file(&entry.backing)?;
|
||||
if !entry.has_rel() {
|
||||
fs::remove_file(entry.top())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn remove_dir(&self, entry: &TrashEntry) -> io::Result<()> {
|
||||
fs::remove_dir(&entry.backing)?;
|
||||
if !entry.has_rel() {
|
||||
fs::remove_file(entry.top())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn restore(&self, entries: TrashEntries) -> io::Result<()> {
|
||||
for entry in entries {
|
||||
let to = entry.original.clone().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::NotFound, "trash item has no put-back location")
|
||||
})?;
|
||||
|
||||
restore_item(&entry.backing, &to)?;
|
||||
|
||||
if !entry.has_rel() {
|
||||
fs::remove_file(entry.top())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// FIXME: also rename or remove the .trashinfo file in the info folder
|
||||
pub(crate) fn rename(&self, entry: &TrashEntry, path: &Path) -> io::Result<()> {
|
||||
fs::rename(&entry.backing, path)
|
||||
}
|
||||
|
||||
pub(crate) fn empty(&self) -> io::Result<()> {
|
||||
for entry in self.tops()? {
|
||||
if entry.lcha.is_dir() {
|
||||
fs::remove_dir_all(&entry.backing)?;
|
||||
} else {
|
||||
fs::remove_file(&entry.backing)?;
|
||||
}
|
||||
fs::remove_file(entry.top())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tops(&self) -> io::Result<Vec<TrashEntry>> {
|
||||
let mut tops = Vec::new();
|
||||
for root in os_limited::trash_folders().map_err(io::Error::other)? {
|
||||
for entry in ok_or_not_found!(fs::read_dir(root.join("info")), continue) {
|
||||
let entry = entry?;
|
||||
let info = entry.path();
|
||||
if let Ok(parsed) = TrashInfo::parse(&info) {
|
||||
tops.push(TrashEntry::top(info, parsed.backing, Some(parsed.original))?);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(tops)
|
||||
}
|
||||
}
|
||||
96
yazi-fs/src/trash/freedesktop/trash_info.rs
Normal file
96
yazi-fs/src/trash/freedesktop/trash_info.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
use std::{borrow::Cow, ffi::OsStr, fs::File, io::{self, BufRead, BufReader}, os::unix::ffi::OsStrExt, path::{Path, PathBuf}};
|
||||
|
||||
use percent_encoding::percent_decode;
|
||||
use uzers::Users;
|
||||
use yazi_shared::USERS_CACHE;
|
||||
use yazi_shim::path::PathExt;
|
||||
|
||||
pub(super) struct TrashInfo {
|
||||
pub(super) root: PathBuf,
|
||||
pub(super) backing: PathBuf,
|
||||
pub(super) original: PathBuf,
|
||||
}
|
||||
|
||||
impl TrashInfo {
|
||||
// Parses from a trashinfo path, e.g.:
|
||||
// /home/alice/.local/share/Trash/info/cat.jpg.trashinfo
|
||||
pub(super) fn parse(info: &Path) -> io::Result<Self> {
|
||||
if info.extension() != Some(OsStr::new("trashinfo")) {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid trash info path"));
|
||||
}
|
||||
|
||||
// /home/alice/.local/share/Trash
|
||||
let root = info
|
||||
.parent()
|
||||
.filter(|p| p.file_name() == Some(OsStr::new("info")))
|
||||
.and_then(Path::parent)
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid trash info path"))?;
|
||||
|
||||
// cat.jpg
|
||||
let stem = info
|
||||
.file_stem()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid trash info path"))?;
|
||||
|
||||
let original = Self::parse_original(info, root)?;
|
||||
if original.file_name().is_none() {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid original trash path"));
|
||||
}
|
||||
|
||||
Ok(Self { root: root.to_owned(), backing: root.join("files").join(stem), original })
|
||||
}
|
||||
|
||||
fn parse_original(info: &Path, root: &Path) -> io::Result<PathBuf> {
|
||||
let mut reader = BufReader::new(File::open(info)?);
|
||||
let mut line = Vec::new();
|
||||
|
||||
reader.read_until(b'\n', &mut line)?;
|
||||
Self::trim_line(&mut line);
|
||||
if line != b"[Trash Info]" {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid trash info header"));
|
||||
}
|
||||
|
||||
loop {
|
||||
line.clear();
|
||||
if reader.read_until(b'\n', &mut line)? == 0 {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "trash info has no Path"));
|
||||
}
|
||||
|
||||
Self::trim_line(&mut line);
|
||||
let Some(value) = line.strip_prefix(b"Path=") else { continue };
|
||||
let decoded: Cow<[u8]> = percent_decode(value).into();
|
||||
|
||||
let path = Path::new(OsStr::from_bytes(decoded.as_ref()));
|
||||
if path.as_os_str().is_empty() || !path.is_absolute() && path.has_parent_component() {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid original trash path"));
|
||||
}
|
||||
|
||||
return Ok(if path.is_absolute() {
|
||||
path.to_owned()
|
||||
} else {
|
||||
Self::mount_point(root)?.join(path)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// /mnt/disk/.Trash/1000 => /mnt/disk
|
||||
// /home/alice/.local/share/Trash => /home/alice/.local/share
|
||||
fn mount_point(root: &Path) -> io::Result<&Path> {
|
||||
let uid = USERS_CACHE.get_current_uid().to_string();
|
||||
|
||||
if root.file_name() == Some(OsStr::new(&uid))
|
||||
&& let Some(parent) = root.parent()
|
||||
&& parent.file_name() == Some(OsStr::new(".Trash"))
|
||||
{
|
||||
parent.parent()
|
||||
} else {
|
||||
root.parent()
|
||||
}
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid trash mount point"))
|
||||
}
|
||||
|
||||
fn trim_line(line: &mut Vec<u8>) {
|
||||
while matches!(line.last(), Some(b'\n' | b'\r')) {
|
||||
line.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
use std::io;
|
||||
|
||||
use mlua::{ExternalError, ExternalResult, IntoLuaMulti, LuaString, UserData, UserDataMethods, Value};
|
||||
use mlua::{ExternalError, ExternalResult, FromLua, IntoLuaMulti, LuaString, UserData, UserDataMethods, Value};
|
||||
use tokio::task::spawn_blocking;
|
||||
use yazi_binding::Error;
|
||||
use yazi_shared::path::{PathBufDyn, PathLike};
|
||||
|
||||
use super::{Trash, TrashNode, TrashNodes};
|
||||
use super::{Trash, TrashEntries, TrashEntry, TrashId};
|
||||
use crate::file::File;
|
||||
|
||||
impl UserData for Trash {
|
||||
|
|
@ -16,31 +17,72 @@ impl UserData for Trash {
|
|||
}
|
||||
});
|
||||
|
||||
methods.add_async_function("list", |lua, node: Option<TrashNode>| async move {
|
||||
match spawn_blocking(move || Trash::new()?.list(node.as_ref())).await.into_lua_err()? {
|
||||
Ok(items) => lua.create_sequence_from(items)?.into_lua_multi(&lua),
|
||||
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
|
||||
methods.add_async_function("entry", |lua, value: Value| async move {
|
||||
if let Value::UserData(ud) = value {
|
||||
return ud.borrow::<TrashEntry>()?.clone().into_lua_multi(&lua);
|
||||
}
|
||||
});
|
||||
|
||||
methods.add_async_function("entry", |lua, node: TrashNode| async move {
|
||||
match spawn_blocking(move || Trash::new()?.entry(&node)).await.into_lua_err()? {
|
||||
let id = TrashId::from_lua(value, &lua)?;
|
||||
match spawn_blocking(move || Trash::new()?.entry(&id)).await.into_lua_err()? {
|
||||
Ok(entry) => entry.into_lua_multi(&lua),
|
||||
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
|
||||
}
|
||||
});
|
||||
|
||||
methods.add_async_function("metadata", |lua, (node, follow): (TrashNode, bool)| async move {
|
||||
match spawn_blocking(move || Trash::new()?.metadata(&node, follow)).await.into_lua_err()? {
|
||||
methods.add_async_function("list", |lua, entry: Option<TrashEntry>| async move {
|
||||
match spawn_blocking(move || Trash::new()?.list(entry.as_ref())).await.into_lua_err()? {
|
||||
Ok(items) => lua.create_sequence_from(items)?.into_lua_multi(&lua),
|
||||
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
|
||||
}
|
||||
});
|
||||
|
||||
methods.add_async_function("metadata", |lua, (entry, follow): (TrashEntry, bool)| async move {
|
||||
match spawn_blocking(move || Trash::new()?.metadata(&entry, follow)).await.into_lua_err()? {
|
||||
Ok(cha) => cha.into_lua_multi(&lua),
|
||||
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
|
||||
}
|
||||
});
|
||||
|
||||
methods.add_async_function(
|
||||
"remove",
|
||||
|lua, (kind, entry): (LuaString, TrashEntry)| async move {
|
||||
let f: fn(&Trash, &TrashEntry) -> io::Result<()> = match &*kind.as_bytes() {
|
||||
b"file" => Trash::remove_file,
|
||||
b"dir" => Trash::remove_dir,
|
||||
_ => Err("Removal type must be 'file' or 'dir'".into_lua_err())?,
|
||||
};
|
||||
|
||||
match spawn_blocking(move || f(&Trash::new()?, &entry)).await.into_lua_err()? {
|
||||
Ok(()) => true.into_lua_multi(&lua),
|
||||
Err(e) => (false, Error::Io(e)).into_lua_multi(&lua),
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
methods.add_async_function(
|
||||
"rename",
|
||||
|lua, (entry, path): (TrashEntry, PathBufDyn)| async move {
|
||||
match spawn_blocking(move || Trash::new()?.rename(&entry, path.as_os()?))
|
||||
.await
|
||||
.into_lua_err()?
|
||||
{
|
||||
Ok(()) => true.into_lua_multi(&lua),
|
||||
Err(e) => (false, Error::Io(e)).into_lua_multi(&lua),
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
methods.add_async_function("restore", |lua, entries: TrashEntries| async move {
|
||||
match spawn_blocking(move || Trash::new()?.restore(entries)).await.into_lua_err()? {
|
||||
Ok(()) => true.into_lua_multi(&lua),
|
||||
Err(e) => (false, Error::Io(e)).into_lua_multi(&lua),
|
||||
}
|
||||
});
|
||||
|
||||
methods.add_async_function(
|
||||
"revalidate",
|
||||
|lua, (node, file): (Option<TrashNode>, File)| async move {
|
||||
match spawn_blocking(move || Trash::new()?.revalidate(node.as_ref(), &file))
|
||||
|lua, (entry, file): (Option<TrashEntry>, File)| async move {
|
||||
match spawn_blocking(move || Trash::new()?.revalidate(entry.as_ref(), &file))
|
||||
.await
|
||||
.into_lua_err()?
|
||||
{
|
||||
|
|
@ -49,25 +91,5 @@ impl UserData for Trash {
|
|||
}
|
||||
},
|
||||
);
|
||||
|
||||
methods.add_async_function("remove", |lua, (kind, node): (LuaString, TrashNode)| async move {
|
||||
let f: fn(&Trash, &TrashNode) -> io::Result<()> = match &*kind.as_bytes() {
|
||||
b"file" => Trash::remove_file,
|
||||
b"dir" => Trash::remove_dir,
|
||||
_ => Err("Removal type must be 'file' or 'dir'".into_lua_err())?,
|
||||
};
|
||||
|
||||
match spawn_blocking(move || f(&Trash::new()?, &node)).await.into_lua_err()? {
|
||||
Ok(()) => true.into_lua_multi(&lua),
|
||||
Err(e) => (false, Error::Io(e)).into_lua_multi(&lua),
|
||||
}
|
||||
});
|
||||
|
||||
methods.add_async_function("restore", |lua, nodes: TrashNodes| async move {
|
||||
match spawn_blocking(move || Trash::new()?.restore(nodes)).await.into_lua_err()? {
|
||||
Ok(()) => true.into_lua_multi(&lua),
|
||||
Err(e) => (false, Error::Io(e)).into_lua_multi(&lua),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,188 +0,0 @@
|
|||
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) })
|
||||
}
|
||||
}
|
||||
57
yazi-fs/src/trash/macos/ds_store.rs
Normal file
57
yazi-fs/src/trash/macos/ds_store.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
use std::{ffi::{OsStr, OsString}, fs, io, path::{Component, Path, PathBuf}};
|
||||
|
||||
use ds_parser::Value;
|
||||
use hashbrown::HashMap;
|
||||
use yazi_shim::path::PathExt;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct DsStore {
|
||||
parent: Option<PathBuf>,
|
||||
name: Option<OsString>,
|
||||
}
|
||||
|
||||
impl DsStore {
|
||||
pub(super) fn parse(path: &Path) -> io::Result<HashMap<OsString, DsStore>> {
|
||||
let bytes = fs::read(path)?;
|
||||
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)
|
||||
}
|
||||
|
||||
pub(super) fn join(&self, rel: &Path) -> io::Result<PathBuf> {
|
||||
if !rel.is_relative() || rel.has_parent_component() {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash entry path"));
|
||||
}
|
||||
|
||||
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 rel.as_os_str().is_empty() { top_path } else { top_path.join(rel) })
|
||||
}
|
||||
}
|
||||
1
yazi-fs/src/trash/macos/mod.rs
Normal file
1
yazi-fs/src/trash/macos/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
yazi_macro::mod_flat!(trash ds_store);
|
||||
133
yazi-fs/src/trash/macos/trash.rs
Normal file
133
yazi-fs/src/trash/macos/trash.rs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
use std::{fs, io, path::{Path, PathBuf}};
|
||||
|
||||
use yazi_macro::ok_or_not_found;
|
||||
|
||||
use super::{super::{TrashCha, TrashEntries, TrashEntry, TrashId, restore_item}, DsStore};
|
||||
use crate::{cha::Cha, file::File};
|
||||
|
||||
pub struct Trash;
|
||||
|
||||
impl Trash {
|
||||
pub(crate) fn new() -> io::Result<Self> { Ok(Self) }
|
||||
|
||||
pub(crate) fn list(&self, entry: Option<&TrashEntry>) -> io::Result<Vec<TrashEntry>> {
|
||||
if entry.is_some_and(|entry| !entry.lcha.is_dir()) {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "trash item is not a directory"));
|
||||
}
|
||||
|
||||
let root = self.root()?;
|
||||
let store = if entry.is_none() {
|
||||
DsStore::parse(&root.join(".DS_Store")).unwrap_or_default()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let path = entry.map_or(&root, |entry| &entry.backing);
|
||||
let it = match fs::read_dir(path) {
|
||||
Ok(it) => it,
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound && entry.is_none() => return Ok(vec![]),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
it.map(|dirent| {
|
||||
let dirent = dirent?;
|
||||
if let Some(entry) = entry {
|
||||
entry.child(dirent.file_name())
|
||||
} else {
|
||||
let path = dirent.path();
|
||||
let original = store
|
||||
.get(path.file_name().unwrap_or_default())
|
||||
.and_then(|ds| ds.join(Path::new("")).ok());
|
||||
TrashEntry::top(path.clone(), path, original)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn entry(&self, id: &TrashId) -> io::Result<TrashEntry> {
|
||||
let root = self.root()?;
|
||||
if id.top().parent() != Some(&root) {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "item is not in the trash"));
|
||||
}
|
||||
if id.has_rel() && !fs::symlink_metadata(id.top())?.file_type().is_dir() {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "trash item is not a directory"));
|
||||
}
|
||||
|
||||
let store = DsStore::parse(&root.join(".DS_Store")).unwrap_or_default();
|
||||
let name = id.top().file_name().unwrap_or_default();
|
||||
let original = store.get(name).and_then(|ds| ds.join(id.rel()).ok());
|
||||
|
||||
TrashEntry::new(id.clone(), id.path(), original)
|
||||
}
|
||||
|
||||
pub(crate) fn metadata(&self, entry: &TrashEntry, follow: bool) -> io::Result<Cha> {
|
||||
Ok(if follow { entry.cha } else { entry.lcha })
|
||||
}
|
||||
|
||||
pub(crate) fn revalidate(
|
||||
&self,
|
||||
entry: Option<&TrashEntry>,
|
||||
current: &File,
|
||||
) -> io::Result<Option<File>> {
|
||||
let latest = if let Some(entry) = entry {
|
||||
entry.clone().into_file(¤t.url)
|
||||
} else {
|
||||
let path = self.root()?;
|
||||
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(crate) fn remove_file(&self, entry: &TrashEntry) -> io::Result<()> {
|
||||
fs::remove_file(&entry.backing)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_dir(&self, entry: &TrashEntry) -> io::Result<()> {
|
||||
fs::remove_dir(&entry.backing)
|
||||
}
|
||||
|
||||
pub(crate) fn restore(&self, entries: TrashEntries) -> io::Result<()> {
|
||||
for entry in entries {
|
||||
let to = entry.original.as_ref().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::NotFound, "trash item has no put-back location")
|
||||
})?;
|
||||
|
||||
restore_item(&entry.backing, &to)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn rename(&self, entry: &TrashEntry, path: &Path) -> io::Result<()> {
|
||||
fs::rename(&entry.backing, path)
|
||||
}
|
||||
|
||||
pub(crate) fn empty(&self) -> io::Result<()> {
|
||||
let root = self.root()?;
|
||||
for dirent in ok_or_not_found!(fs::read_dir(root), return Ok(())) {
|
||||
let dirent = dirent?;
|
||||
if dirent.file_type()?.is_dir() {
|
||||
fs::remove_dir_all(dirent.path())?;
|
||||
} else {
|
||||
fs::remove_file(dirent.path())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn root(&self) -> io::Result<PathBuf> {
|
||||
dirs::home_dir()
|
||||
.filter(|p| p.is_absolute())
|
||||
.ok_or_else(|| io::Error::other("cannot determine home directory for trash root resolution"))
|
||||
.map(|home| home.join(".Trash"))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
yazi_macro::mod_flat!(entry lua node nodes);
|
||||
yazi_macro::mod_flat!(entries entry lua trash_id);
|
||||
|
||||
#[cfg(trash_unix)]
|
||||
yazi_macro::mod_flat!(common);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
yazi_macro::mod_flat!(macos);
|
||||
|
|
@ -6,11 +9,11 @@ yazi_macro::mod_flat!(macos);
|
|||
#[cfg(windows)]
|
||||
yazi_macro::mod_flat!(windows);
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos"), not(target_os = "android"), not(target_os = "ios")))]
|
||||
#[cfg(trash_freedesktop)]
|
||||
yazi_macro::mod_flat!(freedesktop);
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
#[cfg(trash_unsupported)]
|
||||
yazi_macro::mod_flat!(unsupported);
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[cfg(not(trash_unsupported))]
|
||||
yazi_macro::mod_flat!(traits);
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
use std::{ffi::OsString, io, path::{Component, PathBuf}};
|
||||
|
||||
use mlua::{BorrowedBytes, FromLua, IntoLua, Lua, Table, Value};
|
||||
use yazi_shared::{path::PathBufDyn, strand::AsStrand};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TrashNode {
|
||||
pub(super) key: OsString,
|
||||
pub(super) top: OsString,
|
||||
pub(super) rel: PathBuf,
|
||||
}
|
||||
|
||||
impl TrashNode {
|
||||
pub(super) fn validate(&self) -> io::Result<()> {
|
||||
if self.key.is_empty() || self.top.is_empty() || !self.rel.is_relative() {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash node"));
|
||||
}
|
||||
if self.rel.components().any(|c| matches!(c, Component::ParentDir)) {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash node path"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromLua for TrashNode {
|
||||
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
|
||||
let t = Table::from_lua(value, lua)?;
|
||||
let node = Self {
|
||||
key: t.raw_get::<BorrowedBytes>("key")?.as_strand().to_os_string()?,
|
||||
top: t.raw_get::<BorrowedBytes>("top")?.as_strand().to_os_string()?,
|
||||
rel: t.raw_get::<PathBufDyn>("rel")?.into_os()?,
|
||||
};
|
||||
|
||||
node.validate().map_err(mlua::Error::external)?;
|
||||
Ok(node)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoLua for TrashNode {
|
||||
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
|
||||
lua
|
||||
.create_table_from([
|
||||
("key", lua.create_external_string(self.key.into_encoded_bytes())?.into_lua(lua)?),
|
||||
("top", lua.create_external_string(self.top.into_encoded_bytes())?.into_lua(lua)?),
|
||||
("rel", PathBufDyn::from(self.rel).into_lua(lua)?),
|
||||
])?
|
||||
.into_lua(lua)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
use std::vec;
|
||||
|
||||
use mlua::{FromLua, Lua, Value};
|
||||
|
||||
use super::TrashNode;
|
||||
|
||||
pub struct TrashNodes(Vec<TrashNode>);
|
||||
|
||||
impl TrashNodes {
|
||||
fn new(mut nodes: Vec<TrashNode>) -> Self {
|
||||
nodes.sort_unstable_by_key(|node| node.rel.components().count());
|
||||
|
||||
let mut seen = Vec::<TrashNode>::with_capacity(nodes.len());
|
||||
for node in nodes {
|
||||
if seen.iter().any(|parent| parent.top == node.top && node.rel.starts_with(&parent.rel)) {
|
||||
continue;
|
||||
}
|
||||
seen.push(node);
|
||||
}
|
||||
Self(seen)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromLua for TrashNodes {
|
||||
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
|
||||
let nodes = Vec::<TrashNode>::from_lua(value, lua)?;
|
||||
Ok(Self::new(nodes))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for TrashNodes {
|
||||
type IntoIter = vec::IntoIter<TrashNode>;
|
||||
type Item = TrashNode;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter { self.0.into_iter() }
|
||||
}
|
||||
|
|
@ -1,14 +1,11 @@
|
|||
use std::fs;
|
||||
|
||||
use crate::cha::{Cha, ChaKind, ChaMode};
|
||||
|
||||
pub(super) trait TrashCha: Sized {
|
||||
fn from_mold(is_dir: bool) -> Self;
|
||||
|
||||
#[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))]
|
||||
fn from_trash(
|
||||
path: &std::path::Path,
|
||||
name: &std::ffi::OsStr,
|
||||
follow: bool,
|
||||
) -> std::io::Result<Self>;
|
||||
fn from_trash(path: &std::path::Path, name: &std::ffi::OsStr) -> std::io::Result<(Self, Self)>;
|
||||
}
|
||||
|
||||
impl TrashCha for Cha {
|
||||
|
|
@ -20,17 +17,13 @@ impl TrashCha for Cha {
|
|||
cha
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "android"), not(target_os = "ios")))]
|
||||
fn from_trash(
|
||||
path: &std::path::Path,
|
||||
name: &std::ffi::OsStr,
|
||||
follow: bool,
|
||||
) -> std::io::Result<Self> {
|
||||
let cha = Cha::new(name, std::fs::symlink_metadata(path)?);
|
||||
Ok(if cha.is_link() && follow {
|
||||
cha.follow(std::fs::metadata(path).ok().map(|meta| Cha::new(name, meta)))
|
||||
fn from_trash(path: &std::path::Path, name: &std::ffi::OsStr) -> std::io::Result<(Self, Self)> {
|
||||
let lcha = Cha::new(name, fs::symlink_metadata(path)?);
|
||||
let cha = if lcha.is_link() {
|
||||
lcha.follow(fs::metadata(path).ok().map(|meta| Cha::new(name, meta)))
|
||||
} else {
|
||||
cha
|
||||
})
|
||||
lcha
|
||||
};
|
||||
Ok((lcha, cha))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
59
yazi-fs/src/trash/trash_id.rs
Normal file
59
yazi-fs/src/trash/trash_id.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
use std::{ffi::OsStr, io, path::{Path, PathBuf}};
|
||||
|
||||
use mlua::{BorrowedBytes, ExternalResult, FromLua, Lua, Table, Value};
|
||||
use yazi_shared::{path::PathBufDyn, strand::AsStrand};
|
||||
use yazi_shim::path::PathExt;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct TrashId {
|
||||
top: PathBuf,
|
||||
rel: PathBuf,
|
||||
}
|
||||
|
||||
impl TrashId {
|
||||
pub(super) fn new<T, R>(top: T, rel: R) -> io::Result<Self>
|
||||
where
|
||||
T: Into<PathBuf>,
|
||||
R: Into<PathBuf>,
|
||||
{
|
||||
let top = top.into();
|
||||
let rel = rel.into();
|
||||
|
||||
if top.as_os_str().is_empty() || !rel.is_relative() {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash entry"));
|
||||
}
|
||||
if rel.has_parent_component() {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash entry path"));
|
||||
}
|
||||
|
||||
Ok(Self { top, rel })
|
||||
}
|
||||
|
||||
pub(super) fn top(&self) -> &Path { &self.top }
|
||||
|
||||
pub(super) fn rel(&self) -> &Path { &self.rel }
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(super) fn path(&self) -> PathBuf { self.top.join(&self.rel) }
|
||||
|
||||
pub(super) fn child(&self, name: &OsStr) -> io::Result<Self> {
|
||||
let rel = self.rel.join(name);
|
||||
if !rel.is_relative() || rel.has_parent_component() {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid trash entry path"));
|
||||
}
|
||||
|
||||
Ok(Self { top: self.top.clone(), rel })
|
||||
}
|
||||
|
||||
pub(super) fn has_rel(&self) -> bool { !self.rel.as_os_str().is_empty() }
|
||||
}
|
||||
|
||||
impl FromLua for TrashId {
|
||||
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
|
||||
let t = Table::from_lua(value, lua)?;
|
||||
|
||||
let top = t.raw_get::<BorrowedBytes>("top")?.as_strand().to_os_path()?;
|
||||
let rel = t.raw_get::<PathBufDyn>("rel")?.into_os()?;
|
||||
Self::new(top, rel).into_lua_err()
|
||||
}
|
||||
}
|
||||
1
yazi-fs/src/trash/unsupported/mod.rs
Normal file
1
yazi-fs/src/trash/unsupported/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
yazi_macro::mod_flat!(trash);
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
use std::io;
|
||||
use std::{io, path::Path};
|
||||
|
||||
use super::{TrashEntry, TrashNode, TrashNodes};
|
||||
use super::super::{TrashEntries, TrashEntry, TrashId};
|
||||
use crate::{cha::Cha, file::File};
|
||||
|
||||
pub struct Trash;
|
||||
|
|
@ -8,35 +8,39 @@ pub struct Trash;
|
|||
impl Trash {
|
||||
pub fn new() -> io::Result<Self> { Ok(Self) }
|
||||
|
||||
pub fn list(&self, _node: Option<&TrashNode>) -> io::Result<Vec<TrashEntry>> {
|
||||
pub fn list(&self, _entry: Option<&TrashEntry>) -> io::Result<Vec<TrashEntry>> {
|
||||
Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform"))
|
||||
}
|
||||
|
||||
pub fn entry(&self, _node: &TrashNode) -> io::Result<TrashEntry> {
|
||||
pub fn entry(&self, _id: &TrashId) -> io::Result<TrashEntry> {
|
||||
Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform"))
|
||||
}
|
||||
|
||||
pub fn metadata(&self, _node: &TrashNode, _: bool) -> io::Result<Cha> {
|
||||
pub fn metadata(&self, _entry: &TrashEntry, _: bool) -> io::Result<Cha> {
|
||||
Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform"))
|
||||
}
|
||||
|
||||
pub(super) fn revalidate(
|
||||
pub(crate) fn revalidate(
|
||||
&self,
|
||||
_node: Option<&TrashNode>,
|
||||
_entry: Option<&TrashEntry>,
|
||||
_current: &File,
|
||||
) -> io::Result<Option<File>> {
|
||||
Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform"))
|
||||
}
|
||||
|
||||
pub fn remove_file(&self, _node: &TrashNode) -> io::Result<()> {
|
||||
pub fn remove_file(&self, _entry: &TrashEntry) -> io::Result<()> {
|
||||
Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform"))
|
||||
}
|
||||
|
||||
pub fn remove_dir(&self, _node: &TrashNode) -> io::Result<()> {
|
||||
pub fn remove_dir(&self, _entry: &TrashEntry) -> io::Result<()> {
|
||||
Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform"))
|
||||
}
|
||||
|
||||
pub fn restore(&self, _nodes: TrashNodes) -> io::Result<()> {
|
||||
pub fn restore(&self, _entries: TrashEntries) -> io::Result<()> {
|
||||
Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform"))
|
||||
}
|
||||
|
||||
pub fn rename(&self, _entry: &TrashEntry, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::new(io::ErrorKind::Unsupported, "trash is not supported on this platform"))
|
||||
}
|
||||
|
||||
|
|
@ -1,330 +0,0 @@
|
|||
use std::{ffi::{OsStr, OsString, c_void}, fs, hash::Hash, io, mem, os::windows::ffi::{OsStrExt, OsStringExt}, path::{Path, PathBuf}, time::{Duration, SystemTime, UNIX_EPOCH}};
|
||||
|
||||
use hashbrown::HashMap;
|
||||
use trash::{TrashItem, os_limited};
|
||||
use windows::{Win32::{Foundation::*, Storage::EnhancedStorage::*, System::{Com::*, SystemServices::*}, UI::Shell::*}, core::{Interface, PCWSTR}};
|
||||
use yazi_ffi::Com;
|
||||
use yazi_shim::Twox128;
|
||||
|
||||
use super::{TrashCha, TrashEntry, TrashNode, TrashNodes};
|
||||
use crate::{cha::Cha, file::File};
|
||||
|
||||
thread_local! {
|
||||
static COM: io::Result<Com> = Com::new();
|
||||
}
|
||||
|
||||
pub struct Trash;
|
||||
|
||||
impl Trash {
|
||||
pub fn new() -> io::Result<Self> {
|
||||
COM.with(|result| {
|
||||
result.as_ref().map(|_| Self).map_err(|e| io::Error::new(e.kind(), e.to_string()))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list(&self, node: Option<&TrashNode>) -> io::Result<Vec<TrashEntry>> {
|
||||
let Some(node) = node else {
|
||||
return os_limited::list()
|
||||
.map_err(io::Error::other)?
|
||||
.into_iter()
|
||||
.map(|item| ShellItem::new(&item.id)?.entry(item.name, item.id))
|
||||
.collect();
|
||||
};
|
||||
|
||||
self
|
||||
.resolve(node)?
|
||||
.children()?
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let name = item.display_name(SIGDN_PARENTRELATIVE)?;
|
||||
item.entry(name.clone(), name)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn entry(&self, node: &TrashNode) -> io::Result<TrashEntry> {
|
||||
let item = self.resolve(node)?;
|
||||
item.entry(item.display_name(SIGDN_PARENTRELATIVE)?, &node.key)
|
||||
}
|
||||
|
||||
pub fn metadata(&self, node: &TrashNode, _: bool) -> io::Result<Cha> { self.resolve(node)?.cha() }
|
||||
|
||||
pub(super) fn revalidate(
|
||||
&self,
|
||||
node: Option<&TrashNode>,
|
||||
current: &File,
|
||||
) -> io::Result<Option<File>> {
|
||||
let cha =
|
||||
if let Some(node) = node { TrashSig::item(&self.resolve(node)?)? } else { TrashSig::root()? };
|
||||
|
||||
Ok(if cha.hits(current.cha) { None } else { Some(File { cha, ..current.clone() }) })
|
||||
}
|
||||
|
||||
pub fn remove_file(&self, node: &TrashNode) -> io::Result<()> {
|
||||
if node.rel.as_os_str().is_empty() {
|
||||
os_limited::purge_all([self.top_item(&node.top)?]).map_err(io::Error::other)
|
||||
} else {
|
||||
self.resolve(node)?.delete()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_dir(&self, node: &TrashNode) -> io::Result<()> {
|
||||
if node.rel.as_os_str().is_empty() {
|
||||
return os_limited::purge_all([self.top_item(&node.top)?]).map_err(io::Error::other);
|
||||
}
|
||||
|
||||
let item = self.resolve(node)?;
|
||||
if !item.children()?.is_empty() {
|
||||
return Err(io::Error::new(io::ErrorKind::DirectoryNotEmpty, "trash directory is not empty"));
|
||||
}
|
||||
item.delete()
|
||||
}
|
||||
|
||||
pub fn restore(&self, nodes: TrashNodes) -> io::Result<()> {
|
||||
let mut tops = Vec::new();
|
||||
let items: HashMap<_, _> = os_limited::list()
|
||||
.map_err(io::Error::other)?
|
||||
.into_iter()
|
||||
.map(|item| (item.id.clone(), item))
|
||||
.collect();
|
||||
|
||||
for node in nodes {
|
||||
let item = items
|
||||
.get(&node.top)
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "trash item no longer exists"))?;
|
||||
if node.rel.as_os_str().is_empty() {
|
||||
tops.push(item.clone());
|
||||
} else {
|
||||
let to = item.original_path().join(&node.rel);
|
||||
self.restore_do(&self.resolve(&node)?, &to)?;
|
||||
}
|
||||
}
|
||||
os_limited::restore_all(tops).map_err(io::Error::other)
|
||||
}
|
||||
|
||||
fn restore_do(&self, item: &ShellItem, to: &Path) -> io::Result<()> {
|
||||
match fs::symlink_metadata(to) {
|
||||
Ok(_) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::AlreadyExists,
|
||||
format!("restore target already exists: {to:?}"),
|
||||
));
|
||||
}
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
// Create parent directories
|
||||
let parent = to
|
||||
.parent()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid restore target"))?;
|
||||
fs::create_dir_all(parent)?;
|
||||
|
||||
let parent = ShellItem::new(parent.as_os_str())?;
|
||||
let name: Vec<u16> = to
|
||||
.file_name()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid restore target"))?
|
||||
.encode_wide()
|
||||
.chain([0])
|
||||
.collect();
|
||||
|
||||
operate(FOF_NO_UI | FOFX_EARLYFAILURE, |operation| unsafe {
|
||||
operation.MoveItem(&item.0, &parent.0, PCWSTR(name.as_ptr()), None)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn empty(&self) -> io::Result<()> {
|
||||
os_limited::purge_all(os_limited::list().map_err(io::Error::other)?).map_err(io::Error::other)
|
||||
}
|
||||
|
||||
fn resolve(&self, node: &TrashNode) -> io::Result<ShellItem> {
|
||||
let top = ShellItem::new(&node.top)?;
|
||||
if node.rel.as_os_str().is_empty() {
|
||||
return Ok(top);
|
||||
}
|
||||
|
||||
let path = PathBuf::from(top.display_name(SIGDN_FILESYSPATH)?).join(&node.rel);
|
||||
ShellItem::new(path.as_os_str())
|
||||
}
|
||||
|
||||
fn top_item(&self, key: &OsStr) -> io::Result<TrashItem> {
|
||||
os_limited::list()
|
||||
.map_err(io::Error::other)?
|
||||
.into_iter()
|
||||
.find(|item| item.id == key)
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "trash item no longer exists"))
|
||||
}
|
||||
}
|
||||
|
||||
// --- TrashSig
|
||||
struct TrashSig {
|
||||
names: Option<Vec<OsString>>,
|
||||
count_size: Option<(i64, i64)>,
|
||||
}
|
||||
|
||||
impl TrashSig {
|
||||
fn root() -> io::Result<Cha> {
|
||||
let root = ShellItem::root()?;
|
||||
let cha = root.cha()?;
|
||||
|
||||
let mut info =
|
||||
SHQUERYRBINFO { cbSize: mem::size_of::<SHQUERYRBINFO>() as u32, ..Default::default() };
|
||||
|
||||
let sig = if unsafe { SHQueryRecycleBinW(PCWSTR::null(), &mut info) }.is_ok() {
|
||||
Self { names: None, count_size: Some((info.i64NumItems, info.i64Size)) }
|
||||
} else if cha.mtime.is_some() {
|
||||
Self { names: None, count_size: None }
|
||||
} else {
|
||||
Self { names: Some(Self::names(&root)?), count_size: None }
|
||||
};
|
||||
|
||||
Ok(sig.into_cha(cha))
|
||||
}
|
||||
|
||||
fn item(item: &ShellItem) -> io::Result<Cha> {
|
||||
let cha = item.cha()?;
|
||||
let sig = if cha.mtime.is_none() && cha.is_dir() {
|
||||
Self { names: Some(Self::names(item)?), count_size: None }
|
||||
} else {
|
||||
Self { names: None, count_size: None }
|
||||
};
|
||||
|
||||
Ok(sig.into_cha(cha))
|
||||
}
|
||||
|
||||
fn names(item: &ShellItem) -> io::Result<Vec<OsString>> {
|
||||
let mut names: Vec<_> = item
|
||||
.children()?
|
||||
.into_iter()
|
||||
.map(|item| item.display_name(SIGDN_DESKTOPABSOLUTEPARSING))
|
||||
.collect::<io::Result<_>>()?;
|
||||
names.sort_unstable();
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
fn into_cha(self, mut cha: Cha) -> Cha {
|
||||
let mut h = Twox128::default();
|
||||
if let Some((count, size)) = self.count_size {
|
||||
(size, count).hash(&mut h);
|
||||
} else if let Some(names) = self.names {
|
||||
names.hash(&mut h);
|
||||
} else {
|
||||
return cha;
|
||||
}
|
||||
|
||||
let hash = h.finish_128();
|
||||
cha.ctime = UNIX_EPOCH.checked_add(Duration::from_nanos(hash as u64 ^ (hash >> 64) as u64));
|
||||
cha
|
||||
}
|
||||
}
|
||||
|
||||
// --- ShellItem
|
||||
struct ShellItem(IShellItem);
|
||||
|
||||
impl ShellItem {
|
||||
fn new(name: &OsStr) -> io::Result<Self> {
|
||||
let name: Vec<u16> = name.encode_wide().chain([0]).collect();
|
||||
unsafe { SHCreateItemFromParsingName(PCWSTR(name.as_ptr()), None) }.map(Self).map_err(error)
|
||||
}
|
||||
|
||||
fn root() -> io::Result<Self> {
|
||||
unsafe { SHGetKnownFolderItem(&FOLDERID_RecycleBinFolder, KF_FLAG_DEFAULT, None) }
|
||||
.map(Self)
|
||||
.map_err(error)
|
||||
}
|
||||
|
||||
fn children(&self) -> io::Result<Vec<Self>> {
|
||||
let items: IEnumShellItems =
|
||||
unsafe { self.0.BindToHandler(None, &BHID_EnumItems).map_err(error)? };
|
||||
|
||||
let mut result = Vec::new();
|
||||
loop {
|
||||
let mut fetched = 0;
|
||||
let mut next = [None];
|
||||
unsafe { items.Next(&mut next, Some(&mut fetched)).map_err(error)? };
|
||||
if fetched == 0 {
|
||||
break;
|
||||
} else if let Some(item) = next[0].take() {
|
||||
result.push(Self(item));
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn cha(&self) -> io::Result<Cha> {
|
||||
if let Ok(backing) = self.display_name(SIGDN_FILESYSPATH).map(PathBuf::from) {
|
||||
match fs::metadata(&backing) {
|
||||
Ok(meta) => return Ok(Cha::new(backing.file_name().unwrap_or_default(), meta)),
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => return Err(e),
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
let item: IShellItem2 = self.0.cast().map_err(error)?;
|
||||
let is_dir = unsafe { self.0.GetAttributes(SFGAO_FOLDER) }.map_err(error)? == SFGAO_FOLDER;
|
||||
|
||||
let mut cha = Cha::from_mold(is_dir);
|
||||
cha.len = unsafe { item.GetUInt64(&PKEY_Size) }.unwrap_or_default();
|
||||
cha.mtime = unsafe { item.GetFileTime(&PKEY_DateModified) }.ok().and_then(system_time);
|
||||
Ok(cha)
|
||||
}
|
||||
|
||||
fn entry<N, K>(&self, name: N, key: K) -> io::Result<TrashEntry>
|
||||
where
|
||||
N: Into<OsString>,
|
||||
K: Into<OsString>,
|
||||
{
|
||||
Ok(TrashEntry {
|
||||
name: name.into(),
|
||||
key: key.into(),
|
||||
cha: self.cha()?,
|
||||
link_to: None,
|
||||
backing: self.display_name(SIGDN_FILESYSPATH)?.into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn delete(&self) -> io::Result<()> {
|
||||
operate(FOF_NO_UI, |operation| unsafe { operation.DeleteItem(&self.0, None) })
|
||||
}
|
||||
|
||||
fn display_name(&self, kind: SIGDN) -> io::Result<OsString> {
|
||||
unsafe {
|
||||
let name = self.0.GetDisplayName(kind).map_err(error)?;
|
||||
let result = OsString::from_wide(name.as_wide());
|
||||
CoTaskMemFree(Some(name.0.cast::<c_void>()));
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn operate<F>(flags: FILEOPERATION_FLAGS, f: F) -> io::Result<()>
|
||||
where
|
||||
F: FnOnce(&IFileOperation) -> windows::core::Result<()>,
|
||||
{
|
||||
let aborted = unsafe {
|
||||
let operation: IFileOperation =
|
||||
CoCreateInstance(&FileOperation, None, CLSCTX_ALL).map_err(error)?;
|
||||
operation.SetOperationFlags(flags).map_err(error)?;
|
||||
f(&operation).map_err(error)?;
|
||||
operation.PerformOperations().map_err(error)?;
|
||||
operation.GetAnyOperationsAborted().map_err(error)?.as_bool()
|
||||
};
|
||||
|
||||
if aborted { Err(io::Error::other("trash operation was aborted")) } else { Ok(()) }
|
||||
}
|
||||
|
||||
fn error(error: windows::core::Error) -> io::Error {
|
||||
if error.code() == ERROR_FILE_NOT_FOUND.to_hresult()
|
||||
|| error.code() == ERROR_PATH_NOT_FOUND.to_hresult()
|
||||
{
|
||||
io::Error::new(io::ErrorKind::NotFound, error)
|
||||
} else {
|
||||
io::Error::other(error)
|
||||
}
|
||||
}
|
||||
|
||||
fn system_time(time: FILETIME) -> Option<SystemTime> {
|
||||
let ticks = (u64::from(time.dwHighDateTime) << 32) | u64::from(time.dwLowDateTime);
|
||||
let ticks = ticks.checked_sub(116_444_736_000_000_000)?;
|
||||
Some(UNIX_EPOCH + Duration::new(ticks / 10_000_000, (ticks % 10_000_000) as u32 * 100))
|
||||
}
|
||||
1
yazi-fs/src/trash/windows/mod.rs
Normal file
1
yazi-fs/src/trash/windows/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
yazi_macro::mod_flat!(shell_item trash trash_sig);
|
||||
96
yazi-fs/src/trash/windows/shell_item.rs
Normal file
96
yazi-fs/src/trash/windows/shell_item.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
use std::{ffi::{OsStr, OsString, c_void}, io, os::windows::ffi::{OsStrExt, OsStringExt}, path::{Path, PathBuf}};
|
||||
|
||||
use windows::{Win32::{Foundation::*, Storage::EnhancedStorage::*, System::{Com::{StructuredStorage::PropVariantToBSTR, *}, SystemServices::*}, UI::Shell::*}, core::{Interface, PCWSTR}};
|
||||
|
||||
use super::{super::{TrashCha, TrashEntry, TrashId}, trash::{error, operate, system_time}};
|
||||
use crate::cha::Cha;
|
||||
|
||||
pub(super) struct ShellItem(pub(super) IShellItem);
|
||||
|
||||
impl ShellItem {
|
||||
pub(super) fn new(name: impl AsRef<OsStr>) -> io::Result<Self> {
|
||||
let name: Vec<u16> = name.as_ref().encode_wide().chain([0]).collect();
|
||||
unsafe { SHCreateItemFromParsingName(PCWSTR(name.as_ptr()), None) }.map(Self).map_err(error)
|
||||
}
|
||||
|
||||
pub(super) fn root() -> io::Result<Self> {
|
||||
unsafe { SHGetKnownFolderItem(&FOLDERID_RecycleBinFolder, KF_FLAG_DEFAULT, None) }
|
||||
.map(Self)
|
||||
.map_err(error)
|
||||
}
|
||||
|
||||
pub(super) fn top(name: &Path) -> io::Result<Self> {
|
||||
Self::root()?
|
||||
.children()?
|
||||
.into_iter()
|
||||
.find(|item| item.display_name(SIGDN_DESKTOPABSOLUTEPARSING).is_ok_and(|s| s == name))
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "item is not in the recycle bin"))
|
||||
}
|
||||
|
||||
pub(super) fn children(&self) -> io::Result<Vec<Self>> {
|
||||
let items: IEnumShellItems =
|
||||
unsafe { self.0.BindToHandler(None, &BHID_EnumItems).map_err(error)? };
|
||||
|
||||
let mut result = Vec::new();
|
||||
loop {
|
||||
let mut fetched = 0;
|
||||
let mut next = [None];
|
||||
unsafe { items.Next(&mut next, Some(&mut fetched)).map_err(error)? };
|
||||
if fetched == 0 {
|
||||
break;
|
||||
} else if let Some(item) = next[0].take() {
|
||||
result.push(Self(item));
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub(super) fn cha(&self) -> io::Result<Cha> {
|
||||
if let Ok(backing) = self.display_name(SIGDN_FILESYSPATH).map(PathBuf::from) {
|
||||
let name = backing.file_name().unwrap_or_default();
|
||||
match Cha::from_trash(&backing, name) {
|
||||
Ok((_, cha)) => return Ok(cha),
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => return Err(e),
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
let item: IShellItem2 = self.0.cast().map_err(error)?;
|
||||
let is_dir = unsafe { self.0.GetAttributes(SFGAO_FOLDER) }.map_err(error)? == SFGAO_FOLDER;
|
||||
|
||||
let mut cha = Cha::from_mold(is_dir);
|
||||
cha.len = unsafe { item.GetUInt64(&PKEY_Size) }.unwrap_or_default();
|
||||
cha.mtime = unsafe { item.GetFileTime(&PKEY_DateModified) }.ok().and_then(system_time);
|
||||
Ok(cha)
|
||||
}
|
||||
|
||||
pub(super) fn original(&self) -> io::Result<PathBuf> {
|
||||
let item: IShellItem2 = self.0.cast().map_err(error)?;
|
||||
let value = unsafe {
|
||||
item.GetProperty(&PROPERTYKEY { fmtid: PSGUID_DISPLACED, pid: PID_DISPLACED_FROM })
|
||||
}
|
||||
.map_err(error)?;
|
||||
|
||||
let parent = unsafe { PropVariantToBSTR(&value) }.map_err(error)?;
|
||||
Ok(PathBuf::from(OsString::from_wide(&parent)).join(self.display_name(SIGDN_PARENTRELATIVE)?))
|
||||
}
|
||||
|
||||
pub(super) fn entry(&self, id: TrashId, original: Option<PathBuf>) -> io::Result<TrashEntry> {
|
||||
let backing: PathBuf = self.display_name(SIGDN_FILESYSPATH)?.into();
|
||||
let (lcha, cha) = Cha::from_trash(&backing, backing.file_name().unwrap_or_default())?;
|
||||
Ok(TrashEntry { id, cha, lcha, original, link_to: None, backing })
|
||||
}
|
||||
|
||||
pub(super) fn delete(&self) -> io::Result<()> {
|
||||
operate(FOF_NO_UI, |operation| unsafe { operation.DeleteItem(&self.0, None) })
|
||||
}
|
||||
|
||||
pub(super) fn display_name(&self, kind: SIGDN) -> io::Result<OsString> {
|
||||
unsafe {
|
||||
let name = self.0.GetDisplayName(kind).map_err(error)?;
|
||||
let result = OsString::from_wide(name.as_wide());
|
||||
CoTaskMemFree(Some(name.0.cast::<c_void>()));
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
199
yazi-fs/src/trash/windows/trash.rs
Normal file
199
yazi-fs/src/trash/windows/trash.rs
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
use std::{fs, io, os::windows::ffi::OsStrExt, path::{Path, PathBuf}, time::{Duration, SystemTime, UNIX_EPOCH}};
|
||||
|
||||
use windows::{Win32::{Foundation::*, System::Com::*, UI::Shell::*}, core::PCWSTR};
|
||||
use yazi_ffi::Com;
|
||||
|
||||
use super::{super::{TrashEntries, TrashEntry, TrashId}, shell_item::ShellItem, trash_sig::TrashSig};
|
||||
use crate::{cha::Cha, file::File};
|
||||
|
||||
thread_local! {
|
||||
static COM: io::Result<Com> = Com::new();
|
||||
}
|
||||
|
||||
pub struct Trash;
|
||||
|
||||
impl Trash {
|
||||
pub(crate) fn new() -> io::Result<Self> {
|
||||
COM.with(|result| {
|
||||
result.as_ref().map(|_| Self).map_err(|e| io::Error::new(e.kind(), e.to_string()))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn list(&self, entry: Option<&TrashEntry>) -> io::Result<Vec<TrashEntry>> {
|
||||
let Some(entry) = entry else {
|
||||
return self.tops();
|
||||
};
|
||||
|
||||
if !entry.lcha.is_dir() || entry.lcha.is_indirect() {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "trash item is not a directory"));
|
||||
}
|
||||
|
||||
let original = entry.original.as_deref().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::NotFound, "trash item has no put-back location")
|
||||
})?;
|
||||
|
||||
self
|
||||
.resolve(entry)?
|
||||
.children()?
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let name = item.display_name(SIGDN_PARENTRELATIVE)?;
|
||||
item.entry(entry.id.child(&name)?, Some(original.join(&name)))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn entry(&self, id: &TrashId) -> io::Result<TrashEntry> {
|
||||
let top = ShellItem::top(id.top())?;
|
||||
let original = top.original()?.join(id.rel());
|
||||
if !id.has_rel() {
|
||||
return top.entry(id.clone(), Some(original));
|
||||
}
|
||||
|
||||
let backing: PathBuf = top.display_name(SIGDN_FILESYSPATH)?.into();
|
||||
let cha = Cha::new(backing.file_name().unwrap_or_default(), fs::symlink_metadata(&backing)?);
|
||||
if cha.is_dir() && !cha.is_indirect() {
|
||||
ShellItem::new(backing.join(id.rel()))?.entry(id.clone(), Some(original))
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::InvalidInput, "trash item is not a directory"))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn metadata(&self, entry: &TrashEntry, follow: bool) -> io::Result<Cha> {
|
||||
Ok(if follow { entry.cha } else { entry.lcha })
|
||||
}
|
||||
|
||||
pub(crate) fn revalidate(
|
||||
&self,
|
||||
entry: Option<&TrashEntry>,
|
||||
current: &File,
|
||||
) -> io::Result<Option<File>> {
|
||||
let cha = if let Some(entry) = entry {
|
||||
TrashSig::item(&self.resolve(entry)?)?
|
||||
} else {
|
||||
TrashSig::root()?
|
||||
};
|
||||
|
||||
Ok(if cha.hits(current.cha) { None } else { Some(File { cha, ..current.clone() }) })
|
||||
}
|
||||
|
||||
pub(crate) fn remove_file(&self, entry: &TrashEntry) -> io::Result<()> {
|
||||
self.resolve(entry)?.delete()
|
||||
}
|
||||
|
||||
pub(crate) fn remove_dir(&self, entry: &TrashEntry) -> io::Result<()> {
|
||||
let item = self.resolve(entry)?;
|
||||
if !entry.has_rel() {
|
||||
item.delete()
|
||||
} else if !item.children()?.is_empty() {
|
||||
Err(io::Error::new(io::ErrorKind::DirectoryNotEmpty, "trash directory is not empty"))
|
||||
} else {
|
||||
item.delete()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn restore(&self, entries: TrashEntries) -> io::Result<()> {
|
||||
for entry in entries {
|
||||
let to = entry.original.as_deref().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::NotFound, "trash item has no put-back location")
|
||||
})?;
|
||||
|
||||
self.restore_do(&self.resolve(&entry)?, &to)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn rename(&self, entry: &TrashEntry, path: &Path) -> io::Result<()> {
|
||||
fs::rename(self.resolve(entry)?.display_name(SIGDN_FILESYSPATH)?, path)
|
||||
}
|
||||
|
||||
fn restore_do(&self, item: &ShellItem, to: &Path) -> io::Result<()> {
|
||||
match fs::symlink_metadata(to) {
|
||||
Ok(_) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::AlreadyExists,
|
||||
format!("restore target already exists: {to:?}"),
|
||||
));
|
||||
}
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
// Create parent directories
|
||||
let parent = to
|
||||
.parent()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid restore target"))?;
|
||||
fs::create_dir_all(parent)?;
|
||||
|
||||
let parent = ShellItem::new(parent.as_os_str())?;
|
||||
let name: Vec<u16> = to
|
||||
.file_name()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid restore target"))?
|
||||
.encode_wide()
|
||||
.chain([0])
|
||||
.collect();
|
||||
|
||||
operate(FOF_NO_UI | FOFX_EARLYFAILURE, |operation| unsafe {
|
||||
operation.MoveItem(&item.0, &parent.0, PCWSTR(name.as_ptr()), None)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn empty(&self) -> io::Result<()> {
|
||||
unsafe {
|
||||
SHEmptyRecycleBinW(
|
||||
None,
|
||||
PCWSTR::null(),
|
||||
SHERB_NOCONFIRMATION | SHERB_NOPROGRESSUI | SHERB_NOSOUND,
|
||||
)
|
||||
}
|
||||
.map_err(error)
|
||||
}
|
||||
|
||||
fn resolve(&self, entry: &TrashEntry) -> io::Result<ShellItem> {
|
||||
if entry.has_rel() { ShellItem::new(&entry.backing) } else { ShellItem::top(entry.top()) }
|
||||
}
|
||||
|
||||
fn tops(&self) -> io::Result<Vec<TrashEntry>> {
|
||||
ShellItem::root()?
|
||||
.children()?
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let top = item.display_name(SIGDN_DESKTOPABSOLUTEPARSING)?;
|
||||
let original = item.original()?;
|
||||
item.entry(TrashId::new(top, PathBuf::new())?, Some(original))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn operate<F>(flags: FILEOPERATION_FLAGS, f: F) -> io::Result<()>
|
||||
where
|
||||
F: FnOnce(&IFileOperation) -> windows::core::Result<()>,
|
||||
{
|
||||
let aborted = unsafe {
|
||||
let operation: IFileOperation =
|
||||
CoCreateInstance(&FileOperation, None, CLSCTX_ALL).map_err(error)?;
|
||||
operation.SetOperationFlags(flags).map_err(error)?;
|
||||
f(&operation).map_err(error)?;
|
||||
operation.PerformOperations().map_err(error)?;
|
||||
operation.GetAnyOperationsAborted().map_err(error)?.as_bool()
|
||||
};
|
||||
|
||||
if aborted { Err(io::Error::other("trash operation was aborted")) } else { Ok(()) }
|
||||
}
|
||||
|
||||
pub(super) fn error(error: windows::core::Error) -> io::Error {
|
||||
if error.code() == ERROR_FILE_NOT_FOUND.to_hresult()
|
||||
|| error.code() == ERROR_PATH_NOT_FOUND.to_hresult()
|
||||
{
|
||||
io::Error::new(io::ErrorKind::NotFound, error)
|
||||
} else {
|
||||
io::Error::other(error)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn system_time(time: FILETIME) -> Option<SystemTime> {
|
||||
let ticks = (u64::from(time.dwHighDateTime) << 32) | u64::from(time.dwLowDateTime);
|
||||
let ticks = ticks.checked_sub(116_444_736_000_000_000)?;
|
||||
Some(UNIX_EPOCH + Duration::new(ticks / 10_000_000, (ticks % 10_000_000) as u32 * 100))
|
||||
}
|
||||
68
yazi-fs/src/trash/windows/trash_sig.rs
Normal file
68
yazi-fs/src/trash/windows/trash_sig.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
use std::{ffi::OsString, hash::Hash, io, mem, time::{Duration, UNIX_EPOCH}};
|
||||
|
||||
use windows::{Win32::UI::Shell::*, core::PCWSTR};
|
||||
use yazi_shim::Twox128;
|
||||
|
||||
use super::shell_item::ShellItem;
|
||||
use crate::cha::Cha;
|
||||
|
||||
pub(super) struct TrashSig {
|
||||
names: Option<Vec<OsString>>,
|
||||
count_size: Option<(i64, i64)>,
|
||||
}
|
||||
|
||||
impl TrashSig {
|
||||
pub(super) fn root() -> io::Result<Cha> {
|
||||
let root = ShellItem::root()?;
|
||||
let cha = root.cha()?;
|
||||
|
||||
let mut info =
|
||||
SHQUERYRBINFO { cbSize: mem::size_of::<SHQUERYRBINFO>() as u32, ..Default::default() };
|
||||
|
||||
let sig = if unsafe { SHQueryRecycleBinW(PCWSTR::null(), &mut info) }.is_ok() {
|
||||
Self { names: None, count_size: Some((info.i64NumItems, info.i64Size)) }
|
||||
} else if cha.mtime.is_some() {
|
||||
Self { names: None, count_size: None }
|
||||
} else {
|
||||
Self { names: Some(Self::names(&root)?), count_size: None }
|
||||
};
|
||||
|
||||
Ok(sig.into_cha(cha))
|
||||
}
|
||||
|
||||
pub(super) fn item(item: &ShellItem) -> io::Result<Cha> {
|
||||
let cha = item.cha()?;
|
||||
let sig = if cha.mtime.is_none() && cha.is_dir() {
|
||||
Self { names: Some(Self::names(item)?), count_size: None }
|
||||
} else {
|
||||
Self { names: None, count_size: None }
|
||||
};
|
||||
|
||||
Ok(sig.into_cha(cha))
|
||||
}
|
||||
|
||||
fn names(item: &ShellItem) -> io::Result<Vec<OsString>> {
|
||||
let mut names: Vec<_> = item
|
||||
.children()?
|
||||
.into_iter()
|
||||
.map(|item| item.display_name(SIGDN_DESKTOPABSOLUTEPARSING))
|
||||
.collect::<io::Result<_>>()?;
|
||||
names.sort_unstable();
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
fn into_cha(self, mut cha: Cha) -> Cha {
|
||||
let mut h = Twox128::default();
|
||||
if let Some((count, size)) = self.count_size {
|
||||
(size, count).hash(&mut h);
|
||||
} else if let Some(names) = self.names {
|
||||
names.hash(&mut h);
|
||||
} else {
|
||||
return cha;
|
||||
}
|
||||
|
||||
let hash = h.finish_128();
|
||||
cha.ctime = UNIX_EPOCH.checked_add(Duration::from_nanos(hash as u64 ^ (hash >> 64) as u64));
|
||||
cha
|
||||
}
|
||||
}
|
||||
|
|
@ -7,18 +7,43 @@ local function top(url)
|
|||
return url.name and url or nil
|
||||
end
|
||||
|
||||
local function node(url)
|
||||
local function entry(url)
|
||||
local top = top(url)
|
||||
if not top then
|
||||
return
|
||||
return -- We are at the root, no entry to return
|
||||
end
|
||||
return {
|
||||
key = url.spec.domain,
|
||||
return fs.trash.entry {
|
||||
top = top.spec.domain,
|
||||
rel = url:strip_prefix(top) or Path.os(""),
|
||||
}
|
||||
end
|
||||
|
||||
local function file(url, ent)
|
||||
return File {
|
||||
url = url,
|
||||
cha = ent.cha,
|
||||
link_to = ent.link_to,
|
||||
backing = ent.backing,
|
||||
}
|
||||
end
|
||||
|
||||
local function files(parent, ents)
|
||||
for i, ent in ipairs(ents) do
|
||||
local url = parent:join(Path.os(ent.name)):into_domain(ent.key)
|
||||
ents[i] = file(url, ent)
|
||||
end
|
||||
return ents
|
||||
end
|
||||
|
||||
local function notify(action, err)
|
||||
ya.notify {
|
||||
title = "Trash",
|
||||
content = string.format("Failed to %s: %s", action, err),
|
||||
level = "error",
|
||||
timeout = 10,
|
||||
}
|
||||
end
|
||||
|
||||
local function absolute(url)
|
||||
if url.is_absolute then
|
||||
return fs.clean_url(url)
|
||||
|
|
@ -36,44 +61,72 @@ local function absolute(url)
|
|||
return fs.clean_url(url:join(root:join(url.path)))
|
||||
end
|
||||
|
||||
local function file(url, entry)
|
||||
entry.url = url
|
||||
return File(entry)
|
||||
end
|
||||
|
||||
local function files(parent, entries)
|
||||
for i, entry in ipairs(entries) do
|
||||
local url = parent:join(Path.os(entry.name)):into_domain(entry.key)
|
||||
entries[i] = file(url, entry)
|
||||
local restore_recursively
|
||||
local function restore(ent)
|
||||
local id = { top = ent.top, rel = ent.rel }
|
||||
local ok, err = fs.trash.restore { ent }
|
||||
if ok then
|
||||
return true, 0
|
||||
elseif err.kind ~= "AlreadyExists" then
|
||||
return false, 1, err
|
||||
end
|
||||
return entries
|
||||
return restore_recursively(id, err)
|
||||
end
|
||||
|
||||
local function notify(action, err)
|
||||
ya.notify {
|
||||
title = "Trash",
|
||||
content = string.format("Failed to %s: %s", action, err),
|
||||
level = "error",
|
||||
timeout = 10,
|
||||
}
|
||||
restore_recursively = function(id, collision)
|
||||
local ent, err = fs.trash.entry(id)
|
||||
if not ent then
|
||||
return false, 1, err
|
||||
elseif not ent.cha.is_dir or ent.cha.is_indirect then
|
||||
return false, 1, collision
|
||||
end
|
||||
|
||||
local target = ent.original and fs.cha(Url(ent.original), false)
|
||||
if not target or not target.is_dir or target.is_indirect then
|
||||
return false, 1, collision
|
||||
end
|
||||
|
||||
local ents, err = fs.trash.list(fs.trash.entry(ent))
|
||||
if not ents then
|
||||
return false, 1, err
|
||||
end
|
||||
|
||||
local failed, changed, first = 0, false, nil
|
||||
for _, child in ipairs(ents) do
|
||||
local a, b, c = restore(child)
|
||||
changed, failed, first = changed or a, failed + b, first or c
|
||||
end
|
||||
|
||||
if failed ~= 0 then
|
||||
return changed, failed, first
|
||||
end
|
||||
|
||||
local ok, err = fs.trash.remove("dir", ent)
|
||||
return changed or ok, ok and 0 or 1, err
|
||||
end
|
||||
|
||||
function M:setup()
|
||||
ps.sub_remote("trash-restore", function(args)
|
||||
ya.async(function()
|
||||
local nodes = {}
|
||||
local ents, err = {}, nil
|
||||
for i, arg in ipairs(args) do
|
||||
nodes[i] = node(Url(arg))
|
||||
if not nodes[i] then
|
||||
return notify("restore", "Cannot restore the trash root")
|
||||
ents[i], err = entry(Url(arg))
|
||||
if not ents[i] then
|
||||
return notify("restore", err)
|
||||
end
|
||||
end
|
||||
|
||||
local ok, err = fs.trash.restore(nodes)
|
||||
if ok then
|
||||
local changed, failed, first = false, 0, nil
|
||||
for _, ent in ipairs(ents) do
|
||||
local a, b, c = restore(ent)
|
||||
changed, failed, first = changed or a, failed + b, first or c
|
||||
end
|
||||
|
||||
if changed then
|
||||
ya.emit("refresh", {})
|
||||
else
|
||||
notify("restore", err)
|
||||
end
|
||||
if failed ~= 0 then
|
||||
notify("restore", string.format("%d item(s) could not be restored: %s", failed, first))
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
|
@ -115,36 +168,46 @@ function M:provide(job)
|
|||
elseif op == "Casefold" then
|
||||
return job.url
|
||||
elseif op == "Metadata" or op == "SymlinkMetadata" then
|
||||
local n = node(job.url)
|
||||
if n then
|
||||
return fs.trash.metadata(n, op == "Metadata")
|
||||
local ent, err = entry(job.url)
|
||||
if ent then
|
||||
return fs.trash.metadata(ent, op == "Metadata")
|
||||
elseif err then
|
||||
return nil, err
|
||||
else
|
||||
return Cha { mode = tonumber("40700", 8) }
|
||||
end
|
||||
elseif op == "ReadDir" then
|
||||
local entries, err = fs.trash.list(node(job.url))
|
||||
if entries then
|
||||
return files(job.url, entries)
|
||||
local ent, err = entry(job.url)
|
||||
if err then
|
||||
return nil, err
|
||||
end
|
||||
local ents, err = fs.trash.list(ent)
|
||||
if ents then
|
||||
return files(job.url, ents)
|
||||
else
|
||||
return nil, err
|
||||
end
|
||||
elseif op == "Revalidate" then
|
||||
return fs.trash.revalidate(node(job.file.url), job.file)
|
||||
elseif op == "File" then
|
||||
local n = node(job.url)
|
||||
if not n then
|
||||
return nil, Error.custom("Cannot construct a file for the trash root")
|
||||
local ent, err = entry(job.file.url)
|
||||
if err then
|
||||
return nil, err
|
||||
else
|
||||
return fs.trash.revalidate(ent, job.file)
|
||||
end
|
||||
local entry, err = fs.trash.entry(n)
|
||||
if entry then
|
||||
return file(job.url, entry)
|
||||
elseif op == "File" then
|
||||
local ent, err = entry(job.url)
|
||||
if ent then
|
||||
return file(job.url, ent)
|
||||
else
|
||||
return nil, err
|
||||
end
|
||||
elseif op == "RemoveFile" then
|
||||
return fs.trash.remove("file", node(job.url))
|
||||
elseif op == "RemoveDir" then
|
||||
return fs.trash.remove("dir", node(job.url))
|
||||
elseif op == "RemoveFile" or op == "RemoveDir" then
|
||||
local ent, err = entry(job.url)
|
||||
if ent then
|
||||
return fs.trash.remove(op == "RemoveDir" and "dir" or "file", ent)
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
return false, Error.custom("Unsupported trash operation: " .. op)
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ impl File {
|
|||
_ => Ok(()),
|
||||
},
|
||||
async |task, cha| {
|
||||
Ok(if cha.is_orphan() || (cha.is_link() && !task.follow) {
|
||||
Ok(if cha.is_orphan() || (cha.is_indirect() && !task.follow) {
|
||||
self.ops.out(id, FileOutCopy::New(0));
|
||||
self.requeue(task.into_link(), NORMAL);
|
||||
} else {
|
||||
|
|
@ -115,7 +115,7 @@ impl File {
|
|||
_ => Ok(()),
|
||||
},
|
||||
|task, cha| {
|
||||
let nofollow = cha.is_orphan() || (cha.is_link() && !task.follow);
|
||||
let nofollow = cha.is_orphan() || (cha.is_indirect() && !task.follow);
|
||||
self.ops.out(id, FileOutCut::New(if nofollow { 0 } else { cha.len }));
|
||||
|
||||
if nofollow {
|
||||
|
|
@ -196,6 +196,7 @@ impl File {
|
|||
}
|
||||
|
||||
pub(crate) async fn link_do(&self, task: FileInLink) -> Result<(), FileOutLink> {
|
||||
let mut cha = task.cha;
|
||||
let mut src: PathCow = if task.resolve {
|
||||
ok_or_not_found!(
|
||||
task,
|
||||
|
|
@ -216,17 +217,29 @@ impl File {
|
|||
ctx!(
|
||||
task,
|
||||
engine::symlink(&task.to, src, async || {
|
||||
Ok(match task.cha {
|
||||
Ok(match cha {
|
||||
Some(cha) => cha.is_dir(),
|
||||
None => Self::cha(&task.from, task.resolve, None).await?.is_dir(),
|
||||
None => {
|
||||
cha = Some(Self::cha(&task.from, task.resolve, None).await?);
|
||||
cha.unwrap().is_dir()
|
||||
}
|
||||
})
|
||||
})
|
||||
.await
|
||||
)?;
|
||||
|
||||
if task.delete {
|
||||
engine::remove_file(&task.from).await.ok();
|
||||
let cha = match cha {
|
||||
Some(cha) => cha,
|
||||
None => ctx!(task, Self::cha(&task.from, task.resolve, None).await)?,
|
||||
};
|
||||
if cha.is_dir() && cha.is_indirect() {
|
||||
engine::remove_dir(&task.from).await.ok();
|
||||
} else {
|
||||
engine::remove_file(&task.from).await.ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.ops.out(task.id, FileOutLink::Succ))
|
||||
}
|
||||
|
||||
|
|
@ -291,13 +304,20 @@ impl File {
|
|||
}
|
||||
|
||||
pub(crate) async fn delete_do(&self, task: FileInDelete) -> Result<(), FileOutDeleteDo> {
|
||||
match engine::remove_file(&task.target).await {
|
||||
let cha = task.cha.unwrap();
|
||||
let result = if cha.is_dir() && cha.is_indirect() {
|
||||
engine::remove_dir(&task.target).await
|
||||
} else {
|
||||
engine::remove_file(&task.target).await
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(()) => {}
|
||||
Err(e) if e.kind() == NotFound => {}
|
||||
Err(_) if !maybe_exists(&task.target).await => {}
|
||||
Err(e) => ctx!(task, Err(e))?,
|
||||
}
|
||||
Ok(self.ops.out(task.id, FileOutDeleteDo::Succ(task.cha.unwrap().len)))
|
||||
Ok(self.ops.out(task.id, FileOutDeleteDo::Succ(cha.len)))
|
||||
}
|
||||
|
||||
pub(crate) async fn trash(&self, task: FileInTrash) -> Result<(), FileOutTrash> {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ impl Transaction {
|
|||
let url = url.as_url();
|
||||
|
||||
let cha = ok_or_not_found!(engine::symlink_metadata(url).await, return Ok(()));
|
||||
if cha.is_link() {
|
||||
if cha.is_indirect() {
|
||||
engine::rename(Self::tmp(url).await?, url).await?;
|
||||
} else if !cha.contains(ChaMode::U_WRITE) {
|
||||
engine::set_attrs(url, Attrs::mode(cha.mode | ChaMode::U_WRITE)).await?;
|
||||
|
|
|
|||
|
|
@ -159,7 +159,8 @@ where
|
|||
E: Fn(String),
|
||||
{
|
||||
let cha = ctx!(task, task.init().await)?;
|
||||
if !cha.is_dir() {
|
||||
let follow_symlink = cha.is_link() && task.follow();
|
||||
if !cha.is_dir() || (!follow_symlink && cha.is_indirect()) {
|
||||
return on_file(task, cha).await;
|
||||
}
|
||||
|
||||
|
|
@ -198,7 +199,8 @@ where
|
|||
"Cannot get metadata for {from:?}"
|
||||
);
|
||||
|
||||
if cha.is_dir() {
|
||||
let follow_symlink = cha.is_link() && task.follow();
|
||||
if cha.is_dir() && (follow_symlink || !cha.is_indirect()) {
|
||||
dirs.push_back(from);
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::{fmt, hash::{Hash, Hasher}, sync::Arc};
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use serde::Deserialize;
|
||||
use yazi_shim::cell::RoCell;
|
||||
|
|
@ -7,7 +7,7 @@ use crate::{auth::{AuthInventory, AuthKind, Domain, EncodeAuth, Scheme}, path::{
|
|||
|
||||
pub(super) static DEFAULT_ARC: RoCell<Arc<Auth>> = RoCell::new();
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq)]
|
||||
pub struct Auth {
|
||||
pub kind: AuthKind,
|
||||
pub scheme: Scheme,
|
||||
|
|
@ -16,22 +16,6 @@ pub struct Auth {
|
|||
pub parent: Option<Arc<Auth>>,
|
||||
}
|
||||
|
||||
impl PartialEq for Auth {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.kind == other.kind && self.scheme == other.scheme && self.domain == other.domain
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Auth {}
|
||||
|
||||
impl Hash for Auth {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.kind.hash(state);
|
||||
self.scheme.hash(state);
|
||||
self.domain.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Auth {
|
||||
fn default() -> Self { Self::DEFAULT }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ impl From<std::path::PathBuf> for PathBufDyn {
|
|||
fn from(value: std::path::PathBuf) -> Self { Self::Os(value) }
|
||||
}
|
||||
|
||||
impl From<&std::path::PathBuf> for PathBufDyn {
|
||||
fn from(value: &std::path::PathBuf) -> Self { Self::Os(value.clone()) }
|
||||
}
|
||||
|
||||
impl From<typed_path::UnixPathBuf> for PathBufDyn {
|
||||
fn from(value: typed_path::UnixPathBuf) -> Self { Self::Unix(value) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,14 +92,19 @@ impl<'a> Strand<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn to_os_string(self) -> Result<OsString, StrandError> { self.as_os().map(|s| s.to_owned()) }
|
||||
|
||||
#[inline]
|
||||
pub fn as_os_path(self) -> Result<&'a std::path::Path, StrandError> {
|
||||
self.as_os().map(std::path::Path::new)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn to_os_path(self) -> Result<std::path::PathBuf, StrandError> {
|
||||
self.as_os().map(std::path::PathBuf::from)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn to_os_string(self) -> Result<OsString, StrandError> { self.as_os().map(|s| s.to_owned()) }
|
||||
|
||||
#[inline]
|
||||
pub fn as_unix_path(self) -> &'a typed_path::UnixPath {
|
||||
typed_path::UnixPath::new(self.encoded_bytes())
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
|
|||
# External dependencies
|
||||
anyhow = { workspace = true }
|
||||
arc-swap = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
hashbrown = { workspace = true }
|
||||
mlua = { workspace = true }
|
||||
percent-encoding = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
use base64::{alphabet::STANDARD, engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig}};
|
||||
|
||||
pub const BASE64_SANE: GeneralPurpose = GeneralPurpose::new(
|
||||
&STANDARD,
|
||||
GeneralPurposeConfig::new()
|
||||
.with_encode_padding(false)
|
||||
.with_decode_padding_mode(DecodePaddingMode::Indifferent),
|
||||
);
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
yazi_macro::mod_pub!(arc_swap cell fs mlua path ratatui serde strum toml vec wtf8);
|
||||
|
||||
yazi_macro::mod_flat!(base64 option percent_encoding sstr twox utf8);
|
||||
yazi_macro::mod_flat!(option percent_encoding sstr twox utf8);
|
||||
|
||||
#[cfg(windows)]
|
||||
yazi_macro::mod_flat!(win32);
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
yazi_macro::mod_flat!(separator);
|
||||
yazi_macro::mod_flat!(separator traits);
|
||||
|
|
|
|||
11
yazi-shim/src/path/traits.rs
Normal file
11
yazi-shim/src/path/traits.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
use std::path::{Component, Path};
|
||||
|
||||
pub trait PathExt {
|
||||
fn has_parent_component(&self) -> bool;
|
||||
}
|
||||
|
||||
impl PathExt for Path {
|
||||
fn has_parent_component(&self) -> bool {
|
||||
self.components().any(|c| matches!(c, Component::ParentDir))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
use std::str::SplitWhitespace;
|
||||
|
||||
use base64::Engine;
|
||||
use base64::{Engine, engine::general_purpose::STANDARD_NO_PAD_INDIFFERENT};
|
||||
use strum::{FromRepr, IntoStaticStr};
|
||||
use yazi_shim::BASE64_SANE;
|
||||
|
||||
use crate::parser::StateOsc72;
|
||||
|
||||
|
|
@ -195,7 +194,7 @@ impl DndEvent {
|
|||
}),
|
||||
b'r' => Self::DropArrive(DndDropArrive {
|
||||
idx: s.x?.try_into().ok()?,
|
||||
data: BASE64_SANE.decode(&s.payload).ok()?,
|
||||
data: STANDARD_NO_PAD_INDIFFERENT.decode(&s.payload).ok()?,
|
||||
}),
|
||||
b'R' => {
|
||||
let (name, desc) = parse_error(s.payload)?;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use std::{fmt::{self, Display}, str};
|
||||
|
||||
use base64::Engine;
|
||||
use yazi_shim::BASE64_SANE;
|
||||
use base64::{Engine, engine::general_purpose::STANDARD_NO_PAD_INDIFFERENT};
|
||||
|
||||
use super::traits::Mimelist;
|
||||
|
||||
|
|
@ -92,7 +91,7 @@ pub struct PresentDrag<'a>(pub u8, pub &'a [u8]);
|
|||
|
||||
impl Display for PresentDrag<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let b64 = BASE64_SANE.encode(self.1).into_bytes();
|
||||
let b64 = STANDARD_NO_PAD_INDIFFERENT.encode(self.1).into_bytes();
|
||||
let chunks = b64.len().div_ceil(4096);
|
||||
|
||||
for (i, chunk) in b64.chunks(4096).enumerate() {
|
||||
|
|
@ -120,7 +119,7 @@ pub struct PresentDragIcon<'a> {
|
|||
|
||||
impl Display for PresentDragIcon<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let b64 = BASE64_SANE.encode(self.data).into_bytes();
|
||||
let b64 = STANDARD_NO_PAD_INDIFFERENT.encode(self.data).into_bytes();
|
||||
let chunks = b64.len().div_ceil(4096);
|
||||
|
||||
for (i, chunk) in b64.chunks(4096).enumerate() {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@ impl Raterm {
|
|||
If(TMUX.get(), EnterAlternateScreen),
|
||||
PushKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES
|
||||
| PushKeyboardFlags::REPORT_ALTERNATE_KEYS
|
||||
| PushKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES,
|
||||
| PushKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES
|
||||
| PushKeyboardFlags::REPORT_ASSOCIATED_TEXT,
|
||||
EnableDrag(""),
|
||||
EnableDrop(&["text/uri-list"]),
|
||||
If(opt.mouse, EnableMouseCapture),
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ impl SizeCalculator {
|
|||
{
|
||||
let url = url.as_url();
|
||||
let cha = super::symlink_metadata(url).await?;
|
||||
Ok(if cha.is_dir() {
|
||||
Ok(if cha.is_dir() && !cha.is_indirect() {
|
||||
Self::Dir(VecDeque::from([Either::Left(url.to_owned())]), cha)
|
||||
} else {
|
||||
Self::File(Some(cha.len), cha)
|
||||
|
|
@ -77,10 +77,10 @@ impl SizeCalculator {
|
|||
pop_and_continue!();
|
||||
};
|
||||
|
||||
let Ok(ft) = ent.file_type().await else { continue };
|
||||
if ft.is_dir() {
|
||||
let Ok(cha) = ent.metadata().await else { continue };
|
||||
if cha.is_dir() && !cha.is_indirect() {
|
||||
buf.push_back(Either::Left(ent.url()));
|
||||
} else if let Ok(cha) = ent.metadata().await {
|
||||
} else {
|
||||
size += cha.len;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,6 +193,14 @@ impl<'a> Engine for Engines<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
async fn remove_dir_clean(&self) -> io::Result<()> {
|
||||
match self {
|
||||
Self::Local(p) => p.remove_dir_clean().await,
|
||||
Self::Lua(p) => p.remove_dir_clean().await,
|
||||
Self::Sftp(p) => p.remove_dir_clean().await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_file(&self) -> io::Result<()> {
|
||||
match self {
|
||||
Self::Local(p) => p.remove_file().await,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue