feat: add cli packie repo-admin
Signed-off-by: sisungo <[email protected]>
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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: "文件完整性检查失败"
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user