forked from semios/packie
feat: add basic repository support
Signed-off-by: sisungo <[email protected]>
This commit is contained in:
+8
-1
@@ -4,22 +4,29 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = ["zstd", "gzip"]
|
||||
default = ["zstd", "gzip", "sha3"]
|
||||
zstd = ["dep:zstd"]
|
||||
gzip = ["dep:flate2"]
|
||||
sha3 = ["dep:sha3"]
|
||||
|
||||
[workspace]
|
||||
members = ["cli"]
|
||||
|
||||
[dependencies]
|
||||
const-hex = "1"
|
||||
digest = "0.11"
|
||||
flate2 = { version = "1", optional = true }
|
||||
rustc-hash = "2"
|
||||
rust-i18n = "4"
|
||||
rusqlite = { version = "0.40", features = ["bundled"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sha3 = { version = "0.12", optional = true }
|
||||
tar = "0.4"
|
||||
tempfile = "3"
|
||||
thiserror = "2"
|
||||
toml = "1"
|
||||
ureq = { version = "3", features = ["socks-proxy"] }
|
||||
url = { version = "2", features = ["serde"] }
|
||||
nix = { version = "0.31", features = ["fs"] }
|
||||
zstd = { version = "0.13", optional = true }
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ pub struct Cli {
|
||||
|
||||
pub fn main(cli: Cli) -> anyhow::Result<()> {
|
||||
let mut packie = PackieBuilder::new().readonly(true).build()?;
|
||||
let mut found = packie::installation::by_pkgspec(&mut packie, &cli.pkgspec)?;
|
||||
let mut found = packie.search_installation(&cli.pkgspec)?;
|
||||
if found.len() == 0 {
|
||||
return Err(anyhow!("no package found"));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod common;
|
||||
mod info;
|
||||
mod initdb;
|
||||
mod install;
|
||||
|
||||
+2
-1
@@ -10,7 +10,8 @@ pub struct Cli {
|
||||
pub fn main(cli: Cli) -> anyhow::Result<()> {
|
||||
let mut packie = PackieBuilder::new().build()?;
|
||||
for pkgspec in cli.pkgspecs {
|
||||
packie::remove::remove(&mut packie, &pkgspec)
|
||||
packie
|
||||
.remove(&pkgspec)
|
||||
.map_err(|err| anyhow!("failed to remove {pkgspec}: {err}"))?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
+26
-14
@@ -1,22 +1,34 @@
|
||||
_version: 2
|
||||
common.CopyError:
|
||||
en: 'failed to copy from "%{src}" to "%{dst}": %{error}'
|
||||
zh-CN: '无法将 "%{src}" 复制到 "%{dst}":%{error}'
|
||||
en: 'failed to copy from "%{src}" to "%{dst}": %{error}'
|
||||
zh-CN: '无法将 "%{src}" 复制到 "%{dst}":%{error}'
|
||||
install.InstallError.Database:
|
||||
en: 'local database error: %{error}'
|
||||
zh-CN: '本地数据库错误:%{error}'
|
||||
en: "local database error: %{error}"
|
||||
zh-CN: "本地数据库错误:%{error}"
|
||||
remove.RemoveError.NotUniquePkgSpec:
|
||||
en: 'not unique package spec'
|
||||
zh-CN: '指定的包范围匹配到了多个包'
|
||||
en: "not unique package spec"
|
||||
zh-CN: "指定的包范围匹配到了多个包"
|
||||
remove.RemoveError.Database:
|
||||
en: 'local database error: %{error}'
|
||||
zh-CN: '本地数据库错误:%{error}'
|
||||
en: "local database error: %{error}"
|
||||
zh-CN: "本地数据库错误:%{error}"
|
||||
remove.RemoveError.NotFound:
|
||||
en: 'package not found'
|
||||
zh-CN: '找不到包'
|
||||
en: "package not found"
|
||||
zh-CN: "找不到包"
|
||||
remove.RemoveError.RemoveFiles:
|
||||
en: 'failed to remove package files: %{error}'
|
||||
zh-CN: '无法删除包文件:%{error}'
|
||||
en: "failed to remove package files: %{error}"
|
||||
zh-CN: "无法删除包文件:%{error}"
|
||||
install.InstallError.AlreadyInstalled:
|
||||
en: 'package already installed'
|
||||
zh-CN: '此包已经安装'
|
||||
en: "package already installed"
|
||||
zh-CN: "此包已经安装"
|
||||
repo.RepoError.Database:
|
||||
en: "repository database error: %{error}"
|
||||
zh-CN: "软件仓库数据库错误:%{error}"
|
||||
repo.RepoError.NoSuchRepo:
|
||||
en: "no such repository"
|
||||
zh-CN: "找不到仓库"
|
||||
repo.RepoError.NoSuchPackage:
|
||||
en: "no such package"
|
||||
zh-CN: "找不到包"
|
||||
repo.RepoError.AllTriesFailed:
|
||||
en: "all tries failed"
|
||||
zh-CN: "所有尝试均失败"
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
use digest::{Digest, DynDigest};
|
||||
use std::{fs::File, io::Read, path::Path};
|
||||
|
||||
#[cfg(feature = "sha3")]
|
||||
pub const DEFAULT_HASHER: &str = "sha3-512";
|
||||
|
||||
pub fn find_hasher(s: &str) -> std::io::Result<Box<dyn DynDigest>> {
|
||||
match s {
|
||||
#[cfg(feature = "sha3")]
|
||||
"sha3-512" => Ok(Box::new(sha3::Sha3_512::new())),
|
||||
|
||||
_ => Err(std::io::ErrorKind::InvalidInput.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hash_reader<R: Read>(hasher_name: &str, reader: &mut R) -> std::io::Result<String> {
|
||||
let mut buf = [0u8; 1024];
|
||||
let mut hasher = find_hasher(hasher_name)?;
|
||||
loop {
|
||||
let n = reader.read(&mut buf)?;
|
||||
hasher.update(&buf[..n]);
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(format!(
|
||||
"{hasher_name}:{}",
|
||||
const_hex::encode(hasher.finalize()),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn verify_reader<R: Read>(reader: &mut R, expected: &str) -> std::io::Result<bool> {
|
||||
let hasher = expected.split(':').next().unwrap();
|
||||
let hash = hash_reader(hasher, reader)?;
|
||||
Ok(hash == expected)
|
||||
}
|
||||
|
||||
pub fn hash_file<P: AsRef<Path>>(hasher: &str, path: P) -> std::io::Result<String> {
|
||||
hash_reader(hasher, &mut File::open(path)?)
|
||||
}
|
||||
|
||||
pub fn verify_file<P: AsRef<Path>>(path: P, expected: &str) -> std::io::Result<bool> {
|
||||
verify_reader(&mut File::open(path)?, expected)
|
||||
}
|
||||
@@ -1,40 +1,10 @@
|
||||
use rust_i18n::t;
|
||||
use std::{
|
||||
fs::{FileType, ReadDir},
|
||||
io::{Read, Seek, SeekFrom},
|
||||
path::{Path, PathBuf},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
pub fn decompress(
|
||||
mut raw: impl Read + Seek + Send + Sync + 'static,
|
||||
) -> std::io::Result<Box<dyn Read + Send + Sync>> {
|
||||
#[cfg(feature = "zstd")]
|
||||
if stream_starts_with(&mut raw, &[0x28, 0xb5, 0x2f, 0xfd])? {
|
||||
return Ok(Box::new(zstd::Decoder::new(raw)?));
|
||||
}
|
||||
|
||||
#[cfg(feature = "gzip")]
|
||||
if stream_starts_with(&mut raw, &[0x1F, 0x8B])? {
|
||||
return Ok(Box::new(flate2::read::GzDecoder::new(raw)));
|
||||
}
|
||||
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"Unknown compression algorithm",
|
||||
))
|
||||
}
|
||||
|
||||
fn stream_starts_with(
|
||||
stream: &mut (impl Read + Seek + Send + Sync + 'static),
|
||||
magic: &[u8],
|
||||
) -> std::io::Result<bool> {
|
||||
let mut magic_buf = vec![0u8; magic.len()];
|
||||
stream.read_exact(&mut magic_buf)?;
|
||||
stream.seek(SeekFrom::Start(0))?;
|
||||
Ok(magic_buf == magic)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TreeDir {
|
||||
stack: Vec<(ReadDir, PathBuf)>,
|
||||
@@ -190,17 +160,3 @@ pub struct CopyError {
|
||||
dst: PathBuf,
|
||||
error: std::io::Error,
|
||||
}
|
||||
|
||||
pub fn timestamp_secs() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or_else(|e| -(e.duration().as_secs() as i64))
|
||||
}
|
||||
|
||||
pub fn timestamp_millis() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as i64)
|
||||
.unwrap_or_else(|e| -(e.duration().as_millis() as i64))
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
pub mod checksum;
|
||||
pub mod fs;
|
||||
|
||||
pub use fs::*;
|
||||
|
||||
use std::{
|
||||
io::{Read, Seek, SeekFrom},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
pub fn decompress(
|
||||
mut raw: impl Read + Seek + Send + Sync + 'static,
|
||||
) -> std::io::Result<Box<dyn Read + Send + Sync>> {
|
||||
#[cfg(feature = "zstd")]
|
||||
if stream_starts_with(&mut raw, &[0x28, 0xb5, 0x2f, 0xfd])? {
|
||||
return Ok(Box::new(zstd::Decoder::new(raw)?));
|
||||
}
|
||||
|
||||
#[cfg(feature = "gzip")]
|
||||
if stream_starts_with(&mut raw, &[0x1F, 0x8B])? {
|
||||
return Ok(Box::new(flate2::read::GzDecoder::new(raw)));
|
||||
}
|
||||
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"Unknown compression algorithm",
|
||||
))
|
||||
}
|
||||
|
||||
fn stream_starts_with(
|
||||
stream: &mut (impl Read + Seek + Send + Sync + 'static),
|
||||
magic: &[u8],
|
||||
) -> std::io::Result<bool> {
|
||||
let mut magic_buf = vec![0u8; magic.len()];
|
||||
stream.read_exact(&mut magic_buf)?;
|
||||
stream.seek(SeekFrom::Start(0))?;
|
||||
Ok(magic_buf == magic)
|
||||
}
|
||||
|
||||
pub fn timestamp_secs() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or_else(|e| -(e.duration().as_secs() as i64))
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
mod repo;
|
||||
|
||||
pub use repo::RepoEntry;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::path::Path;
|
||||
|
||||
/// A collection of all loaded Packie config files.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AllConfig {
|
||||
pub repos: FxHashMap<String, RepoEntry>,
|
||||
}
|
||||
impl AllConfig {
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> Self {
|
||||
let path = path.as_ref();
|
||||
|
||||
Self {
|
||||
repos: repo::read_repos(&path.join("repos.d")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct RepoConfig(FxHashMap<String, RepoEntry>);
|
||||
impl RepoConfig {
|
||||
pub fn read<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let s = std::fs::read_to_string(path)?;
|
||||
let result: Self = toml::from_str(&s)?;
|
||||
for ent in result.0.values() {
|
||||
if ent.url.is_some() && ent.urls.is_some() {
|
||||
return Err(Box::from("cannot specify both `url` and `urls`"));
|
||||
}
|
||||
if ent.url.is_none() && ent.urls.is_none() {
|
||||
return Err(Box::from("no url specified"));
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RepoEntry {
|
||||
url: Option<Url>,
|
||||
urls: Option<Vec<Url>>,
|
||||
}
|
||||
impl RepoEntry {
|
||||
pub fn urls(&self) -> &[Url] {
|
||||
self.urls
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| std::slice::from_ref(self.url.as_ref().unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_repos(repos_dir: &Path) -> FxHashMap<String, RepoEntry> {
|
||||
let mut repos = FxHashMap::default();
|
||||
if let Ok(tree_dir) = crate::common::TreeDir::new(repos_dir) {
|
||||
for ent in tree_dir {
|
||||
let Ok(ent) = ent else {
|
||||
continue;
|
||||
};
|
||||
if !ent.ty.is_file() {
|
||||
continue;
|
||||
}
|
||||
let Ok(repo_config) = RepoConfig::read(repos_dir.join(&ent.relpath)) else {
|
||||
continue;
|
||||
};
|
||||
for (key, val) in repo_config.0 {
|
||||
repos.insert(key, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
repos
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
Packie,
|
||||
installation::Installation,
|
||||
local::Installation,
|
||||
package::{Package, PackageError, PkgIdent, PkgSpec},
|
||||
};
|
||||
use rust_i18n::t;
|
||||
@@ -36,7 +36,8 @@ pub fn install_package(
|
||||
.unwrap_or_else(|| default_install_pkgspec(pkg_manifest.pkg_ident()));
|
||||
|
||||
// Check if the package is previously installed
|
||||
if !crate::installation::by_pkgspec(packie, &pkg_manifest.pkg_ident().into())
|
||||
if !packie
|
||||
.search_installation(&pkg_manifest.pkg_ident().into())
|
||||
.map_err(InstallError::Database)?
|
||||
.is_empty()
|
||||
{
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
use crate::{
|
||||
Packie,
|
||||
package::{PkgManifest, PkgSpec},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Installation {
|
||||
pub pkg_manifest: PkgManifest,
|
||||
pub install_pkgspec: PkgSpec,
|
||||
pub install_date: i64,
|
||||
pub update_date: i64,
|
||||
pub installed_size: u64,
|
||||
}
|
||||
|
||||
pub fn by_pkgspec(packie: &mut Packie, pkgspec: &PkgSpec) -> rusqlite::Result<Vec<Installation>> {
|
||||
Ok(packie
|
||||
.local_db
|
||||
.select_installation_by_pkgname(&pkgspec.name)?
|
||||
.into_iter()
|
||||
.filter(|x| pkgspec.matches(&x.pkg_manifest.pkg_ident()))
|
||||
.collect())
|
||||
}
|
||||
+11
-3
@@ -1,14 +1,16 @@
|
||||
mod common;
|
||||
pub mod config;
|
||||
pub mod install;
|
||||
pub mod installation;
|
||||
pub mod link;
|
||||
pub mod local_db;
|
||||
pub mod local;
|
||||
pub mod package;
|
||||
pub mod profile;
|
||||
pub mod remove;
|
||||
pub mod repo;
|
||||
pub mod version;
|
||||
|
||||
use local_db::LocalDb;
|
||||
use config::AllConfig;
|
||||
use local::{Cache, LocalDb};
|
||||
use profile::Profile;
|
||||
|
||||
rust_i18n::i18n!("locales/libpackie", fallback = "en");
|
||||
@@ -18,6 +20,8 @@ rust_i18n::i18n!("locales/libpackie", fallback = "en");
|
||||
pub struct Packie {
|
||||
profile: Profile,
|
||||
local_db: LocalDb,
|
||||
config: AllConfig,
|
||||
cache: Cache,
|
||||
}
|
||||
impl Packie {
|
||||
/// Returns profile of the Packie instance.
|
||||
@@ -63,9 +67,13 @@ impl PackieBuilder {
|
||||
LocalDb::open_rw
|
||||
};
|
||||
let local_db = open_local_db(local_db_path).map_err(BuildPackieError::LocalDb)?;
|
||||
let config = AllConfig::open(&self.profile.packie_config_dir);
|
||||
let cache = Cache(self.profile.packie_cache_dir.clone());
|
||||
Ok(Packie {
|
||||
profile: self.profile,
|
||||
local_db,
|
||||
config,
|
||||
cache,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,70 @@
|
||||
use crate::{installation::Installation, package::PkgIdent};
|
||||
use crate::package::{PkgIdent, PkgManifest, PkgSpec};
|
||||
use rusqlite::{OpenFlags, Row, params};
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Schema of the database.
|
||||
const SCHEMA: &str = r#"
|
||||
CREATE TABLE IF NOT EXISTS "installation"(
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"pkg_name" TEXT NOT NULL,
|
||||
"pkg_version" TEXT NOT NULL,
|
||||
"pkg_arch" TEXT NOT NULL,
|
||||
"pkg_manifest" TEXT NOT NULL,
|
||||
"install_pkgspec" TEXT NOT NULL,
|
||||
"install_date" INTEGER NOT NULL,
|
||||
"update_date" INTEGER NOT NULL,
|
||||
"installed_size" INTEGER NOT NULL
|
||||
);
|
||||
"#;
|
||||
impl super::Packie {
|
||||
pub fn search_installation(
|
||||
&mut self,
|
||||
pkgspec: &PkgSpec,
|
||||
) -> rusqlite::Result<Vec<Installation>> {
|
||||
Ok(self
|
||||
.local_db
|
||||
.select_installation_by_pkgname(&pkgspec.name)?
|
||||
.into_iter()
|
||||
.filter(|x| pkgspec.matches(&x.pkg_manifest.pkg_ident()))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Cache(pub PathBuf);
|
||||
impl Cache {
|
||||
const REPO_DIR_NAME: &str = "repo";
|
||||
|
||||
pub fn repos_dir(&self) -> PathBuf {
|
||||
self.dir(&[Self::REPO_DIR_NAME])
|
||||
}
|
||||
|
||||
pub fn repo_dir(&self, name: &str) -> PathBuf {
|
||||
self.dir(&[Self::REPO_DIR_NAME, name])
|
||||
}
|
||||
|
||||
fn dir(&self, relative: &[&str]) -> PathBuf {
|
||||
let mut path = self.0.clone();
|
||||
for i in relative {
|
||||
path.push(i);
|
||||
}
|
||||
_ = std::fs::create_dir_all(&path);
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LocalDb(rusqlite::Connection);
|
||||
impl LocalDb {
|
||||
pub const FILENAME: &str = "local.db";
|
||||
|
||||
pub const SCHEMA: &str = r#"
|
||||
CREATE TABLE IF NOT EXISTS "installation"(
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"pkg_name" TEXT NOT NULL,
|
||||
"pkg_version" TEXT NOT NULL,
|
||||
"pkg_arch" TEXT NOT NULL,
|
||||
"pkg_manifest" TEXT NOT NULL,
|
||||
"install_pkgspec" TEXT NOT NULL,
|
||||
"install_date" INTEGER NOT NULL,
|
||||
"update_date" INTEGER NOT NULL,
|
||||
"installed_size" INTEGER NOT NULL
|
||||
);
|
||||
"#;
|
||||
|
||||
pub fn open_ro<P: AsRef<Path>>(path: P) -> rusqlite::Result<Self> {
|
||||
let conn = rusqlite::Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY)?;
|
||||
Ok(Self(conn))
|
||||
rusqlite::Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY).map(Self)
|
||||
}
|
||||
|
||||
pub fn open_rw<P: AsRef<Path>>(path: P) -> rusqlite::Result<Self> {
|
||||
let conn = rusqlite::Connection::open(path)?;
|
||||
conn.execute_batch(SCHEMA)?;
|
||||
conn.execute_batch(Self::SCHEMA)?;
|
||||
Ok(Self(conn))
|
||||
}
|
||||
|
||||
@@ -92,6 +127,15 @@ impl LocalDb {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Installation {
|
||||
pub pkg_manifest: PkgManifest,
|
||||
pub install_pkgspec: PkgSpec,
|
||||
pub install_date: i64,
|
||||
pub update_date: i64,
|
||||
pub installed_size: u64,
|
||||
}
|
||||
|
||||
fn map_installation(row: &Row) -> rusqlite::Result<Installation> {
|
||||
Ok(Installation {
|
||||
pkg_manifest: serde_json::from_str(&row.get::<_, String>("pkg_manifest")?)
|
||||
+34
-32
@@ -1,38 +1,40 @@
|
||||
use crate::{Packie, package::PkgSpec};
|
||||
use crate::package::PkgSpec;
|
||||
use rust_i18n::t;
|
||||
|
||||
pub fn remove(packie: &mut Packie, pkgspec: &PkgSpec) -> Result<(), RemoveError> {
|
||||
// Find the installation to remove
|
||||
let mut found =
|
||||
crate::installation::by_pkgspec(packie, pkgspec).map_err(RemoveError::Database)?;
|
||||
if found.is_empty() {
|
||||
return Err(RemoveError::NotFound);
|
||||
impl super::Packie {
|
||||
pub fn remove(&mut self, pkgspec: &PkgSpec) -> Result<(), RemoveError> {
|
||||
// Find the installation to remove
|
||||
let mut found = self
|
||||
.search_installation(pkgspec)
|
||||
.map_err(RemoveError::Database)?;
|
||||
if found.is_empty() {
|
||||
return Err(RemoveError::NotFound);
|
||||
}
|
||||
if found.len() != 1 {
|
||||
return Err(RemoveError::NotUniquePkgSpec);
|
||||
}
|
||||
assert_eq!(found.len(), 1);
|
||||
let found = found.remove(0);
|
||||
|
||||
// Remove all links
|
||||
for link in found.pkg_manifest.links.iter() {
|
||||
_ = crate::link::deactivate_by_description(self, &found.pkg_manifest.pkg_ident(), link);
|
||||
}
|
||||
|
||||
// Remove package files
|
||||
let bundle_dir = self
|
||||
.profile
|
||||
.pkg_dir
|
||||
.join(found.pkg_manifest.pkg_ident().to_string());
|
||||
std::fs::remove_dir_all(&bundle_dir).map_err(RemoveError::RemoveFiles)?;
|
||||
|
||||
// Remove database records
|
||||
self.local_db
|
||||
.remove_installation(&found.pkg_manifest.pkg_ident())
|
||||
.map_err(RemoveError::Database)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
if found.len() != 1 {
|
||||
return Err(RemoveError::NotUniquePkgSpec);
|
||||
}
|
||||
assert_eq!(found.len(), 1);
|
||||
let found = found.remove(0);
|
||||
|
||||
// Remove all links
|
||||
for link in found.pkg_manifest.links.iter() {
|
||||
_ = crate::link::deactivate_by_description(packie, &found.pkg_manifest.pkg_ident(), link);
|
||||
}
|
||||
|
||||
// Remove package files
|
||||
let bundle_dir = packie
|
||||
.profile
|
||||
.pkg_dir
|
||||
.join(found.pkg_manifest.pkg_ident().to_string());
|
||||
std::fs::remove_dir_all(&bundle_dir).map_err(RemoveError::RemoveFiles)?;
|
||||
|
||||
// Remove database records
|
||||
packie
|
||||
.local_db
|
||||
.remove_installation(&found.pkg_manifest.pkg_ident())
|
||||
.map_err(RemoveError::Database)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
use crate::{
|
||||
Packie,
|
||||
common::checksum::DEFAULT_HASHER,
|
||||
package::{Package, PkgIdent, PkgManifest},
|
||||
};
|
||||
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 url::Url;
|
||||
|
||||
impl super::Packie {
|
||||
/// Creates a session for syncing repositories.
|
||||
pub fn sync_repo(&mut self) -> SyncSession<'_> {
|
||||
SyncSession::new(self)
|
||||
}
|
||||
|
||||
/// Download a package.
|
||||
pub fn download_package(
|
||||
&self,
|
||||
repo: &str,
|
||||
pkg_ident: &PkgIdent,
|
||||
mut on_event: impl FnMut(DownloadPackageEvent),
|
||||
) -> Result<PathBuf, RepoError> {
|
||||
let repo_cache_dir = self.cache.repo_dir(repo);
|
||||
|
||||
let repo = self.config.repos.get(repo).ok_or(RepoError::NoSuchRepo)?;
|
||||
let repo_db = repo_cache_dir.join(RepoDb::FILENAME);
|
||||
let mut repo_db = RepoDb::open_ro(repo_db).map_err(RepoError::Database)?;
|
||||
let repo_package = repo_db
|
||||
.select_package_by_pkgident(pkg_ident)
|
||||
.map_err(|_| RepoError::NoSuchPackage)?;
|
||||
|
||||
let standard_filename = format!("{pkg_ident}.pkg");
|
||||
|
||||
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) => {
|
||||
on_event(DownloadPackageEvent::Error(
|
||||
repo_url.clone(),
|
||||
RepoError::Download(err),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RepoDb(rusqlite::Connection);
|
||||
impl RepoDb {
|
||||
const FILENAME: &str = "repo.db";
|
||||
|
||||
const SCHEMA: &str = r#"
|
||||
CREATE TABLE IF NOT EXISTS "package"(
|
||||
"pkg_name" TEXT NOT NULL,
|
||||
"pkg_version" TEXT NOT NULL,
|
||||
"pkg_arch" TEXT NOT NULL,
|
||||
"pkg_manifest" TEXT NOT NULL,
|
||||
"download_size" INTEGER NOT NULL,
|
||||
"installed_size" INTEGER NOT NULL,
|
||||
"checksum" TEXT NOT NULL,
|
||||
PRIMARY KEY ("pkg_name", "pkg_version", "pkg_arch")
|
||||
);
|
||||
"#;
|
||||
|
||||
pub fn open_rw<P: AsRef<Path>>(path: P) -> rusqlite::Result<Self> {
|
||||
let conn = rusqlite::Connection::open(path)?;
|
||||
conn.execute_batch(Self::SCHEMA)?;
|
||||
Ok(Self(conn))
|
||||
}
|
||||
|
||||
pub fn open_ro<P: AsRef<Path>>(path: P) -> rusqlite::Result<Self> {
|
||||
rusqlite::Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY).map(Self)
|
||||
}
|
||||
|
||||
pub fn select_package_by_pkgident(
|
||||
&mut self,
|
||||
pkg_ident: &PkgIdent,
|
||||
) -> rusqlite::Result<RepoPackage> {
|
||||
self.0.prepare_cached(
|
||||
r#"SELECT * FROM "package" WHERE "pkg_name" = ?1 AND "pkg_version" = ?2 AND "pkg_arch" = ?3"#,
|
||||
)?.query_one(params![pkg_ident.name, pkg_ident.version.to_string(), pkg_ident.arch], map_repo_package)
|
||||
}
|
||||
|
||||
pub fn select_package_by_pkgname(&mut self) {}
|
||||
|
||||
pub fn insert_package(&mut self, package: &RepoPackage) -> rusqlite::Result<()> {
|
||||
let pkg_manifest = serde_json::to_string(&package.pkg_manifest)
|
||||
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
|
||||
self.0
|
||||
.prepare_cached(
|
||||
r#"
|
||||
INSERT INTO "package"(
|
||||
"pkg_name", "pkg_version", "pkg_arch", "pkg_manifest",
|
||||
"download_size", "installed_size"
|
||||
)
|
||||
VALUES(?1, ?2, ?3, ?4, ?5, ?6)
|
||||
"#,
|
||||
)?
|
||||
.execute(params![
|
||||
package.pkg_manifest.name,
|
||||
package.pkg_manifest.version.to_string(),
|
||||
package.pkg_manifest.arch,
|
||||
pkg_manifest,
|
||||
package.download_size as i64,
|
||||
package.installed_size as i64
|
||||
])
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
fn map_repo_package(row: &Row) -> rusqlite::Result<RepoPackage> {
|
||||
let pkg_manifest: String = row.get("pkg_manifest")?;
|
||||
let pkg_manifest = serde_json::from_str(&pkg_manifest).map_err(|error| {
|
||||
rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Text, Box::new(error))
|
||||
})?;
|
||||
Ok(RepoPackage {
|
||||
pkg_manifest,
|
||||
download_size: row.get::<_, i64>("download_size")? as u64,
|
||||
installed_size: row.get::<_, i64>("installed_size")? as u64,
|
||||
checksum: row.get("checksum")?,
|
||||
})
|
||||
}
|
||||
|
||||
/// A record representing to a package in a repository.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RepoPackage {
|
||||
/// Package manifest.
|
||||
pub pkg_manifest: PkgManifest,
|
||||
|
||||
/// Download size of the package.
|
||||
pub download_size: u64,
|
||||
|
||||
/// Installed size of the package.
|
||||
pub installed_size: u64,
|
||||
|
||||
/// Checksum of the package file.
|
||||
pub checksum: String,
|
||||
}
|
||||
|
||||
/// Session of syncing repositories.
|
||||
pub struct SyncSession<'a, E = fn(SyncEvent)> {
|
||||
packie: &'a mut Packie,
|
||||
repo: Option<String>,
|
||||
on_event: E,
|
||||
}
|
||||
impl<'a> SyncSession<'a> {
|
||||
pub fn new(packie: &'a mut Packie) -> Self {
|
||||
Self {
|
||||
packie,
|
||||
repo: None,
|
||||
on_event: |_| (),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'a, E> SyncSession<'a, E> {
|
||||
pub fn repo(mut self, repo: String) -> Self {
|
||||
self.repo = Some(repo);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_event<E2>(self, on_event: E2) -> SyncSession<'a, E2> {
|
||||
SyncSession {
|
||||
packie: self.packie,
|
||||
repo: self.repo,
|
||||
on_event,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<E: FnMut(SyncEvent)> SyncSession<'_, E> {
|
||||
pub fn run(mut self) {
|
||||
let repos = match self.repo.clone() {
|
||||
Some(x) => vec![x],
|
||||
None => self.packie.config.repos.keys().cloned().collect(),
|
||||
};
|
||||
for repo_name in repos {
|
||||
let Some(repo) = self.packie.config.repos.get(&repo_name).cloned() else {
|
||||
self.raise_error(Some(repo_name), None, RepoError::NoSuchRepo);
|
||||
continue;
|
||||
};
|
||||
for url in repo.urls() {
|
||||
match self.sync_url(&repo_name, &url) {
|
||||
Ok(()) => break,
|
||||
Err(err) => self.raise_error(Some(repo_name.clone()), Some(url.clone()), err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_url(&mut self, repo_name: &str, url: &Url) -> Result<(), RepoError> {
|
||||
let repo_db_url = url
|
||||
.join(RepoDb::FILENAME)
|
||||
.map_err(|_| DownloadError::InvalidUrl)
|
||||
.map_err(RepoError::Download)?;
|
||||
|
||||
let repo_cache_dir = self.packie.cache.repo_dir(repo_name);
|
||||
Download::new(
|
||||
repo_db_url.to_string(),
|
||||
repo_cache_dir.join(RepoDb::FILENAME),
|
||||
)
|
||||
.on_progress(Box::new(|sum, total| {
|
||||
self.raise_progress(repo_name.into(), repo_db_url.clone(), sum, total)
|
||||
}))
|
||||
.run()
|
||||
.map_err(RepoError::Download)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn raise_error(&mut self, repo: Option<String>, url: Option<Url>, error: RepoError) {
|
||||
(self.on_event)(SyncEvent::Error(repo, url, error));
|
||||
}
|
||||
|
||||
fn raise_progress(&mut self, repo: String, url: Url, downloaded_bytes: u64, total_bytes: u64) {
|
||||
(self.on_event)(SyncEvent::Progress(SyncProgress {
|
||||
repo: Some(repo),
|
||||
url: Some(url),
|
||||
downloaded_bytes,
|
||||
total_bytes,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/// An event during running [`SyncSession`], which may report progress changes or errors.
|
||||
#[derive(Debug)]
|
||||
pub enum SyncEvent {
|
||||
Progress(SyncProgress),
|
||||
Error(Option<String>, Option<Url>, RepoError),
|
||||
}
|
||||
|
||||
/// A progress while syncing repositories.
|
||||
#[derive(Debug)]
|
||||
pub struct SyncProgress {
|
||||
pub repo: Option<String>,
|
||||
pub url: Option<Url>,
|
||||
pub downloaded_bytes: u64,
|
||||
pub total_bytes: u64,
|
||||
}
|
||||
|
||||
/// An event occurred while downloading a package.
|
||||
#[derive(Debug)]
|
||||
pub enum DownloadPackageEvent {
|
||||
Progress(Url, u64, u64),
|
||||
Error(Url, RepoError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RepoError {
|
||||
#[error("{}", t!("repo.RepoError.NoSuchRepo"))]
|
||||
NoSuchRepo,
|
||||
|
||||
#[error("{}", t!("repo.RepoError.NoSuchPackage"))]
|
||||
NoSuchPackage,
|
||||
|
||||
#[error("{}", t!("repo.RepoError.Database", error = 0))]
|
||||
Database(rusqlite::Error),
|
||||
|
||||
#[error("{0}")]
|
||||
Download(DownloadError),
|
||||
|
||||
#[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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user