feat: add basic repository support

Signed-off-by: sisungo <[email protected]>
This commit is contained in:
2026-06-16 21:07:01 +08:00
parent f86ddb80bb
commit 8b042a3e8e
17 changed files with 731 additions and 139 deletions
+8 -1
View File
@@ -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 }
+1
View File
@@ -0,0 +1 @@
+1 -1
View File
@@ -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
View File
@@ -1,3 +1,4 @@
mod common;
mod info;
mod initdb;
mod install;
+2 -1
View File
@@ -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
View File
@@ -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: "所有尝试均失败"
+44
View File
@@ -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)
}
-44
View File
@@ -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))
}
+45
View File
@@ -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))
}
+21
View File
@@ -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")),
}
}
}
+57
View File
@@ -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
View File
@@ -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()
{
-22
View File
@@ -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
View File
@@ -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,
})
}
}
+63 -19
View File
@@ -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
View File
@@ -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
View File
@@ -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(())
}
}