feat: more decent package locking mechanism (#2168)

This commit is contained in:
三咲雅 · Misaki Masa 2025-01-08 16:03:40 +08:00 committed by GitHub
parent 7dd4454bf1
commit 2eed881f12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 346 additions and 111 deletions

12
Cargo.lock generated
View file

@ -2770,6 +2770,15 @@ dependencies = [
"windows",
]
[[package]]
name = "twox-hash"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7b17f197b3050ba473acf9181f7b1d3b66d1cf7356c6cc57886662276e65908"
dependencies = [
"rand",
]
[[package]]
name = "typeid"
version = "1.0.2"
@ -3364,11 +3373,11 @@ dependencies = [
"clap_complete_fig",
"clap_complete_nushell",
"crossterm",
"md-5",
"serde",
"serde_json",
"tokio",
"toml",
"twox-hash",
"vergen-gitcl",
"yazi-boot",
"yazi-dds",
@ -3537,6 +3546,7 @@ dependencies = [
"tokio-stream",
"tokio-util",
"tracing",
"twox-hash",
"unicode-width 0.2.0",
"uzers",
"yazi-adapter",

View file

@ -32,6 +32,8 @@ shell-words = "1.1.0"
tokio = { version = "1.42.0", features = [ "full" ] }
tokio-stream = "0.1.17"
tokio-util = "0.7.13"
toml = { version = "0.8.19" }
tracing = { version = "0.1.41", features = [ "max_level_debug", "release_max_level_debug" ] }
twox-hash = { version = "2.1.0", default-features = false, features = [ "std", "random", "xxhash3_128" ] }
unicode-width = "0.2.0"
uzers = "0.12.1"

View file

@ -1 +1 @@
{"flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","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","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp"," Überzug"," Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","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","Sysinfo","ffprobe","vframes","luma","obase","outln","errln","tmtheme"],"version":"0.2","language":"en"}
{"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","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","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp"," Überzug"," Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","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","Sysinfo","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox"],"version":"0.2","language":"en","flagWords":[]}

View file

@ -19,11 +19,11 @@ yazi-shared = { path = "../yazi-shared", version = "0.4.3" }
anyhow = { workspace = true }
clap = { workspace = true }
crossterm = { workspace = true }
md-5 = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
toml = { version = "0.8.19" }
toml = { workspace = true }
twox-hash = { workspace = true }
[build-dependencies]
yazi-shared = { path = "../yazi-shared", version = "0.4.3" }

View file

@ -66,6 +66,9 @@ pub(super) struct CommandPack {
/// Upgrade all packages.
#[arg(short = 'u', long)]
pub(super) upgrade: bool,
/// Migrate all packages.
#[arg(short = 'm', long)]
pub(super) migrate: bool, // TODO: remove this
}
#[derive(clap::Args)]

View file

@ -62,7 +62,10 @@ async fn run() -> anyhow::Result<()> {
Command::Pack(cmd) => {
package::init()?;
if cmd.install {
package::Package::load().await?.sync().await.ok();
if cmd.migrate {
outln!("Migration successful")?;
} else if cmd.install {
package::Package::load().await?.install(false).await?;
} else if cmd.list {
package::Package::load().await?.print()?;

View file

@ -1,63 +1,36 @@
use std::{borrow::Cow, io::BufWriter, path::PathBuf};
use std::{io::BufWriter, path::PathBuf, str::FromStr};
use anyhow::Result;
use md5::{Digest, Md5};
use anyhow::{Result, bail};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use twox_hash::XxHash3_128;
use yazi_fs::Xdg;
#[derive(Default)]
pub(crate) struct Dependency {
pub(crate) repo: String,
pub(crate) child: String,
pub(crate) rev: String,
pub(crate) use_: String, // owner/repo:child
pub(crate) name: String, // child.yazi
pub(crate) parent: String, // owner/repo
pub(crate) child: String, // child
pub(crate) rev: String,
pub(crate) hash: String,
pub(super) is_flavor: bool,
}
impl Dependency {
pub(super) fn new(url: &str, rev: Option<&str>) -> Self {
let mut parts = url.splitn(2, ':');
let mut repo = parts.next().unwrap_or_default().to_owned();
let child = if let Some(s) = parts.next() {
format!("{s}.yazi")
} else {
repo.push_str(".yazi");
String::new()
};
Self { repo, child, rev: rev.unwrap_or_default().to_owned(), is_flavor: false }
}
#[inline]
pub(super) fn use_(&self) -> Cow<str> {
if self.child.is_empty() {
self.repo.trim_end_matches(".yazi").into()
} else {
format!("{}:{}", self.repo, self.child.trim_end_matches(".yazi")).into()
}
}
#[inline]
pub(super) fn name(&self) -> Option<&str> {
let s = if self.child.is_empty() {
self.repo.split('/').last().filter(|s| !s.is_empty())
} else {
Some(self.child.as_str())
};
s.filter(|s| s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-' | b'.')))
}
#[inline]
pub(super) fn local(&self) -> PathBuf {
Xdg::state_dir()
.join("packages")
.join(format!("{:x}", Md5::new_with_prefix(self.remote()).finalize()))
.join(format!("{:x}", XxHash3_128::oneshot(self.remote().as_bytes())))
}
#[inline]
pub(super) fn remote(&self) -> String {
// Support more Git hosting services in the future
format!("https://github.com/{}.git", self.repo)
format!("https://github.com/{}.git", self.parent)
}
pub(super) fn header(&self, s: &str) -> Result<()> {
@ -69,7 +42,7 @@ impl Dependency {
SetAttributes(Attribute::Reverse.into()),
SetAttributes(Attribute::Bold.into()),
Print(" "),
Print(s.replacen("{name}", self.name().unwrap_or_default(), 1)),
Print(s.replacen("{name}", &self.name, 1)),
Print(" "),
SetAttributes(Attribute::Reset.into()),
Print("\n\n"),
@ -78,6 +51,34 @@ impl Dependency {
}
}
impl FromStr for Dependency {
type Err = anyhow::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut parts = s.splitn(2, ':');
let Some(parent) = parts.next() else { bail!("Package url cannot be empty") };
let child = parts.next().unwrap_or_default();
let Some((_, repo)) = parent.split_once('/') else {
bail!("Package url `{parent}` must be in the format `owner/repo`")
};
let name = if child.is_empty() { repo } else { child };
if !name.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-')) {
bail!("Package name `{name}` must be in kebab-case")
}
Ok(Self {
use_: s.to_owned(),
name: format!("{name}.yazi"),
parent: format!("{parent}{}", if child.is_empty() { ".yazi" } else { "" }),
child: child.to_owned(),
..Default::default()
})
}
}
impl<'de> Deserialize<'de> for Dependency {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
@ -87,11 +88,18 @@ impl<'de> Deserialize<'de> for Dependency {
struct Shadow {
#[serde(rename = "use")]
use_: String,
rev: Option<String>,
#[serde(default)]
rev: String,
#[serde(default)]
hash: String,
}
let outer = Shadow::deserialize(deserializer)?;
Ok(Self::new(&outer.use_, outer.rev.as_deref()))
Ok(Self {
rev: outer.rev,
hash: outer.hash,
..Self::from_str(&outer.use_).map_err(serde::de::Error::custom)?
})
}
}
@ -103,11 +111,11 @@ impl Serialize for Dependency {
#[derive(Serialize)]
struct Shadow<'a> {
#[serde(rename = "use")]
use_: Cow<'a, str>,
rev: Option<&'a String>,
use_: &'a str,
rev: &'a str,
hash: &'a str,
}
Shadow { use_: self.use_(), rev: Some(&self.rev).filter(|&s| !s.is_empty()) }
.serialize(serializer)
Shadow { use_: &self.use_, rev: &self.rev, hash: &self.hash }.serialize(serializer)
}
}

View file

@ -2,48 +2,51 @@ use std::path::PathBuf;
use anyhow::{Context, Result, bail};
use tokio::fs;
use yazi_fs::{Xdg, maybe_exists, must_exists, remove_dir_clean};
use yazi_fs::{Xdg, copy_and_seal, maybe_exists, remove_dir_clean};
use yazi_macro::outln;
use super::Dependency;
const TRACKER: &str = "DO_NOT_MODIFY_ANYTHING_IN_THIS_DIRECTORY";
impl Dependency {
pub(super) async fn deploy(&mut self) -> Result<()> {
let Some(name) = self.name().map(ToOwned::to_owned) else { bail!("Invalid package url") };
let from = self.local().join(&self.child);
self.header("Deploying package `{name}`")?;
self.is_flavor = maybe_exists(&from.join("flavor.toml")).await;
let to = if self.is_flavor {
Xdg::config_dir().join(format!("flavors/{name}"))
Xdg::config_dir().join(format!("flavors/{}", self.name))
} else {
Xdg::config_dir().join(format!("plugins/{name}"))
Xdg::config_dir().join(format!("plugins/{}", self.name))
};
let tracker = to.join(TRACKER);
if maybe_exists(&to).await && !must_exists(&tracker).await {
if maybe_exists(&to).await && self.hash != self.hash().await? {
bail!(
"A user package with the same name `{name}` already exists.
For safety, please manually delete it from your plugin/flavor directory and re-run the command."
"The user has modified the contents of the `{}` package. For safety, the operation has been aborted.
Please manually delete it from your plugins/flavors directory and re-run the command.",
self.name
);
}
fs::create_dir_all(&to).await?;
fs::write(tracker, []).await?;
let files = if self.is_flavor {
&["flavor.toml", "tmtheme.xml", "README.md", "preview.png", "LICENSE", "LICENSE-tmtheme"][..]
} else {
// TODO: init.lua
&["init.lua", "README.md", "LICENSE"][..]
&["main.lua", "README.md", "LICENSE"][..]
};
for file in files {
let (from, to) = (from.join(file), to.join(file));
// TODO: remove this
let (from, to) = if *file == "main.lua" {
if maybe_exists(from.join(file)).await {
(from.join(file), to.join(file))
} else {
(from.join("init.lua"), to.join("main.lua"))
}
} else {
(from.join(file), to.join(file))
};
fs::copy(&from, &to)
copy_and_seal(&from, &to)
.await
.with_context(|| format!("failed to copy `{}` to `{}`", from.display(), to.display()))?;
}
@ -75,7 +78,7 @@ For safety, please manually delete it from your plugin/flavor directory and re-r
fs::create_dir_all(&to).await?;
while let Some(entry) = it.next_entry().await? {
let (src, dist) = (entry.path(), to.join(entry.file_name()));
fs::copy(&src, &dist).await.with_context(|| {
copy_and_seal(&src, &dist).await.with_context(|| {
format!("failed to copy `{}` to `{}`", src.display(), dist.display())
})?;
}

View file

@ -0,0 +1,57 @@
use anyhow::{Context, Result};
use tokio::fs;
use twox_hash::XxHash3_128;
use yazi_fs::{Xdg, ok_or_not_found};
use super::Dependency;
impl Dependency {
pub(crate) async fn hash(&self) -> Result<String> {
let dir = if self.is_flavor {
Xdg::config_dir().join(format!("flavors/{}", self.name))
} else {
Xdg::config_dir().join(format!("plugins/{}", self.name))
};
let files = if self.is_flavor {
&[
"LICENSE",
"LICENSE-tmtheme",
"README.md",
"filestyle.toml",
"flavor.toml",
"preview.png",
"tmtheme.xml",
][..]
} else {
&["LICENSE", "README.md", "main.lua"][..]
};
let mut hasher = XxHash3_128::new();
for file in files {
hasher.write(file.as_bytes());
hasher.write(b"VpvFw9Atb7cWGOdqhZCra634CcJJRlsRl72RbZeV0vpG1\0");
hasher.write(&ok_or_not_found(fs::read(dir.join(file)).await)?);
}
let mut assets = vec![];
match fs::read_dir(dir.join("assets")).await {
Ok(mut it) => {
while let Some(entry) = it.next_entry().await? {
assets.push((entry.file_name(), fs::read(entry.path()).await?));
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => Err(e).context(format!("failed to read `{}`", dir.join("assets").display()))?,
}
assets.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
for (name, data) in assets {
hasher.write(name.as_encoded_bytes());
hasher.write(b"pQU2in0xcsu97Y77Nuq2LnT8mczMlFj22idcYRmMrglqU\0");
hasher.write(&data);
}
Ok(format!("{:x}", hasher.finish_128()))
}
}

View file

@ -1,6 +1,6 @@
#![allow(clippy::module_inception)]
yazi_macro::mod_flat!(add dependency deploy git install package upgrade);
yazi_macro::mod_flat!(add dependency deploy git hash install package upgrade);
use anyhow::Context;
use yazi_fs::Xdg;

View file

@ -1,22 +1,22 @@
use std::path::PathBuf;
use std::{path::{Path, PathBuf}, str::FromStr};
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use tokio::fs;
use yazi_fs::Xdg;
use yazi_fs::{Xdg, create_and_seal, ok_or_not_found, unique_name};
use yazi_macro::outln;
use super::Dependency;
#[derive(Default)]
pub(crate) struct Package {
plugins: Vec<Dependency>,
flavors: Vec<Dependency>,
pub(crate) plugins: Vec<Dependency>,
pub(crate) flavors: Vec<Dependency>,
}
impl Package {
pub(crate) async fn load() -> Result<Self> {
Ok(match fs::read_to_string(Self::path()).await {
Ok(match fs::read_to_string(Self::toml()).await {
Ok(s) => toml::from_str(&s)?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Self::default(),
Err(e) => Err(e)?,
@ -24,14 +24,12 @@ impl Package {
}
pub(crate) async fn add(&mut self, use_: &str) -> Result<()> {
let mut dep = Dependency::new(use_, None);
let Some(name) = dep.name() else { bail!("Invalid package `use`") };
if self.plugins.iter().any(|d| d.repo == dep.repo && d.child == dep.child) {
bail!("Plugin `{name}` already exists in package.toml");
let mut dep = Dependency::from_str(use_)?;
if self.plugins.iter().any(|d| d.parent == dep.parent && d.child == dep.child) {
bail!("Plugin `{}` already exists in package.toml", dep.name);
}
if self.flavors.iter().any(|d| d.repo == dep.repo && d.child == dep.child) {
bail!("Flavor `{name}` already exists in package.toml");
if self.flavors.iter().any(|d| d.parent == dep.parent && d.child == dep.child) {
bail!("Flavor `{}` already exists in package.toml", dep.name);
}
dep.add().await?;
@ -41,7 +39,8 @@ impl Package {
self.plugins.push(dep);
}
Ok(fs::write(Self::path(), toml::to_string_pretty(self)?).await?)
let s = toml::to_string_pretty(self)?;
create_and_seal(&Self::toml(), s.as_bytes()).await.context("Failed to write package.toml")
}
pub(crate) async fn install(&mut self, upgrade: bool) -> Result<()> {
@ -61,33 +60,113 @@ impl Package {
}
let s = toml::to_string_pretty(self)?;
fs::write(Self::path(), s).await.context("Failed to write package.toml")
create_and_seal(&Self::toml(), s.as_bytes()).await.context("Failed to write package.toml")
}
pub(crate) fn print(&self) -> Result<()> {
outln!("Plugins:")?;
for d in &self.plugins {
if d.rev.is_empty() {
outln!("\t{}", d.use_())?;
outln!("\t{}", d.use_)?;
} else {
outln!("\t{} ({})", d.use_(), d.rev)?;
outln!("\t{} ({})", d.use_, d.rev)?;
}
}
outln!("Flavors:")?;
for d in &self.flavors {
if d.rev.is_empty() {
outln!("\t{}", d.use_())?;
outln!("\t{}", d.use_)?;
} else {
outln!("\t{} ({})", d.use_(), d.rev)?;
outln!("\t{} ({})", d.use_, d.rev)?;
}
}
Ok(())
}
// TODO: remove this
pub(crate) async fn sync(&mut self) -> Result<()> {
async fn make_readonly(p: &Path) -> Result<()> {
let mut perms = fs::metadata(p).await?.permissions();
perms.set_readonly(true);
fs::set_permissions(p, perms).await?;
Ok(())
}
match fs::read_dir(Xdg::config_dir().join("plugins")).await {
Ok(mut it) => {
while let Some(entry) = it.next_entry().await? {
let dir = entry.path();
if !dir.is_dir() || dir.extension().is_none_or(|s| s != "yazi") {
continue;
}
match fs::symlink_metadata(dir.join("init.lua")).await {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(e) => Err(e)?,
}
ok_or_not_found(
fs::rename(
dir.join("main.lua"),
unique_name(dir.join("main.lua.bak").into(), async { false }).await?,
)
.await,
)?;
ok_or_not_found(fs::rename(dir.join("init.lua"), dir.join("main.lua")).await)?;
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => Err(e)
.context(format!("failed to read `{}`", Xdg::config_dir().join("plugins").display()))?,
}
for d in &mut self.plugins {
let dir = Xdg::config_dir().join(format!("plugins/{}", d.name));
for f in ["LICENSE", "README.md", "main.lua"] {
make_readonly(&dir.join(f)).await.ok();
}
let tracker = dir.join("DO_NOT_MODIFY_ANYTHING_IN_THIS_DIRECTORY");
if fs::read(&tracker).await.is_ok_and(|v| v.is_empty()) {
if d.hash.is_empty() {
d.hash = d.hash().await?;
}
fs::remove_file(&tracker).await.ok();
}
}
for d in &mut self.flavors {
let dir = Xdg::config_dir().join(format!("flavors/{}", d.name));
for f in [
"LICENSE",
"LICENSE-tmtheme",
"README.md",
"filestyle.toml",
"flavor.toml",
"preview.png",
"tmtheme.xml",
] {
make_readonly(&dir.join(f)).await.ok();
}
let tracker = dir.join("DO_NOT_MODIFY_ANYTHING_IN_THIS_DIRECTORY");
if fs::read(&tracker).await.is_ok_and(|v| v.is_empty()) {
if d.hash.is_empty() {
d.hash = d.hash().await?;
}
fs::remove_file(&tracker).await.ok();
}
}
let s = toml::to_string_pretty(self)?;
create_and_seal(&Self::toml(), s.as_bytes()).await.context("Failed to write package.toml")
}
#[inline]
fn path() -> PathBuf { Xdg::config_dir().join("package.toml") }
fn toml() -> PathBuf { Xdg::config_dir().join("package.toml") }
}
impl<'de> Deserialize<'de> for Package {
@ -107,7 +186,9 @@ impl<'de> Deserialize<'de> for Package {
deps: Vec<Dependency>,
}
let outer = Outer::deserialize(deserializer)?;
let mut outer = Outer::deserialize(deserializer)?;
outer.flavor.deps.iter_mut().for_each(|d| d.is_flavor = true);
Ok(Self { plugins: outer.plugin.deps, flavors: outer.flavor.deps })
}
}

View file

@ -22,7 +22,7 @@ indexmap = { version = "2.7.0", features = [ "serde" ] }
ratatui = { workspace = true }
regex = { workspace = true }
serde = { workspace = true }
toml = { version = "0.8.19" }
toml = { workspace = true }
tracing = { workspace = true }
validator = { version = "0.19.0", features = [ "derive" ] }

View file

@ -1,7 +1,7 @@
use std::{borrow::Cow, collections::{HashMap, HashSet, VecDeque}, ffi::{OsStr, OsString}, path::{Path, PathBuf}};
use anyhow::{Result, bail};
use tokio::{fs, io, select, sync::{mpsc, oneshot}, time};
use tokio::{fs, io::{self, AsyncWriteExt}, select, sync::{mpsc, oneshot}, time};
use super::Cha;
@ -22,10 +22,10 @@ pub async fn must_be_dir(p: impl AsRef<Path>) -> bool {
}
#[inline]
pub fn ok_or_not_found(result: io::Result<()>) -> io::Result<()> {
pub fn ok_or_not_found<T: Default>(result: io::Result<T>) -> io::Result<T> {
match result {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Ok(t) => Ok(t),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(T::default()),
Err(_) => result,
}
}
@ -83,6 +83,24 @@ async fn _paths_to_same_file(a: &Path, b: &Path) -> std::io::Result<bool> {
Ok(final_name(a).await? == final_name(b).await?)
}
#[inline]
pub async fn copy_and_seal(from: &Path, to: &Path) -> io::Result<()> {
create_and_seal(to, &fs::read(from).await?).await
}
pub async fn create_and_seal(p: &Path, b: &[u8]) -> io::Result<()> {
ok_or_not_found(fs::remove_file(p).await)?;
let mut file = fs::OpenOptions::new().create_new(true).write(true).truncate(true).open(p).await?;
file.write_all(b).await?;
let mut perm = file.metadata().await?.permissions();
perm.set_readonly(true);
file.set_permissions(perm).await?;
Ok(())
}
pub async fn realname(p: &Path) -> Option<OsString> {
let name = p.file_name()?;
if p == fs::canonicalize(p).await.ok()? {

View file

@ -39,6 +39,7 @@ tokio = { workspace = true }
tokio-stream = { workspace = true }
tokio-util = { workspace = true }
tracing = { workspace = true }
twox-hash = { workspace = true }
unicode-width = { workspace = true }
yazi-prebuild = "0.1.2"

View file

@ -55,12 +55,32 @@ impl Loader {
return Ok(());
}
// TODO: init.lua
let p = BOOT.plugin_dir.join(format!("{name}.yazi/init.lua"));
let chunk =
fs::read(&p).await.with_context(|| format!("Failed to load plugin from {p:?}"))?.into();
// TODO: remove this
let p = BOOT.plugin_dir.join(format!("{name}.yazi/main.lua"));
let chunk = match fs::read(&p).await {
Ok(b) => b,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
yazi_proxy::AppProxy::notify(yazi_proxy::options::NotifyOpt {
title: "Deprecated entry file".to_owned(),
content: format!(
"The plugin entry file `init.lua` has been deprecated in v0.4.3 in favor of the new `main.lua`, and it will be fully removed in the next major version 0.5.
self.cache.write().insert(name.to_owned(), chunk);
Please run `ya pack -m` to automatically migrate all plugins, or manually rename your `{name}.yazi/init.lua` to `{name}.yazi/main.lua`."
),
level: yazi_proxy::options::NotifyLevel::Warn,
timeout: std::time::Duration::from_secs(25),
});
}
let p = BOOT.plugin_dir.join(format!("{name}.yazi/init.lua"));
fs::read(&p).await.with_context(|| format!("Failed to load plugin from {p:?}"))?
}
Err(e) => Err(e).with_context(|| format!("Failed to load plugin from {p:?}"))?,
};
self.cache.write().insert(name.to_owned(), chunk.into());
Ok(())
}

View file

@ -120,3 +120,22 @@ macro_rules! impl_file_methods {
});
};
}
#[macro_export]
macro_rules! deprecate {
($lua:ident, $tt:tt) => {{
static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
let id = match $lua.named_registry_value::<$crate::RtRef>("rt")?.current() {
Some(id) => &format!("`{id}.yazi` plugin"),
None => "`init.lua` config",
};
yazi_proxy::AppProxy::notify(yazi_proxy::options::NotifyOpt {
title: "Deprecated API".to_owned(),
content: format!($tt, id),
level: yazi_proxy::options::NotifyLevel::Warn,
timeout: std::time::Duration::from_secs(20),
});
}
}};
}

View file

@ -1,5 +1,5 @@
use md5::{Digest, Md5};
use mlua::{Function, Lua, Table};
use twox_hash::XxHash3_128;
use yazi_config::PREVIEW;
use super::Utils;
@ -14,9 +14,10 @@ impl Utils {
}
let hex = {
let mut digest = Md5::new_with_prefix(file.url.as_os_str().as_encoded_bytes());
digest.update(format!("//{:?}//{}", file.cha.mtime, t.raw_get("skip").unwrap_or(0)));
format!("{:x}", digest.finalize())
let mut h = XxHash3_128::new();
h.write(file.url.as_os_str().as_encoded_bytes());
h.write(format!("//{:?}//{}", file.cha.mtime, t.raw_get("skip").unwrap_or(0)).as_bytes());
format!("{:x}", h.finish_128())
};
Some(Url::cast(lua, PREVIEW.cache_dir.join(hex))).transpose()

View file

@ -2,15 +2,24 @@ use std::ops::ControlFlow;
use md5::{Digest, Md5};
use mlua::{Function, Lua, Table};
use twox_hash::XxHash3_128;
use unicode_width::UnicodeWidthChar;
use super::Utils;
use crate::CLIPBOARD;
impl Utils {
pub(super) fn hash(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|_, s: mlua::String| async move {
Ok(format!("{:x}", Md5::new_with_prefix(s.as_bytes()).finalize()))
pub(super) fn hash(lua: &Lua, deprecated: bool) -> mlua::Result<Function> {
lua.create_async_function(move |lua, s: mlua::String| async move {
if deprecated {
crate::deprecate!(
lua,
"The `ya.md5()` function is deprecated, please use `ya.hash()` instead, in your {}"
);
Ok(format!("{:x}", Md5::new_with_prefix(s.as_bytes()).finalize()))
} else {
Ok(format!("{:x}", XxHash3_128::oneshot(s.as_bytes().as_ref())))
}
})
}

View file

@ -53,8 +53,8 @@ pub fn compose(lua: &Lua, isolate: bool) -> mlua::Result<Table> {
b"target_family" => Utils::target_family(lua)?,
// Text
b"md5" => Utils::hash(lua)?, // TODO: deprecate this in the future
b"hash" => Utils::hash(lua)?,
b"md5" => Utils::hash(lua, true)?, // TODO: deprecate this in the future
b"hash" => Utils::hash(lua, false)?,
b"quote" => Utils::quote(lua)?,
b"truncate" => Utils::truncate(lua)?,
b"clipboard" => Utils::clipboard(lua)?,