feat: update ui experience #1

Merged
sisungo merged 1 commits from sisungo/packie:master into master 2026-08-09 22:24:50 +08:00
21 changed files with 464 additions and 43 deletions
+2
View File
@@ -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"
+78
View File
@@ -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
)
);
}
};
}
}
}
}
+38 -3
View File
@@ -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<Item = impl AsRef<str>>) {
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()
}
+15 -6
View File
@@ -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!(
"{}: {}",
+1 -1
View File
@@ -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(())
}
+15 -4
View File
@@ -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<String>,
}
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(),
+41 -2
View File
@@ -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<String>,
}
impl<E: ToString> From<E> 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,
}
};
}
+1 -1
View File
@@ -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..]),
+7 -4
View File
@@ -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<PkgSpec>,
}
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));
}
+1 -1
View File
@@ -25,7 +25,7 @@ pub struct Cli {
clone: Option<String>,
}
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| {
+69
View File
@@ -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::<anyhow::Error>(|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(())
}
+8 -2
View File
@@ -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);
+16 -1
View File
@@ -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}"
+93 -1
View File
@@ -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で管理されているリンクではありません。手動で削除してください。"
+5 -3
View File
@@ -163,11 +163,13 @@ pub struct CopyError {
#[cfg(target_family = "unix")]
#[derive(Debug)]
pub struct LockGuard(Option<nix::fcntl::Flock<std::fs::File>>);
pub struct LockGuard {
_inner: Option<nix::fcntl::Flock<std::fs::File>>,
}
#[cfg(target_family = "unix")]
impl LockGuard {
pub fn noop() -> Self {
Self(None)
Self { _inner: None }
}
pub fn open(path: impl AsRef<Path>, nonblocking: bool) -> std::io::Result<Self> {
@@ -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) })
}
}
+5
View File
@@ -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)]
+20 -1
View File
@@ -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<Path>, to: impl AsRef<Path>) -> 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<PathBuf>) -> 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(())
}
+18
View File
@@ -38,6 +38,13 @@ impl super::Packie {
.unique_by(|x| x.pkg_manifest.pkg_ident())
.collect())
}
pub fn for_each_installation<E: From<rusqlite::Error>>(
&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<E: From<rusqlite::Error>>(
&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<bool> {
self
.0
+8
View File
@@ -152,6 +152,14 @@ impl FromStr for PkgSpec {
type Err = crate::version::VersionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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;
+18
View File
@@ -10,6 +10,7 @@ pub struct Download<P> {
to: PathBuf,
on_progress: P,
no_existence_check: bool,
checksum_required: Option<String>,
}
impl Download<fn(u64, u64)> {
pub fn new(from: String, to: PathBuf) -> Self {
@@ -18,6 +19,7 @@ impl Download<fn(u64, u64)> {
to,
no_existence_check: false,
on_progress: |_, _| (),
checksum_required: None,
}
}
}
@@ -28,6 +30,7 @@ impl<P> Download<P> {
to: self.to,
no_existence_check: self.no_existence_check,
on_progress,
checksum_required: self.checksum_required,
}
}
@@ -35,6 +38,11 @@ impl<P> Download<P> {
self.no_existence_check = true;
self
}
pub fn checksum_required(mut self, val: String) -> Self {
self.checksum_required = Some(val);
self
}
}
impl<P: FnMut(u64, u64)> Download<P> {
pub fn run(&mut self) -> Result<(), DownloadError> {
@@ -77,6 +85,16 @@ impl<P: FnMut(u64, u64)> Download<P> {
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(())
+5 -13
View File
@@ -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<E: FnMut(SyncEvent)> 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<E: FnMut(SyncEvent)> 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<String>, Option<Url>, RepoError),
}