diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 2dd2f64..5e5dd8c 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -9,6 +9,7 @@ path = "src/main.rs" [dependencies] anyhow = "1" +chrono = "0.4" clap = { version = "4", features = ["derive"] } console = "0.16" nix = { version = "0.31", features = ["sched"] } @@ -16,3 +17,4 @@ itertools = "0.15" packie = { path = "../" } rust-i18n = "4" indicatif = "0.18" +dialoguer = "0.12" diff --git a/cli/src/clean.rs b/cli/src/clean.rs new file mode 100644 index 0000000..d03406d --- /dev/null +++ b/cli/src/clean.rs @@ -0,0 +1,78 @@ +use clap::Parser; +use packie::PackieBuilder; +use rust_i18n::t; +use std::path::PathBuf; + +#[derive(Debug, Parser)] +pub struct Cli { + #[arg(short, long, default_value = "true")] + packages: bool, + + #[arg(short, long)] + verbose: bool, +} + +pub fn main(cli: Cli) -> Result<(), crate::Error> { + let packie = PackieBuilder::new().build()?; + + let repos_dir = packie.cache().repos_dir(); + + if cli.packages { + clean_packages(&cli, &repos_dir); + } + + Ok(()) +} + +fn clean_packages(cli: &Cli, repos_dir: &PathBuf) { + let read_repos_dir_errx = + |e: &std::io::Error| eprintln!("{}", t!("clean.warn_repos_dir", error = e)); + let Ok(repos) = std::fs::read_dir(&repos_dir).inspect_err(read_repos_dir_errx) else { + return; + }; + for repo in repos { + let Ok(repo) = repo.inspect_err(read_repos_dir_errx) else { + break; + }; + let read_repo_dir_errx = |e: &std::io::Error| { + eprintln!( + "{}", + t!( + "clean.warn_repo_dir", + name = repo.file_name().display().to_string(), + error = e + ) + ) + }; + let Ok(repo) = std::fs::read_dir(repo.path()).inspect_err(read_repo_dir_errx) else { + continue; + }; + for file in repo { + let Ok(file) = file.inspect_err(read_repo_dir_errx) else { + break; + }; + if file.file_name().as_encoded_bytes().ends_with(b".pkg") { + match std::fs::remove_file(file.path()) { + Ok(()) => { + if cli.verbose { + eprintln!( + "{}", + t!("clean.info_remove_file", path = file.path().display()) + ); + } + } + Err(err) => { + eprintln!( + "{}", + t!( + "clean.warn_remove_file", + path = file.path().display(), + error = err + ) + ); + } + }; + } + } + } +} diff --git a/cli/src/common.rs b/cli/src/common.rs index 0592d43..7f0eadc 100644 --- a/cli/src/common.rs +++ b/cli/src/common.rs @@ -1,4 +1,32 @@ -use console::Term; +use chrono::{Local, TimeZone}; +use console::{Term, style}; +use indicatif::ProgressStyle; +use rust_i18n::t; + +pub fn progress_style_multidownload() -> ProgressStyle { + let mut lines = Vec::with_capacity(3); + let (_, cols) = Term::stderr().size(); + + let short_first_line = format!("{{spinner}} {prefix}", prefix = style("{prefix}").green()); + let info_line = format!( + "{speed}{{bytes_per_sec}} {current}{{bytes}} {total}{{total_bytes}} {eta}{{eta}}", + speed = t!("common.progress_style.speed"), + current = t!("common.progress_style.current"), + total = t!("common.progress_style.total"), + eta = t!("common.progress_style.eta"), + ); + let long_first_line = format!("{short_first_line} {info_line}"); + if cols > 100 { + lines.push(&long_first_line[..]); + } else { + lines.push(&short_first_line[..]); + lines.push(&info_line[..]); + } + + lines.push("{wide_bar}"); + + ProgressStyle::with_template(&lines.join("\n")).unwrap() +} pub fn yesno(prompt: &str) -> bool { eprint!("{prompt} [y/N] "); @@ -14,11 +42,11 @@ pub fn yesno(prompt: &str) -> bool { } pub fn print_list(indent: u32, list: impl Iterator>) { - let (row, _) = Term::stdout().size(); + let (_, cols) = Term::stdout().size(); let mut line_used: usize = 0; for i in list { let mut i = i.as_ref().to_string(); - if line_used >= row as _ { + if line_used >= cols as _ { line_used = 0; println!(); } @@ -40,3 +68,10 @@ pub fn format_size(bytes: u64) -> String { _ => format!("{:.3} GB", (bytes as f64) / 1000000000.), } } + +pub fn format_datetime(timestamp: i64) -> String { + let Some(datetime) = Local.timestamp_opt(timestamp, 0).earliest() else { + return "?".into(); + }; + datetime.to_string() +} diff --git a/cli/src/info.rs b/cli/src/info.rs index 9d50322..869acaf 100644 --- a/cli/src/info.rs +++ b/cli/src/info.rs @@ -1,5 +1,4 @@ -use crate::common::format_size; -use anyhow::anyhow; +use crate::{common::format_size, error}; use clap::Parser; use packie::{ PackieBuilder, @@ -7,20 +6,30 @@ use packie::{ package::{PkgManifest, PkgSpec}, }; use rust_i18n::t; +use std::fmt::Write; #[derive(Debug, Parser)] pub struct Cli { pkgspec: PkgSpec, } -pub fn main(cli: Cli) -> anyhow::Result<()> { +pub fn main(cli: Cli) -> Result<(), crate::Error> { let mut packie = PackieBuilder::new().readonly(true).build()?; let mut found = packie.search_installation(&cli.pkgspec)?; if found.len() == 0 { - return Err(anyhow!("{}", t!("info.no_package_found"))); + return Err(error!("info.no_package_found")); } if found.len() != 1 { - return Err(anyhow!("{}", t!("info.pkgspec_not_unique"))); + let mut note = String::new(); + writeln!(&mut note, "{}", t!("info.note_pkgspec_not_unique")).unwrap(); + for i in found.iter() { + writeln!(&mut note, " - {}", i.pkg_manifest.pkg_ident()).unwrap(); + } + + return Err(crate::Error { + message: t!("info.error_pkgspec_not_unique").into(), + note: Some(note), + }); } let found = found.remove(0); @@ -72,7 +81,7 @@ fn print_install_misc(installation: &Installation) { println!( "{}: {}", t!("info.print_install_misc.install_date"), - installation.install_date, + crate::common::format_datetime(installation.install_date), ); println!( "{}: {}", diff --git a/cli/src/initdb.rs b/cli/src/initdb.rs index 489b188..b2d4801 100644 --- a/cli/src/initdb.rs +++ b/cli/src/initdb.rs @@ -4,7 +4,7 @@ use packie::PackieBuilder; #[derive(Debug, Parser)] pub struct Cli {} -pub fn main(cli: Cli) -> anyhow::Result<()> { +pub fn main(_: Cli) -> Result<(), crate::Error> { PackieBuilder::new().build()?; Ok(()) } diff --git a/cli/src/install.rs b/cli/src/install.rs index 77cde88..b2d80f3 100644 --- a/cli/src/install.rs +++ b/cli/src/install.rs @@ -1,5 +1,6 @@ use crate::common::{format_size, print_list, yesno}; use clap::Parser; +use console::style; use indicatif::ProgressBar; use itertools::Itertools; use packie::{ @@ -9,16 +10,19 @@ use rust_i18n::t; #[derive(Debug, Parser)] pub struct Cli { + /// Force the specified items to be files #[arg(short, long)] file: bool, + /// Don't require manual confirmation #[arg(short, long)] yes: bool, + /// Items to be installed items: Vec, } -pub fn main(cli: Cli) -> anyhow::Result<()> { +pub fn main(cli: Cli) -> Result<(), crate::Error> { let mut packie = PackieBuilder::new().build()?; if cli.file { for item in cli.items { @@ -37,7 +41,7 @@ pub fn main(cli: Cli) -> anyhow::Result<()> { .collect(); if packages.is_empty() { - eprintln!("No packages to install."); + eprintln!("{}", t!("install.no_package_to_install")); return Ok(()); } @@ -45,7 +49,12 @@ pub fn main(cli: Cli) -> anyhow::Result<()> { let installed_size: u64 = packages.iter().map(|x| x.repo_pkg.installed_size).sum(); println!("{}", t!("install.prompt_to_install")); - print_list(4, packages.iter().map(|x| x.pkgspec.to_string())); + print_list( + 4, + packages + .iter() + .map(|x| style(&x.pkgspec).green().to_string()), + ); println!( "{}", t!("install.download_size", size = format_size(download_size)) @@ -61,11 +70,13 @@ pub fn main(cli: Cli) -> anyhow::Result<()> { return Ok(()); } - let progress_bar = ProgressBar::new(download_size); + let progress_bar = + ProgressBar::new(download_size).with_style(crate::common::progress_style_multidownload()); let mut package_files = Vec::with_capacity(packages.len()); for package in packages.iter() { let init_pos = progress_bar.position(); progress_bar.println(format!("Downloading \"{}\"...", package.pkgspec)); + progress_bar.set_prefix(package.repo_pkg.pkg_manifest.pkg_ident().to_string()); let path = packie.download_package( &package.repo_name, &package.repo_pkg.pkg_manifest.pkg_ident(), diff --git a/cli/src/main.rs b/cli/src/main.rs index 39c216c..1f831fc 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,3 +1,4 @@ +mod clean; mod common; mod info; mod initdb; @@ -5,9 +6,11 @@ mod install; mod print; mod remove; mod repo_admin; +mod select; mod update; use clap::Parser; +use console::style; use rust_i18n::t; use std::path::{Path, PathBuf}; @@ -24,6 +27,9 @@ struct Cli { #[derive(Debug, Parser)] enum Subcommand { + /// Clean cache data + Clean(clean::Cli), + /// Get package information Info(info::Cli), @@ -44,11 +50,15 @@ enum Subcommand { /// Update repository indexes Update(update::Cli), + + /// Select global link + Select(select::Cli), } fn main() { rust_i18n::set_locale(match std::env::var("LANG").as_deref() { Ok("zh_CN.UTF-8") => "zh-CN", + Ok("ja_JP.UTF-8") => "ja", _ => "en", }); @@ -56,10 +66,11 @@ fn main() { if let Some(new_root) = cli.chroot && let Err(err) = chroot(&new_root) { - eprintln!("{}", t!("main.error", error = err)); + eprintln!("{}", style(t!("main.error", error = err)).red()); std::process::exit(1); } let result = match cli.subcommand { + Subcommand::Clean(cli) => clean::main(cli), Subcommand::Info(cli) => info::main(cli), Subcommand::InitDb(cli) => initdb::main(cli), Subcommand::Install(cli) => install::main(cli), @@ -67,9 +78,13 @@ fn main() { Subcommand::Remove(cli) => remove::main(cli), Subcommand::RepoAdmin(cli) => repo_admin::main(cli), Subcommand::Update(cli) => update::main(cli), + Subcommand::Select(cli) => select::main(cli), }; if let Err(err) = result { - eprintln!("{}", t!("main.error", error = err)); + eprintln!("{}", style(t!("main.error", error = err.message)).red()); + if let Some(note) = err.note { + eprintln!("{}", style(t!("main.note", note = note)).blue()); + } std::process::exit(1); } } @@ -89,3 +104,27 @@ fn chroot(new_root: &Path) -> std::io::Result<()> { std::env::set_current_dir("/")?; Ok(()) } + +#[derive(Debug)] +struct Error { + message: String, + note: Option, +} +impl From for Error { + fn from(value: E) -> Self { + Self { + message: value.to_string(), + note: None, + } + } +} + +#[macro_export] +macro_rules! error { + ($($all:tt)*) => { + $crate::Error { + message: ::rust_i18n::t!($($all)*).into(), + note: ::std::option::Option::None, + } + }; +} diff --git a/cli/src/print.rs b/cli/src/print.rs index d34daa7..95c0754 100644 --- a/cli/src/print.rs +++ b/cli/src/print.rs @@ -7,7 +7,7 @@ pub struct Cli { key: String, } -pub fn main(cli: Cli) -> anyhow::Result<()> { +pub fn main(cli: Cli) -> Result<(), crate::Error> { let key: Vec<&str> = cli.key.split('.').collect(); let result = match key.get(0).copied().unwrap() { "profile" => profile(&key[1..]), diff --git a/cli/src/remove.rs b/cli/src/remove.rs index 9b92fe4..5c2af02 100644 --- a/cli/src/remove.rs +++ b/cli/src/remove.rs @@ -1,4 +1,7 @@ -use crate::common::{print_list, yesno}; +use crate::{ + common::{print_list, yesno}, + error, +}; use anyhow::anyhow; use clap::Parser; use packie::{PackieBuilder, package::PkgSpec}; @@ -12,16 +15,16 @@ pub struct Cli { items: Vec, } -pub fn main(cli: Cli) -> anyhow::Result<()> { +pub fn main(cli: Cli) -> Result<(), crate::Error> { let mut packie = PackieBuilder::new().build()?; let mut packages = Vec::with_capacity(cli.items.len()); for pkgspec in cli.items { let mut found = packie.search_installation(&pkgspec)?; if found.is_empty() { - return Err(anyhow!("{}", t!("remove.package_not_found"))); + return Err(error!("remove.package_not_found")); } if found.len() != 1 { - return Err(anyhow!("{}", t!("remove.pkgspec_not_unique"))); + return Err(error!("remove.pkgspec_not_unique")); } packages.push(found.remove(0)); } diff --git a/cli/src/repo_admin.rs b/cli/src/repo_admin.rs index 473bb95..b3e8876 100644 --- a/cli/src/repo_admin.rs +++ b/cli/src/repo_admin.rs @@ -25,7 +25,7 @@ pub struct Cli { clone: Option, } -pub fn main(cli: Cli) -> anyhow::Result<()> { +pub fn main(cli: Cli) -> Result<(), crate::Error> { if let Some(clone) = cli.clone { let progress_bar = ProgressBar::new(0); RepoServeDir::clone(&clone.parse()?, &cli.repo, |pkg, url, sum, total| { diff --git a/cli/src/select.rs b/cli/src/select.rs new file mode 100644 index 0000000..c65cff4 --- /dev/null +++ b/cli/src/select.rs @@ -0,0 +1,69 @@ +use crate::error; +use clap::Parser; +use console::style; +use dialoguer::Select; +use packie::PackieBuilder; +use rust_i18n::t; +use std::path::PathBuf; + +#[derive(Debug, Parser)] +pub struct Cli { + file: PathBuf, +} + +pub fn main(cli: Cli) -> Result<(), crate::Error> { + let mut packie = PackieBuilder::new().build()?; + let file = if cli.file.is_absolute() { + cli.file.clone() + } else { + std::env::current_dir()?.join(&cli.file) + }; + let mut all_pkgs = Vec::with_capacity(1024); + let mut candidates = Vec::new(); + packie.for_each_installation::(|inst| { + all_pkgs.push(inst.pkg_manifest.pkg_ident()); + Ok(()) + })?; + for pkgident in all_pkgs { + let links = packie.links_of_pkg(&pkgident); + let Ok(links) = links.iter() else { + return Ok(()); + }; + if let Some(link) = links + .filter_map(|x| x.ok()) + .find(|x| x.to == file.to_string_lossy()) + { + candidates.push((pkgident, link)); + } + } + if candidates.is_empty() { + return Err(crate::Error { + message: t!("select.empty_list").into(), + note: Some(t!("select.note_relative_path").into()), + }); + } + let items = std::iter::once(t!("select.disabled").into()) + .chain(candidates.iter().map(|x| x.0.to_string())) + .map(|x| style(x).green()); + let choice = Select::new() + .with_prompt(t!("select.prompt_select_package")) + .items(items) + .interact()?; + if choice == 0 { + if let Err(err) = packie::link::remove(&file) { + if err.kind() == std::io::ErrorKind::InvalidData { + return Err(error!("select.not_a_link")); + } + if err.kind() == std::io::ErrorKind::NotFound { + return Ok(()); + } + Err(err)?; + } + return Ok(()); + } + let (pkgident, linkdes) = candidates.remove(choice - 1); + _ = packie::link::remove(&file); + _ = packie::link::deactivate_by_description(&mut packie, &pkgident, &linkdes); + packie::link::activate_by_description(&mut packie, &pkgident, &linkdes)?; + Ok(()) +} diff --git a/cli/src/update.rs b/cli/src/update.rs index f856d27..1908ddf 100644 --- a/cli/src/update.rs +++ b/cli/src/update.rs @@ -1,16 +1,22 @@ use clap::Parser; use indicatif::ProgressBar; use packie::{PackieBuilder, repo::SyncEvent}; +use rust_i18n::t; #[derive(Debug, Parser)] pub struct Cli {} -pub fn main(cli: Cli) -> anyhow::Result<()> { +pub fn main(cli: Cli) -> Result<(), crate::Error> { let mut packie = PackieBuilder::new().build()?; - let progress_bar = ProgressBar::new(0); + let progress_bar = + ProgressBar::new(0).with_style(crate::common::progress_style_multidownload()); packie .sync_repo() .on_event(|ev| match ev { + SyncEvent::BeginRepo(repo) => { + progress_bar.set_prefix(repo.clone()); + progress_bar.println(t!("update.begin_repo", repo = repo)); + } SyncEvent::Progress(progress) => { progress_bar.set_length(progress.total_bytes); progress_bar.set_position(progress.downloaded_bytes); diff --git a/locales/libpackie/main.yml b/locales/libpackie/main.yml index e37babb..f54c5a9 100644 --- a/locales/libpackie/main.yml +++ b/locales/libpackie/main.yml @@ -2,45 +2,60 @@ _version: 2 common.CopyError: en: 'failed to copy from "%{src}" to "%{dst}": %{error}' zh-CN: '无法将 "%{src}" 复制到 "%{dst}":%{error}' + ja: '"%{src}" から "%{dst}" へのコピーに失敗しました:%{error}' install.InstallError.Database: en: "local database error: %{error}" zh-CN: "本地数据库错误:%{error}" + ja: "ローカルデータベースエラー:%{error}" install.InstallError.NotFound: en: "item not found: %{item}" zh-CN: "找不到项目:%{item}" + ja: "項目が見つかりません:%{item}" install.InstallError.DependencyRing: en: "dependency ring detected" zh-CN: "检测到依赖环" + ja: "依存関係の循環が検出されました" remove.RemoveError.NotUniquePkgSpec: en: "not unique package spec" zh-CN: "指定的包范围匹配到了多个包" + ja: "パッケージ指定が一意ではありません" remove.RemoveError.Database: en: "local database error: %{error}" zh-CN: "本地数据库错误:%{error}" + ja: "ローカルデータベースエラー:%{error}" remove.RemoveError.NotFound: en: "package not found" zh-CN: "找不到包" + ja: "パッケージが見つかりません" remove.RemoveError.RemoveFiles: en: "failed to remove package files: %{error}" zh-CN: "无法删除包文件:%{error}" + ja: "パッケージファイルの削除に失敗しました:%{error}" install.InstallError.AlreadyInstalled: en: "package already installed" zh-CN: "此包已经安装" + ja: "パッケージはすでにインストールされています" repo.RepoError.Database: en: "repository database error: %{error}" - zh-CN: "软件仓库数据库错误:%{error}" + zh-CN: "软件包仓库数据库错误:%{error}" + ja: "リポジトリデータベースエラー:%{error}" repo.RepoError.NoSuchRepo: en: "no such repository" zh-CN: "找不到仓库" + ja: "指定されたリポジトリが存在しません" repo.RepoError.NoSuchPackage: en: "no such package" zh-CN: "找不到包" + ja: "指定されたパッケージが存在しません" repo.RepoError.AllTriesFailed: en: "all tries failed" zh-CN: "所有尝试均失败" + ja: "すべての試行に失敗しました" repo.download.DownloadError.Checksum: en: "checksum failed" zh-CN: "文件完整性检查失败" + ja: "チェックサム検証に失敗しました" BuildPackieError.Lock: en: "failed to lock packie database: %{error}" zh-CN: "无法锁定 Packie 数据库:%{error}" + ja: "Packie データベースのロックに失敗しました:%{error}" diff --git a/locales/packie-cli/main.yml b/locales/packie-cli/main.yml index f23fc75..d515940 100644 --- a/locales/packie-cli/main.yml +++ b/locales/packie-cli/main.yml @@ -2,72 +2,164 @@ _version: 2 main.error: en: "error: %{error}" zh-CN: "错误:%{error}" + ja: "エラー:%{error}" +main.note: + en: "note: %{note}" + zh-CN: "备注:%{note}" + ja: "備考:%{note}" +update.begin_repo: + en: "Synchronizing repository \"%{repo}\"..." + zh-CN: "正在同步软件包仓库 \"%{repo}\"..." + ja: "リポジトリ \"%{repo}\" を同期中..." install.prompt_to_install: en: "The following packages are to be installed:" zh-CN: "将要安装如下的包:" + ja: "以下のパッケージをインストールします:" install.download_size: en: "Download size: %{size}" zh-CN: "下载体积:%{size}" + ja: "ダウンロードサイズ:%{size}" install.installed_size: en: "Installed size: %{size}" zh-CN: "安装体积:%{size}" + ja: "インストール後サイズ:%{size}" install.confirm_installation: en: "Confirm to install?" zh-CN: "确认安装?" + ja: "インストールを続行しますか?" install.aborting: en: "Aborting." zh-CN: "中止。" + ja: "中止します。" +install.no_package_to_install: + en: "No packages to install." + zh-CN: "没有要安装的包。" + ja: "インストールするパッケージがありません。" info.no_package_found: en: "The specified package was not found." zh-CN: "找不到指定的包。" -info.pkgspec_not_unique: + ja: "指定されたパッケージが見つかりません。" +info.error_pkgspec_not_unique: en: "The specified package spec was not unique." zh-CN: "指定的包规范匹配了多个包。" + ja: "指定されたパッケージ指定が複数のパッケージに一致しました。" +info.note_pkgspec_not_unique: + en: "The following packages were matched:" + zh-CN: "匹配到了下面的包:" + ja: "以下のパッケージが一致しました:" info.print_pkgmanifest.name: en: "Package name" zh-CN: "包名" + ja: "パッケージ名" info.print_pkgmanifest.version: en: "Package version" zh-CN: "版本" + ja: "バージョン" info.print_pkgmanifest.arch: en: "Package architecture" zh-CN: "架构" + ja: "アーキテクチャ" info.print_pkgmanifest.dependencies: en: "Dependencies" zh-CN: "依赖" + ja: "依存関係" info.print_pkgmanifest.recommendations: en: "Recommendations" zh-CN: "建议安装" + ja: "推奨パッケージ" info.print_pkgmanifest.description: en: "Description" zh-CN: "描述" + ja: "説明" info.print_install_misc.install_pkgspec: en: "Install package spec" zh-CN: "安装请求" + ja: "インストール要求" info.print_install_misc.installed_size: en: "Installed size" zh-CN: "安装后大小" + ja: "インストール後サイズ" info.print_install_misc.install_date: en: "Install date" zh-CN: "安装日期" + ja: "インストール日時" remove.package_not_found: en: "The specified package was not found." zh-CN: "找不到指定的包。" + ja: "指定されたパッケージが見つかりません。" remove.pkgspec_not_unique: en: "The specified package spec was not unique." zh-CN: "指定的包规范匹配了多个包。" + ja: "指定されたパッケージ指定が複数のパッケージに一致しました。" remove.action_failure: en: "Failed to remove the specified package: %{error}" zh-CN: "无法移除指定的包:%{error}" + ja: "指定されたパッケージの削除に失敗しました:%{error}" remove.prompt_to_remove: en: "The following packages are to be removed:" zh-CN: "将要移除下面的包:" + ja: "以下のパッケージを削除します:" remove.confirm_removal: en: "Confirm to remove?" zh-CN: "确认移除?" + ja: "削除を続行しますか?" remove.aborting: en: "Aborting." zh-CN: "中止。" + ja: "中止します。" remove.action_hint: en: "Removing package %{package} ..." zh-CN: "正在移除包 %{package} ..." + ja: "パッケージ %{package} を削除中 ..." +common.progress_style.speed: + en: "Speed: " + zh-CN: "速度:" + ja: "速度:" +common.progress_style.current: + en: "Current: " + zh-CN: "已下载:" + ja: "現在:" +common.progress_style.total: + en: "Total: " + zh-CN: "总共:" + ja: "合計:" +common.progress_style.eta: + en: "ETA: " + zh-CN: "剩余时间:" + ja: "残り時間:" +clean.warn_repos_dir: + en: "warning: failed to read repository cache directory: %{error}" + zh-CN: "警告:无法读取软件包仓库缓存目录:%{error}" + ja: "警告:リポジトリキャッシュディレクトリの読み取りに失敗しました:%{error}" +clean.warn_repo_dir: + en: "warning: failed to read cache directory of repository \"%{name}\": %{error}" + zh-CN: "警告:无法读取软件包仓库 \"%{name}\" 的缓存目录:%{error}" + ja: "警告:リポジトリ \"%{name}\" のキャッシュディレクトリの読み取りに失敗しました:%{error}" +clean.info_remove_file: + en: "Removing file \"%{path}\"." + zh-CN: "正在删除文件 \"%{path}\"。" + ja: "ファイル \"%{path}\" を削除中。" +clean.warn_remove_file: + en: "warning: failed to remove file \"%{path}\": %{error}" + zh-CN: "警告:无法删除文件 \"%{path}\":%{error}" + ja: "警告:ファイル \"%{path}\" の削除に失敗しました:%{error}" +select.prompt_select_package: + en: "Select the package to use for the link" + zh-CN: "选择要为此链接使用的包" + ja: "リンクに使用するパッケージを選択してください" +select.empty_list: + en: "No package provided the given link" + zh-CN: "没有包提供要求的链接" + ja: "指定されたリンクを提供するパッケージはありません" +select.note_relative_path: + en: "A relative path is specified, and Packie resolves path based on your current directory. If this is not expected, provide an absolute path instead." + zh-CN: "指定了相对路径,Packie 会根据您当前目录解析路径。如果不符合预期,请改用绝对路径。" + ja: "相対パスが指定されています。Packieはカレントディレクトリに基づいてパスを解決します。意図と異なる場合は、絶対パスを指定してください。" +select.disabled: + en: "Disabled" + zh-CN: "禁用" + ja: "無効" +select.not_a_link: + en: "The existing file is not a link managed by Packie. Please remove it manually." + zh-CN: "已存在的文件不是一个被 Packie 管理的链接。请手动删除该文件。" + ja: "既存のファイルはPackieで管理されているリンクではありません。手動で削除してください。" diff --git a/src/common/fs.rs b/src/common/fs.rs index 88f0bf5..bbb3e15 100644 --- a/src/common/fs.rs +++ b/src/common/fs.rs @@ -163,11 +163,13 @@ pub struct CopyError { #[cfg(target_family = "unix")] #[derive(Debug)] -pub struct LockGuard(Option>); +pub struct LockGuard { + _inner: Option>, +} #[cfg(target_family = "unix")] impl LockGuard { pub fn noop() -> Self { - Self(None) + Self { _inner: None } } pub fn open(path: impl AsRef, nonblocking: bool) -> std::io::Result { @@ -181,6 +183,6 @@ impl LockGuard { nix::fcntl::FlockArg::LockExclusive }; let lock = nix::fcntl::Flock::lock(file, flags).map_err(|x| x.1)?; - Ok(Self(Some(lock))) + Ok(Self { _inner: Some(lock) }) } } diff --git a/src/lib.rs b/src/lib.rs index 417755c..39156cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,11 @@ impl Packie { pub fn profile(&self) -> &Profile { &self.profile } + + /// Returns cache manager for the Packie instance. + pub fn cache(&self) -> &Cache { + &self.cache + } } #[derive(Debug, Clone)] diff --git a/src/link.rs b/src/link.rs index f07b769..4998cdd 100644 --- a/src/link.rs +++ b/src/link.rs @@ -6,6 +6,12 @@ use std::{ path::{Path, PathBuf}, }; +impl super::Packie { + pub fn links_of_pkg(&self, pkgident: &PkgIdent) -> LinksFile { + LinksFile(self.local_data.links_file(pkgident)) + } +} + /// Iterator over link descriptions in a links file. #[derive(Debug)] pub struct LinksFile(pub PathBuf); @@ -73,14 +79,27 @@ pub fn create(from: impl AsRef, to: impl AsRef) -> std::io::Result<( Ok(()) } +/// Tries to remove a link at a given path. Also removes all its parent directories if they become empty after +/// this deletion. +/// +/// Note that this is not an atomic operation. Take care of TOCTOU and race condition. +/// +/// # Errors +/// It would return an standard library IO error in given error kind: +/// +/// - `NotFound`: The file could not be located. +/// - `InvalidData`: The file is not a link. pub fn remove(path: impl Into) -> std::io::Result<()> { let mut path = path.into(); + if !path.exists() { + return Err(std::io::ErrorKind::NotFound.into()); + } if std::fs::read_link(&path).is_err() { return Err(std::io::ErrorKind::InvalidData.into()); } std::fs::remove_file(&path)?; while path.pop() { - _ = std::fs::remove_dir(&path)?; + _ = std::fs::remove_dir(&path); } Ok(()) } diff --git a/src/local.rs b/src/local.rs index 462af2e..3452c95 100644 --- a/src/local.rs +++ b/src/local.rs @@ -38,6 +38,13 @@ impl super::Packie { .unique_by(|x| x.pkg_manifest.pkg_ident()) .collect()) } + + pub fn for_each_installation>( + &mut self, + f: impl FnMut(Installation) -> Result<(), E>, + ) -> Result<(), E> { + self.local_db.for_each_installation(f) + } } #[derive(Debug)] @@ -199,6 +206,17 @@ CREATE TABLE IF NOT EXISTS "abspkg"( Ok(abspkg) } + pub fn for_each_installation>( + &mut self, + mut f: impl FnMut(Installation) -> Result<(), E>, + ) -> Result<(), E> { + let mut stmt = self.0.prepare_cached("SELECT * FROM \"installation\"")?; + for i in stmt.query_map(params![], map_installation)? { + f(i?)?; + } + Ok(()) + } + pub fn remove_installation(&mut self, pkg_ident: &PkgIdent) -> rusqlite::Result { self .0 diff --git a/src/package.rs b/src/package.rs index 8809b3d..1151df4 100644 --- a/src/package.rs +++ b/src/package.rs @@ -152,6 +152,14 @@ impl FromStr for PkgSpec { type Err = crate::version::VersionError; fn from_str(s: &str) -> Result { + if let Ok(pkgident) = PkgIdent::from_str(s) { + return Ok(Self { + name: pkgident.name, + version: VersionFilter::Eq(pkgident.version), + arch: Some(pkgident.arch), + }); + } + let s = s.trim(); let mut name = String::new(); let mut version = None; diff --git a/src/repo/download.rs b/src/repo/download.rs index 8775d38..7a18713 100644 --- a/src/repo/download.rs +++ b/src/repo/download.rs @@ -10,6 +10,7 @@ pub struct Download

{ to: PathBuf, on_progress: P, no_existence_check: bool, + checksum_required: Option, } impl Download { pub fn new(from: String, to: PathBuf) -> Self { @@ -18,6 +19,7 @@ impl Download { to, no_existence_check: false, on_progress: |_, _| (), + checksum_required: None, } } } @@ -28,6 +30,7 @@ impl

Download

{ to: self.to, no_existence_check: self.no_existence_check, on_progress, + checksum_required: self.checksum_required, } } @@ -35,6 +38,11 @@ impl

Download

{ self.no_existence_check = true; self } + + pub fn checksum_required(mut self, val: String) -> Self { + self.checksum_required = Some(val); + self + } } impl Download

{ pub fn run(&mut self) -> Result<(), DownloadError> { @@ -77,6 +85,16 @@ impl Download

{ sum += len as u64; (self.on_progress)(sum, total); } + + if let Some(expected) = self.checksum_required.as_ref() { + let verified = + crate::common::checksum::verify_file(&temp_file_path, expected).unwrap_or_default(); + if !verified { + _ = std::fs::remove_file(&temp_file_path); + return Err(DownloadError::Checksum); + } + } + std::fs::rename(temp_file_path, &self.to).map_err(DownloadError::Filesystem)?; Ok(()) diff --git a/src/repo/mod.rs b/src/repo/mod.rs index 0a4bab7..888dd78 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -3,9 +3,6 @@ mod serve; #[doc(inline)] pub use download::DownloadError; - -use itertools::Itertools; -use rustc_hash::FxHashMap; #[doc(inline)] pub use serve::RepoServeDir; @@ -14,8 +11,10 @@ use crate::{ package::{AbsPkgIdent, PkgIdent, PkgManifest, PkgSpec}, }; use download::Download; +use itertools::Itertools; use rusqlite::{OpenFlags, Row, params}; use rust_i18n::t; +use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use url::Url; @@ -66,6 +65,7 @@ impl super::Packie { .on_progress(|sum, total| { on_event(DownloadPackageEvent::Progress(repo_url.clone(), sum, total)) }) + .checksum_required(repo_package.checksum.clone()) .run(); if let Err(err) = download_result { on_event(DownloadPackageEvent::Error( @@ -74,16 +74,6 @@ impl super::Packie { )); continue; } - let checksum_passed = - crate::common::checksum::verify_file(&download_path, &repo_package.checksum) - .unwrap_or_default(); - if !checksum_passed { - on_event(DownloadPackageEvent::Error( - download_url.clone(), - RepoError::Download(DownloadError::Checksum), - )); - continue; - } return Ok(download_path); } @@ -304,6 +294,7 @@ impl SyncSession<'_, E> { self.raise_error(Some(repo_name), None, RepoError::NoSuchRepo); continue; }; + (self.on_event)(SyncEvent::BeginRepo(repo_name.clone())); for url in repo.urls() { match self.sync_url(&repo_name, &url) { Ok(()) => break, @@ -351,6 +342,7 @@ impl SyncSession<'_, E> { /// An event during running [`SyncSession`], which may report progress changes or errors. #[derive(Debug)] pub enum SyncEvent { + BeginRepo(String), Progress(SyncProgress), Error(Option, Option, RepoError), }