mirror of
https://github.com/sxyazi/yazi.git
synced 2026-05-13 08:16:40 +00:00
feat: more decent package locking mechanism (#2168)
This commit is contained in:
parent
7dd4454bf1
commit
2eed881f12
19 changed files with 346 additions and 111 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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":[]}
|
||||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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()?;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
})?;
|
||||
}
|
||||
|
|
|
|||
57
yazi-cli/src/package/hash.rs
Normal file
57
yazi-cli/src/package/hash.rs
Normal 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()))
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" ] }
|
||||
|
||||
|
|
|
|||
|
|
@ -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()? {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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())))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)?,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue