feat: add cli packie repo-admin

Signed-off-by: sisungo <[email protected]>
This commit is contained in:
2026-06-17 20:11:25 +08:00
parent 8b042a3e8e
commit e8d2c85a69
6 changed files with 215 additions and 153 deletions
+5
View File
@@ -4,6 +4,7 @@ mod initdb;
mod install;
mod print;
mod remove;
mod repo_admin;
use clap::Parser;
use rust_i18n::t;
@@ -36,6 +37,9 @@ enum Subcommand {
/// Remove one or more packages
Remove(remove::Cli),
/// Administrate repository service
RepoAdmin(repo_admin::Cli),
}
fn main() {
@@ -57,6 +61,7 @@ fn main() {
Subcommand::Install(cli) => install::main(cli),
Subcommand::Print(cli) => print::main(cli),
Subcommand::Remove(cli) => remove::main(cli),
Subcommand::RepoAdmin(cli) => repo_admin::main(cli),
};
if let Err(err) = result {
eprintln!("{}", t!("main.error", error = err));
+24
View File
@@ -0,0 +1,24 @@
use anyhow::anyhow;
use clap::Parser;
use packie::repo::RepoServeDir;
use std::path::PathBuf;
#[derive(Debug, Parser)]
pub struct Cli {
/// Path of the repository serve directory
repo: PathBuf,
/// Add package(s) to the repository
#[arg(long, short)]
add_package: Option<Vec<PathBuf>>,
}
pub fn main(cli: Cli) -> anyhow::Result<()> {
let mut repo = RepoServeDir::open(cli.repo)?;
for add_package in cli.add_package.into_iter().flatten() {
repo.add_package(add_package).map_err(|x| anyhow!("{x}"))?;
}
Ok(())
}
+3
View File
@@ -32,3 +32,6 @@ repo.RepoError.NoSuchPackage:
repo.RepoError.AllTriesFailed:
en: "all tries failed"
zh-CN: "所有尝试均失败"
repo.download.DownloadError.Checksum:
en: "checksum failed"
zh-CN: "文件完整性检查失败"
+91
View File
@@ -0,0 +1,91 @@
use rust_i18n::t;
use std::{
fs::File,
io::{Read, Seek, Write},
path::PathBuf,
};
pub struct Download<P> {
from: String,
to: PathBuf,
on_progress: P,
}
impl Download<fn(u64, u64)> {
pub fn new(from: String, to: PathBuf) -> Self {
Self {
from,
to,
on_progress: |_, _| (),
}
}
}
impl<P> Download<P> {
pub fn on_progress<P2>(self, on_progress: P2) -> Download<P2> {
Download {
from: self.from,
to: self.to,
on_progress,
}
}
}
impl<P: FnMut(u64, u64)> Download<P> {
pub fn run(&mut self) -> Result<(), DownloadError> {
if self.to.exists() {
return Ok(());
}
let mut temp_file_path = self.to.clone();
temp_file_path.add_extension("tmp");
let mut temp_file = File::options()
.create(true)
.append(true)
.open(&temp_file_path)
.map_err(DownloadError::Filesystem)?;
let seek = temp_file
.seek(std::io::SeekFrom::Current(0))
.map_err(DownloadError::Filesystem)?;
let mut resp = ureq::get(&self.from)
.header(ureq::http::header::RANGE, format!("bytes={seek}-"))
.call()
.map_err(|e| DownloadError::Http(Box::new(e)))?;
let body = resp.body_mut();
let total = body.content_length().unwrap_or_default() + seek;
let mut sum = seek;
let mut buf = [0; 1024];
let mut body_reader = body.as_reader();
loop {
let len = body_reader
.read(&mut buf)
.map_err(|e| DownloadError::Http(Box::new(e)))?;
if len == 0 {
break;
}
temp_file
.write_all(&buf[..len])
.map_err(DownloadError::Filesystem)?;
sum += len as u64;
(self.on_progress)(sum, total);
}
std::fs::rename(temp_file_path, &self.to).map_err(DownloadError::Filesystem)?;
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum DownloadError {
#[error("INVALID_URL")]
InvalidUrl,
#[error("{0}")]
Http(Box<dyn std::error::Error + Send + Sync>),
#[error("{0}")]
Filesystem(std::io::Error),
#[error("{}", t!("repo.download.DownloadError.Checksum"))]
Checksum,
}
+41 -153
View File
@@ -1,16 +1,21 @@
mod download;
mod serve;
#[doc(inline)]
pub use download::DownloadError;
#[doc(inline)]
pub use serve::RepoServeDir;
use crate::{
Packie,
common::checksum::DEFAULT_HASHER,
package::{Package, PkgIdent, PkgManifest},
package::{PkgIdent, PkgManifest},
};
use download::Download;
use rusqlite::{OpenFlags, Row, params};
use rust_i18n::t;
use serde::{Deserialize, Serialize};
use std::{
fs::File,
io::{Read, Seek, Write},
path::{Path, PathBuf},
};
use std::path::{Path, PathBuf};
use url::Url;
impl super::Packie {
@@ -40,77 +45,42 @@ impl super::Packie {
let download_path = repo_cache_dir.join(&standard_filename);
for repo_url in repo.urls() {
match download_package_url(repo_url, &repo_cache_dir, &standard_filename, &mut on_event)
{
Ok(()) => {
return Ok(download_path);
}
Err(err) => {
let download_url = match repo_url.join(&standard_filename) {
Ok(x) => x,
Err(_) => {
on_event(DownloadPackageEvent::Error(
repo_url.clone(),
RepoError::Download(err),
RepoError::Download(DownloadError::InvalidUrl),
));
continue;
}
};
let download_result = Download::new(download_url.to_string(), download_path.clone())
.on_progress(|sum, total| {
on_event(DownloadPackageEvent::Progress(repo_url.clone(), sum, total))
})
.run();
if let Err(err) = download_result {
on_event(DownloadPackageEvent::Error(
download_url.clone(),
RepoError::Download(err),
));
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);
}
todo!()
}
}
fn download_package_url(
repo_url: &Url,
repo_cache_dir: &Path,
filename: &str,
on_event: &mut dyn FnMut(DownloadPackageEvent),
) -> Result<(), DownloadError> {
let download_url = repo_url
.join(filename)
.map_err(|_| DownloadError::InvalidUrl)?;
let download_path = repo_cache_dir.join(filename);
Download::new(download_url.to_string(), download_path)
.on_progress(|sum, total| {
on_event(DownloadPackageEvent::Progress(repo_url.clone(), sum, total))
})
.run()?;
Ok(())
}
#[derive(Debug)]
pub struct RepoServeDir {
path: PathBuf,
db: RepoDb,
}
impl RepoServeDir {
pub fn open<P: Into<PathBuf>>(path: P) -> rusqlite::Result<Self> {
let path = path.into();
Ok(Self {
db: RepoDb::open_rw(&path)?,
path,
})
}
pub fn add<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Box<dyn std::error::Error>> {
let path = path.as_ref();
let package = Package::open(path)?;
let pkg_manifest = package.manifest()?;
let download_size = std::fs::metadata(path)?.len();
let installed_size = package.installed_size()?;
let checksum = crate::common::checksum::hash_file(DEFAULT_HASHER, path)?;
let repo_package = RepoPackage {
pkg_manifest,
download_size,
installed_size,
checksum,
};
let standard_filename =
format!("{}.pkg", repo_package.pkg_manifest.pkg_ident().to_string());
std::fs::copy(path, self.path.join(standard_filename))?;
self.db.insert_package(&repo_package)?;
Ok(())
Err(RepoError::AllTriesFailed)
}
}
@@ -330,85 +300,3 @@ pub enum RepoError {
#[error("{}", t!("repo.RepoError.AllTriesFailed"))]
AllTriesFailed,
}
#[derive(Debug, thiserror::Error)]
pub enum DownloadError {
#[error("INVALID_URL")]
InvalidUrl,
#[error("{0}")]
Http(Box<dyn std::error::Error + Send + Sync>),
#[error("{0}")]
Filesystem(std::io::Error),
}
pub struct Download<P> {
from: String,
to: PathBuf,
on_progress: P,
}
impl Download<fn(u64, u64)> {
pub fn new(from: String, to: PathBuf) -> Self {
Self {
from,
to,
on_progress: |_, _| (),
}
}
}
impl<P> Download<P> {
pub fn on_progress<P2>(self, on_progress: P2) -> Download<P2> {
Download {
from: self.from,
to: self.to,
on_progress,
}
}
}
impl<P: FnMut(u64, u64)> Download<P> {
pub fn run(&mut self) -> Result<(), DownloadError> {
if self.to.exists() {
return Ok(());
}
let mut temp_file_path = self.to.clone();
temp_file_path.add_extension("tmp");
let mut temp_file = File::options()
.create(true)
.append(true)
.open(&temp_file_path)
.map_err(DownloadError::Filesystem)?;
let seek = temp_file
.seek(std::io::SeekFrom::Current(0))
.map_err(DownloadError::Filesystem)?;
let mut resp = ureq::get(&self.from)
.header(ureq::http::header::RANGE, format!("bytes={seek}-"))
.call()
.map_err(|e| DownloadError::Http(Box::new(e)))?;
let body = resp.body_mut();
let total = body.content_length().unwrap_or_default() + seek;
let mut sum = seek;
let mut buf = [0; 1024];
let mut body_reader = body.as_reader();
loop {
let len = body_reader
.read(&mut buf)
.map_err(|e| DownloadError::Http(Box::new(e)))?;
if len == 0 {
break;
}
temp_file
.write_all(&buf[..len])
.map_err(DownloadError::Filesystem)?;
sum += len as u64;
(self.on_progress)(sum, total);
}
std::fs::rename(temp_file_path, &self.to).map_err(DownloadError::Filesystem)?;
Ok(())
}
}
+51
View File
@@ -0,0 +1,51 @@
use crate::{
common::checksum::DEFAULT_HASHER,
package::Package,
repo::{RepoDb, RepoPackage},
};
use std::path::{Path, PathBuf};
/// A helper type to deal with repository serve directories.
#[derive(Debug)]
pub struct RepoServeDir {
path: PathBuf,
db: RepoDb,
}
impl RepoServeDir {
/// Open a directory.
pub fn open<P: Into<PathBuf>>(path: P) -> rusqlite::Result<Self> {
let path = path.into();
Ok(Self {
db: RepoDb::open_rw(&path)?,
path,
})
}
/// Add a package to the repository.
pub fn add_package<P: AsRef<Path>>(
&mut self,
path: P,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let path = path.as_ref();
let package = Package::open(path)?;
let pkg_manifest = package.manifest()?;
let download_size = std::fs::metadata(path)?.len();
let installed_size = package.installed_size()?;
let checksum = crate::common::checksum::hash_file(DEFAULT_HASHER, path)?;
let repo_package = RepoPackage {
pkg_manifest,
download_size,
installed_size,
checksum,
};
let standard_filename =
format!("{}.pkg", repo_package.pkg_manifest.pkg_ident().to_string());
std::fs::copy(path, self.path.join(standard_filename))?;
self.db.insert_package(&repo_package)?;
Ok(())
}
}