feat: introduce new links format

Signed-off-by: sisungo <[email protected]>
This commit is contained in:
2026-06-28 17:40:26 +08:00
parent 8a029d2213
commit ee5b55b79b
10 changed files with 211 additions and 37 deletions
+18 -3
View File
@@ -1,7 +1,10 @@
use anyhow::anyhow;
use clap::Parser;
use indicatif::ProgressBar;
use packie::repo::RepoServeDir;
use packie::{
package::{Package, PkgIdent},
repo::RepoServeDir,
};
use std::path::PathBuf;
#[derive(Debug, Parser)]
@@ -13,6 +16,10 @@ pub struct Cli {
#[arg(long, short)]
add_package: Option<Vec<PathBuf>>,
/// Remove package(s) from the repository
#[arg(long, short)]
remove_package: Option<Vec<PkgIdent>>,
/// Clone or pull remote repository
#[arg(long, short)]
clone: Option<String>,
@@ -34,8 +41,16 @@ 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}"))?;
for pkg in cli.remove_package.into_iter().flatten() {
repo.remove_package(&pkg)
.map_err(|err| anyhow!("\"{pkg}\": {err}"))?;
}
for file in cli.add_package.into_iter().flatten() {
let pkg_ident = Package::open(&file)?.manifest()?.pkg_ident();
_ = repo.remove_package(&pkg_ident);
repo.add_package(&file)
.map_err(|err| anyhow!("\"{}\": {}", file.display(), err))?;
}
Ok(())
+30 -13
View File
@@ -1,5 +1,5 @@
use crate::{
local::Installation,
local::{InstallFlags, Installation},
package::{Package, PackageError, PkgIdent, PkgSpec},
repo::{RepoError, RepoPackage},
};
@@ -98,8 +98,7 @@ impl super::Packie {
// Check if the package is previously installed
if !self
.search_installation(&pkg_manifest.pkg_ident().into())
.map_err(InstallError::Database)?
.search_installation(&pkg_manifest.pkg_ident().into())?
.is_empty()
{
return Err(InstallError::AlreadyInstalled);
@@ -124,24 +123,37 @@ impl super::Packie {
install_date: crate::common::timestamp_secs(),
update_date: crate::common::timestamp_secs(),
installed_size,
install_flags: InstallFlags::default(),
};
self.local_db
.insert_installation(&installation)
.map_err(InstallError::Database)?;
self.local_db.insert_installation(&installation)?;
// Record abstract package provision in the database
for i in installation.pkg_manifest.provides.iter() {
self.local_db
.insert_abspkg(&i, &installation.pkg_manifest.pkg_ident())
.map_err(InstallError::Database)?;
.insert_abspkg(&i, &installation.pkg_manifest.pkg_ident())?;
}
// Copy links file
if let Some(links) = package.links() {
std::fs::copy(
&links.0,
self.local_data
.links_file(&installation.pkg_manifest.pkg_ident()),
)
.map_err(|err| InstallError::Database(Box::new(err)))?;
}
// Enable default links
for link in pkg_manifest.links.iter() {
if !link.default {
continue;
if let Some(links) = package.links() {
for linkdes in links.iter().ok().into_iter().flatten() {
let Ok(linkdes) = linkdes else {
continue;
};
if !linkdes.default {
continue;
}
_ = crate::link::activate_by_description(self, &pkg_manifest.pkg_ident(), &linkdes);
}
_ = crate::link::activate_by_description(self, &pkg_manifest.pkg_ident(), link);
}
Ok(())
@@ -172,13 +184,18 @@ pub enum InstallError {
Repo(RepoError),
#[error("{}", t!("install.InstallError.Database", error = .0))]
Database(rusqlite::Error),
Database(Box<dyn std::error::Error + Send + Sync>),
}
impl From<PackageError> for InstallError {
fn from(value: PackageError) -> Self {
Self::Package(value)
}
}
impl From<rusqlite::Error> for InstallError {
fn from(value: rusqlite::Error) -> Self {
Self::Database(Box::new(value))
}
}
fn default_install_pkgspec(pkg_ident: PkgIdent) -> PkgSpec {
PkgSpec {
+4 -1
View File
@@ -10,7 +10,7 @@ pub mod repo;
pub mod version;
use config::AllConfig;
use local::{Cache, LocalDb};
use local::{Cache, DataDir, LocalDb};
use profile::Profile;
rust_i18n::i18n!("locales/libpackie", fallback = "en");
@@ -21,6 +21,7 @@ pub struct Packie {
profile: Profile,
local_db: LocalDb,
config: AllConfig,
local_data: DataDir,
cache: Cache,
}
impl Packie {
@@ -69,11 +70,13 @@ impl PackieBuilder {
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());
let local_data = DataDir(self.profile.packie_data_dir.clone());
Ok(Packie {
profile: self.profile,
local_db,
config,
cache,
local_data,
})
}
}
+25 -2
View File
@@ -1,6 +1,29 @@
use crate::{Packie, package::PkgIdent};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::{
fs::File,
io::{BufRead, BufReader},
path::{Path, PathBuf},
};
/// Iterator over link descriptions in a links file.
#[derive(Debug)]
pub struct LinksFile(pub PathBuf);
impl LinksFile {
pub const FILENAME: &str = "_links";
pub fn iter(&self) -> std::io::Result<impl Iterator<Item = std::io::Result<LinkDescription>>> {
Ok(BufReader::new(File::open(&self.0)?)
.lines()
.map(|s| {
Ok::<_, std::io::Error>(
serde_json::from_str(&s?)
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err)),
)
})
.flatten())
}
}
/// Description of a link.
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -52,7 +75,7 @@ pub fn create(from: impl AsRef<Path>, to: impl AsRef<Path>) -> std::io::Result<(
pub fn remove(path: impl Into<PathBuf>) -> std::io::Result<()> {
let mut path = path.into();
if !std::fs::metadata(&path)?.is_symlink() {
if std::fs::read_link(&path).is_err() {
return Err(std::io::ErrorKind::InvalidData.into());
}
std::fs::remove_file(&path)?;
+68 -6
View File
@@ -4,7 +4,12 @@ use crate::{
};
use itertools::Itertools;
use rusqlite::{OpenFlags, Row, params};
use std::path::{Path, PathBuf};
use std::{
convert::Infallible,
fmt::Display,
path::{Path, PathBuf},
str::FromStr,
};
impl super::Packie {
pub fn search_installation(
@@ -35,6 +40,29 @@ impl super::Packie {
}
}
#[derive(Debug)]
pub struct DataDir(pub PathBuf);
impl DataDir {
const LINKS_FILES_DIR_NAME: &str = "links_files";
pub fn links_files_dir(&self) -> PathBuf {
self.dir(&[Self::LINKS_FILES_DIR_NAME])
}
pub fn links_file(&self, name: &PkgIdent) -> PathBuf {
self.links_files_dir().join(name.to_string())
}
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 Cache(pub PathBuf);
impl Cache {
@@ -73,7 +101,8 @@ CREATE TABLE IF NOT EXISTS "installation"(
"install_pkgspec" TEXT NOT NULL,
"install_date" INTEGER NOT NULL,
"update_date" INTEGER NOT NULL,
"installed_size" INTEGER NOT NULL
"installed_size" INTEGER NOT NULL,
"install_flags" TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "abspkg"(
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -100,9 +129,10 @@ CREATE TABLE IF NOT EXISTS "abspkg"(
r#"
INSERT INTO "installation"(
"pkg_name", "pkg_version", "pkg_arch", "pkg_manifest",
"install_pkgspec", "install_date", "update_date", "installed_size"
"install_pkgspec", "install_date", "update_date", "installed_size",
"install_flags"
)
VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
"#,
)?
.execute(params![
@@ -114,6 +144,7 @@ CREATE TABLE IF NOT EXISTS "abspkg"(
installation.install_date,
installation.update_date,
installation.installed_size as i64,
installation.install_flags.to_string(),
])?;
Ok(())
}
@@ -125,7 +156,7 @@ CREATE TABLE IF NOT EXISTS "abspkg"(
) -> rusqlite::Result<()> {
self.0
.prepare_cached(r#"INSERT INTO "abspkg"("abspkg_ident", "provider") VALUES(?1, ?2)"#)?
.execute(params![abspkg.to_string(), provider.to_string(),])?;
.execute(params![abspkg.to_string(), provider.to_string()])?;
Ok(())
}
@@ -176,7 +207,7 @@ CREATE TABLE IF NOT EXISTS "abspkg"(
.map(|x| x > 0)
}
pub fn remove_abspkg_by_provider(&mut self, provider: &PkgIdent) -> rusqlite::Result<bool> {
pub fn remove_abspkg_provided_by(&mut self, provider: &PkgIdent) -> rusqlite::Result<bool> {
self.0
.prepare_cached(r#"DELETE FROM "abspkg" WHERE "provider" = ?1"#)?
.execute(params![provider.to_string()])
@@ -191,6 +222,36 @@ pub struct Installation {
pub install_date: i64,
pub update_date: i64,
pub installed_size: u64,
pub install_flags: InstallFlags,
}
#[derive(Debug, Clone, Default)]
pub struct InstallFlags(Vec<String>);
impl InstallFlags {
pub const AUTO_INSTALL: &str = "auto-install";
pub fn new(v: &[&str]) -> Self {
Self(v.iter().map(ToString::to_string).collect())
}
pub fn contains(&self, s: &str) -> bool {
self.0.iter().any(|x| x == s)
}
}
impl FromStr for InstallFlags {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(s.split(' ').map(Into::into).collect()))
}
}
impl Display for InstallFlags {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for i in self.0.iter() {
write!(f, "{i} ")?;
}
Ok(())
}
}
fn map_installation(row: &Row) -> rusqlite::Result<Installation> {
@@ -204,6 +265,7 @@ fn map_installation(row: &Row) -> rusqlite::Result<Installation> {
install_date: row.get("install_date")?,
update_date: row.get("update_date")?,
installed_size: row.get::<_, i64>("installed_size")? as _,
install_flags: row.get::<_, String>("install_flags")?.parse().unwrap(),
})
}
+11 -6
View File
@@ -1,5 +1,5 @@
use crate::{
link::LinkDescription,
link::LinksFile,
version::{Version, VersionFilter},
};
use serde::{Deserialize, Serialize};
@@ -39,6 +39,15 @@ impl Package {
.map_err(PackageError::Manifest)
}
pub fn links(&self) -> Option<LinksFile> {
let path = self.extracted_dir.path().join(LinksFile::FILENAME);
if path.exists() {
Some(LinksFile(path))
} else {
None
}
}
/// Returns path of the bundle directory of this package.
pub fn bundle_dir(&self) -> PathBuf {
self.extracted_dir.path().join("bundle")
@@ -86,15 +95,11 @@ pub struct PkgManifest {
/// Provided abstract packages.
#[serde(default)]
pub provides: Vec<AbsPkgIdent>,
/// Links.
#[serde(default)]
pub links: Vec<LinkDescription>,
}
impl PkgManifest {
pub const FILENAME: &str = "PkgManifest.json";
pub const SIZE_MAX: u64 = 64 * 1024;
pub const SIZE_MAX: u64 = 2 * 1024 * 1024;
pub fn read_at<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
let mut data = Vec::with_capacity(1024);
+17 -4
View File
@@ -1,4 +1,4 @@
use crate::package::PkgSpec;
use crate::{link::LinksFile, package::PkgSpec};
use rust_i18n::t;
impl super::Packie {
@@ -17,8 +17,18 @@ impl super::Packie {
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);
let links = LinksFile(self.local_data.links_file(&found.pkg_manifest.pkg_ident()));
if let Ok(links) = links.iter() {
for link in links {
let Ok(link) = link else {
continue;
};
_ = crate::link::deactivate_by_description(
self,
&found.pkg_manifest.pkg_ident(),
&link,
);
}
}
// Remove package files
@@ -33,9 +43,12 @@ impl super::Packie {
.remove_installation(&found.pkg_manifest.pkg_ident())
.map_err(RemoveError::Database)?;
self.local_db
.remove_abspkg_by_provider(&found.pkg_manifest.pkg_ident())
.remove_abspkg_provided_by(&found.pkg_manifest.pkg_ident())
.map_err(RemoveError::Database)?;
// Remove the links file
_ = std::fs::remove_file(self.local_data.links_file(&found.pkg_manifest.pkg_ident()));
Ok(())
}
}
+9 -1
View File
@@ -9,12 +9,14 @@ pub struct Download<P> {
from: String,
to: PathBuf,
on_progress: P,
no_existence_check: bool,
}
impl Download<fn(u64, u64)> {
pub fn new(from: String, to: PathBuf) -> Self {
Self {
from,
to,
no_existence_check: false,
on_progress: |_, _| (),
}
}
@@ -24,13 +26,19 @@ impl<P> Download<P> {
Download {
from: self.from,
to: self.to,
no_existence_check: self.no_existence_check,
on_progress,
}
}
pub fn no_existence_check(mut self) -> Self {
self.no_existence_check = true;
self
}
}
impl<P: FnMut(u64, u64)> Download<P> {
pub fn run(&mut self) -> Result<(), DownloadError> {
if self.to.exists() {
if self.to.exists() && !self.no_existence_check {
return Ok(());
}
+17
View File
@@ -203,6 +203,22 @@ CREATE TABLE IF NOT EXISTS "abspkg"(
.execute(params![abs.to_string(), real.to_string()])?;
Ok(())
}
pub fn delete_package_by_ident(&mut self, ident: &PkgIdent) -> rusqlite::Result<bool> {
self.0
.prepare_cached(
r#"DELETE FROM "package" WHERE "pkg_name" = ?1 AND "pkg_version" = ?2 AND "pkg_arch" = ?3"#,
)?
.execute(params![ident.name, ident.version.to_string(), ident.arch])
.map(|x| x > 0)
}
pub fn delete_abspkg_by_provider(&mut self, ident: &PkgIdent) -> rusqlite::Result<bool> {
self.0
.prepare_cached(r#"DELETE FROM "abspkg" WHERE "provider" = ?1"#)?
.execute(params![ident.to_string()])
.map(|x| x > 0)
}
}
fn map_repo_package(row: &Row) -> rusqlite::Result<RepoPackage> {
@@ -308,6 +324,7 @@ impl<E: FnMut(SyncEvent)> SyncSession<'_, E> {
repo_db_url.to_string(),
repo_cache_dir.join(RepoDb::FILENAME),
)
.no_existence_check()
.on_progress(Box::new(|sum, total| {
self.raise_progress(repo_name.into(), repo_db_url.clone(), sum, total)
}))
+12 -1
View File
@@ -1,6 +1,6 @@
use crate::{
common::checksum::DEFAULT_HASHER,
package::Package,
package::{Package, PkgIdent},
repo::{RepoDb, RepoPackage, download::Download},
};
use std::path::{Path, PathBuf};
@@ -91,4 +91,15 @@ impl RepoServeDir {
}
Ok(())
}
pub fn remove_package(
&mut self,
ident: &PkgIdent,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let path = self.path.join(format!("{ident}.pkg"));
std::fs::remove_file(path)?;
self.db.delete_package_by_ident(ident)?;
self.db.delete_abspkg_by_provider(ident)?;
Ok(())
}
}