2025-07-24 00:28:33 +02:00
|
|
|
// This Source Code Form is subject to the terms of
|
|
|
|
|
// the Mozilla Public License, v. 2.0. If a copy of the
|
|
|
|
|
// MPL was not distributed with this file, You can
|
|
|
|
|
// obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
|
|
2025-07-26 15:33:39 +02:00
|
|
|
use miette::Diagnostic;
|
2025-07-26 12:54:01 +02:00
|
|
|
use serde::{Deserialize, Serialize};
|
2025-07-24 00:28:33 +02:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
use std::fs;
|
2025-07-26 15:33:39 +02:00
|
|
|
use std::io;
|
2025-07-24 00:28:33 +02:00
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
use std::time::SystemTime;
|
2025-07-26 15:33:39 +02:00
|
|
|
use thiserror::Error;
|
2025-07-24 00:28:33 +02:00
|
|
|
|
|
|
|
|
use crate::fmri::Fmri;
|
|
|
|
|
|
2025-07-26 15:33:39 +02:00
|
|
|
/// Errors that can occur in catalog operations
|
|
|
|
|
#[derive(Debug, Error, Diagnostic)]
|
|
|
|
|
pub enum CatalogError {
|
|
|
|
|
#[error("catalog part does not exist: {name}")]
|
|
|
|
|
#[diagnostic(
|
|
|
|
|
code(ips::repository_error::catalog::part_not_found),
|
|
|
|
|
help("Check that the catalog part exists and is accessible")
|
|
|
|
|
)]
|
2025-07-27 15:22:49 +02:00
|
|
|
CatalogPartNotFound { name: String },
|
2025-07-26 15:33:39 +02:00
|
|
|
|
|
|
|
|
#[error("catalog part not loaded: {name}")]
|
|
|
|
|
#[diagnostic(
|
|
|
|
|
code(ips::repository_error::catalog::part_not_loaded),
|
|
|
|
|
help("Load the catalog part before attempting to save it")
|
|
|
|
|
)]
|
2025-07-27 15:22:49 +02:00
|
|
|
CatalogPartNotLoaded { name: String },
|
2025-07-26 15:33:39 +02:00
|
|
|
|
|
|
|
|
#[error("update log not loaded: {name}")]
|
|
|
|
|
#[diagnostic(
|
|
|
|
|
code(ips::repository_error::catalog::update_log_not_loaded),
|
|
|
|
|
help("Load the update log before attempting to save it")
|
|
|
|
|
)]
|
2025-07-27 15:22:49 +02:00
|
|
|
UpdateLogNotLoaded { name: String },
|
2025-07-26 15:33:39 +02:00
|
|
|
|
|
|
|
|
#[error("update log does not exist: {name}")]
|
|
|
|
|
#[diagnostic(
|
|
|
|
|
code(ips::repository_error::catalog::update_log_not_found),
|
|
|
|
|
help("Check that the update log exists and is accessible")
|
|
|
|
|
)]
|
2025-07-27 15:22:49 +02:00
|
|
|
UpdateLogNotFound { name: String },
|
2025-07-26 15:33:39 +02:00
|
|
|
|
|
|
|
|
#[error("failed to serialize JSON: {0}")]
|
|
|
|
|
#[diagnostic(
|
|
|
|
|
code(ips::repository_error::catalog::json_serialize),
|
|
|
|
|
help("This is likely a bug in the code")
|
|
|
|
|
)]
|
|
|
|
|
JsonSerializationError(#[from] serde_json::Error),
|
|
|
|
|
|
|
|
|
|
#[error("I/O error: {0}")]
|
|
|
|
|
#[diagnostic(
|
|
|
|
|
code(ips::repository_error::catalog::io),
|
|
|
|
|
help("Check system resources and permissions")
|
|
|
|
|
)]
|
|
|
|
|
IoError(#[from] io::Error),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Result type for catalog operations
|
|
|
|
|
pub type Result<T> = std::result::Result<T, CatalogError>;
|
|
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Format a SystemTime as an ISO-8601 'basic format' date in UTC
|
|
|
|
|
fn format_iso8601_basic(time: &SystemTime) -> String {
|
|
|
|
|
let datetime = convert_system_time_to_datetime(time);
|
|
|
|
|
format!("{}Z", datetime.format("%Y%m%dT%H%M%S.%f"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Convert SystemTime to UTC DateTime, handling errors gracefully
|
|
|
|
|
fn convert_system_time_to_datetime(time: &SystemTime) -> chrono::DateTime<chrono::Utc> {
|
|
|
|
|
let duration = time
|
|
|
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
|
|
|
.unwrap_or_else(|_| std::time::Duration::from_secs(0));
|
|
|
|
|
|
|
|
|
|
let secs = duration.as_secs() as i64;
|
|
|
|
|
let nanos = duration.subsec_nanos();
|
|
|
|
|
|
2025-07-26 12:54:01 +02:00
|
|
|
chrono::DateTime::from_timestamp(secs, nanos).unwrap_or_else(|| {
|
|
|
|
|
chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
|
2025-07-24 00:28:33 +02:00
|
|
|
chrono::NaiveDateTime::default(),
|
|
|
|
|
chrono::Utc,
|
2025-07-26 12:54:01 +02:00
|
|
|
)
|
|
|
|
|
})
|
2025-07-24 00:28:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Catalog version
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub enum CatalogVersion {
|
|
|
|
|
V1 = 1,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for CatalogVersion {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
CatalogVersion::V1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Catalog part information
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct CatalogPartInfo {
|
|
|
|
|
/// Last modified timestamp in ISO-8601 'basic format' date in UTC
|
|
|
|
|
#[serde(rename = "last-modified")]
|
|
|
|
|
pub last_modified: String,
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Optional SHA-1 signature of the catalog part
|
|
|
|
|
#[serde(rename = "signature-sha-1", skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub signature_sha1: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Update log information
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct UpdateLogInfo {
|
|
|
|
|
/// Last modified timestamp in ISO-8601 'basic format' date in UTC
|
|
|
|
|
#[serde(rename = "last-modified")]
|
|
|
|
|
pub last_modified: String,
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Optional SHA-1 signature of the update log
|
|
|
|
|
#[serde(rename = "signature-sha-1", skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub signature_sha1: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Catalog attributes
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct CatalogAttrs {
|
|
|
|
|
/// Creation timestamp in ISO-8601 'basic format' date in UTC
|
|
|
|
|
pub created: String,
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Last modified timestamp in ISO-8601 'basic format' date in UTC
|
|
|
|
|
#[serde(rename = "last-modified")]
|
|
|
|
|
pub last_modified: String,
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Number of unique package stems in the catalog
|
|
|
|
|
#[serde(rename = "package-count")]
|
|
|
|
|
pub package_count: usize,
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Number of unique package versions in the catalog
|
|
|
|
|
#[serde(rename = "package-version-count")]
|
|
|
|
|
pub package_version_count: usize,
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Available catalog parts
|
|
|
|
|
pub parts: HashMap<String, CatalogPartInfo>,
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Available update logs
|
|
|
|
|
#[serde(skip_serializing_if = "HashMap::is_empty")]
|
|
|
|
|
pub updates: HashMap<String, UpdateLogInfo>,
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Catalog version
|
|
|
|
|
pub version: u32,
|
2025-12-22 20:11:08 +01:00
|
|
|
|
|
|
|
|
/// Optional signature information
|
|
|
|
|
#[serde(rename = "_SIGNATURE", skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub signature: Option<HashMap<String, String>>,
|
2025-07-24 00:28:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl CatalogAttrs {
|
|
|
|
|
/// Create a new catalog attributes structure
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
let now = SystemTime::now();
|
|
|
|
|
let timestamp = format_iso8601_basic(&now);
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
CatalogAttrs {
|
|
|
|
|
signature: None,
|
|
|
|
|
created: timestamp.clone(),
|
|
|
|
|
last_modified: timestamp,
|
|
|
|
|
package_count: 0,
|
|
|
|
|
package_version_count: 0,
|
|
|
|
|
parts: HashMap::new(),
|
|
|
|
|
updates: HashMap::new(),
|
|
|
|
|
version: CatalogVersion::V1 as u32,
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Save catalog attributes to a file
|
|
|
|
|
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
|
2025-12-09 12:49:25 +01:00
|
|
|
let json = serde_json::to_string(self)?;
|
2025-07-24 00:28:33 +02:00
|
|
|
fs::write(path, json)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Load catalog attributes from a file
|
|
|
|
|
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
|
|
|
|
|
let json = fs::read_to_string(path)?;
|
|
|
|
|
let attrs: CatalogAttrs = serde_json::from_str(&json)?;
|
|
|
|
|
Ok(attrs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Package version entry in a catalog
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct PackageVersionEntry {
|
|
|
|
|
/// Package version string
|
|
|
|
|
pub version: String,
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Optional actions associated with this package version
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub actions: Option<Vec<String>>,
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Optional SHA-1 signature of the package manifest
|
|
|
|
|
#[serde(rename = "signature-sha-1", skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub signature_sha1: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Catalog part (base, dependency, summary)
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct CatalogPart {
|
|
|
|
|
/// Packages by publisher and stem
|
2025-12-08 22:39:28 +01:00
|
|
|
#[serde(flatten)]
|
2025-07-24 00:28:33 +02:00
|
|
|
pub packages: HashMap<String, HashMap<String, Vec<PackageVersionEntry>>>,
|
2025-12-22 20:11:08 +01:00
|
|
|
|
|
|
|
|
/// Optional signature information
|
|
|
|
|
#[serde(rename = "_SIGNATURE", skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub signature: Option<HashMap<String, String>>,
|
2025-07-24 00:28:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl CatalogPart {
|
|
|
|
|
/// Create a new catalog part
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
CatalogPart {
|
|
|
|
|
signature: None,
|
|
|
|
|
packages: HashMap::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Add a package to the catalog part
|
2025-07-26 12:54:01 +02:00
|
|
|
pub fn add_package(
|
|
|
|
|
&mut self,
|
|
|
|
|
publisher: &str,
|
|
|
|
|
fmri: &Fmri,
|
|
|
|
|
actions: Option<Vec<String>>,
|
|
|
|
|
signature: Option<String>,
|
|
|
|
|
) {
|
|
|
|
|
let publisher_packages = self
|
|
|
|
|
.packages
|
|
|
|
|
.entry(publisher.to_string())
|
|
|
|
|
.or_insert_with(HashMap::new);
|
|
|
|
|
let stem_versions = publisher_packages
|
|
|
|
|
.entry(fmri.stem().to_string())
|
|
|
|
|
.or_insert_with(Vec::new);
|
|
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
// Check if this version already exists
|
|
|
|
|
for entry in stem_versions.iter_mut() {
|
|
|
|
|
if !fmri.version().is_empty() && entry.version == fmri.version() {
|
|
|
|
|
// Update existing entry
|
|
|
|
|
if let Some(acts) = actions {
|
|
|
|
|
entry.actions = Some(acts);
|
|
|
|
|
}
|
|
|
|
|
if let Some(sig) = signature {
|
|
|
|
|
entry.signature_sha1 = Some(sig);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
// Add a new entry
|
|
|
|
|
stem_versions.push(PackageVersionEntry {
|
|
|
|
|
version: fmri.version(),
|
|
|
|
|
actions,
|
|
|
|
|
signature_sha1: signature,
|
|
|
|
|
});
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
// Sort versions (should be in ascending order)
|
|
|
|
|
stem_versions.sort_by(|a, b| a.version.cmp(&b.version));
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Save a catalog part to a file
|
|
|
|
|
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
|
|
|
|
|
let json = serde_json::to_string_pretty(self)?;
|
|
|
|
|
fs::write(path, json)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Load catalog part from a file
|
|
|
|
|
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
|
2025-08-04 23:45:41 +02:00
|
|
|
let path_ref = path.as_ref();
|
2025-12-08 22:39:28 +01:00
|
|
|
let json = fs::File::open(path_ref)?;
|
2025-12-22 20:11:08 +01:00
|
|
|
|
2025-12-08 22:39:28 +01:00
|
|
|
// Try to parse the JSON directly
|
|
|
|
|
match serde_json::from_reader(json) {
|
|
|
|
|
Ok(part) => Ok(part),
|
2025-12-22 20:11:08 +01:00
|
|
|
Err(e) => Err(CatalogError::JsonSerializationError(e)),
|
2025-08-04 23:45:41 +02:00
|
|
|
}
|
2025-07-24 00:28:33 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Operation type for catalog updates
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub enum CatalogOperationType {
|
|
|
|
|
#[serde(rename = "add")]
|
|
|
|
|
Add,
|
|
|
|
|
#[serde(rename = "remove")]
|
|
|
|
|
Remove,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Package update entry in an update log
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct PackageUpdateEntry {
|
|
|
|
|
/// Type of operation (add or remove)
|
|
|
|
|
#[serde(rename = "op-type")]
|
|
|
|
|
pub op_type: CatalogOperationType,
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Timestamp of the operation in ISO-8601 'basic format' date in UTC
|
|
|
|
|
#[serde(rename = "op-time")]
|
|
|
|
|
pub op_time: String,
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Package version string
|
|
|
|
|
pub version: String,
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Catalog part entries
|
|
|
|
|
#[serde(flatten)]
|
|
|
|
|
pub catalog_parts: HashMap<String, HashMap<String, Vec<String>>>,
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Optional SHA-1 signature of the package manifest
|
|
|
|
|
#[serde(rename = "signature-sha-1", skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub signature_sha1: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Update log
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct UpdateLog {
|
2025-12-22 20:11:08 +01:00
|
|
|
/// Updates by publisher and stem
|
|
|
|
|
pub updates: HashMap<String, HashMap<String, Vec<PackageUpdateEntry>>>,
|
|
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Optional signature information
|
|
|
|
|
#[serde(rename = "_SIGNATURE", skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub signature: Option<HashMap<String, String>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl UpdateLog {
|
|
|
|
|
/// Create a new update log
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
UpdateLog {
|
|
|
|
|
signature: None,
|
|
|
|
|
updates: HashMap::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Add a package update to the log
|
|
|
|
|
pub fn add_update(
|
|
|
|
|
&mut self,
|
|
|
|
|
publisher: &str,
|
|
|
|
|
fmri: &Fmri,
|
|
|
|
|
op_type: CatalogOperationType,
|
|
|
|
|
catalog_parts: HashMap<String, HashMap<String, Vec<String>>>,
|
|
|
|
|
signature: Option<String>,
|
|
|
|
|
) {
|
2025-07-26 12:54:01 +02:00
|
|
|
let publisher_updates = self
|
|
|
|
|
.updates
|
|
|
|
|
.entry(publisher.to_string())
|
|
|
|
|
.or_insert_with(HashMap::new);
|
|
|
|
|
let stem_updates = publisher_updates
|
|
|
|
|
.entry(fmri.stem().to_string())
|
|
|
|
|
.or_insert_with(Vec::new);
|
|
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
let now = SystemTime::now();
|
|
|
|
|
let timestamp = format_iso8601_basic(&now);
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
stem_updates.push(PackageUpdateEntry {
|
|
|
|
|
op_type,
|
|
|
|
|
op_time: timestamp,
|
|
|
|
|
version: fmri.version(),
|
|
|
|
|
catalog_parts,
|
|
|
|
|
signature_sha1: signature,
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Save update log to a file
|
|
|
|
|
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
|
2025-12-09 12:49:25 +01:00
|
|
|
let json = serde_json::to_string(self)?;
|
2025-07-24 00:28:33 +02:00
|
|
|
fs::write(path, json)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Load update log from a file
|
|
|
|
|
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
|
|
|
|
|
let json = fs::read_to_string(path)?;
|
|
|
|
|
let log: UpdateLog = serde_json::from_str(&json)?;
|
|
|
|
|
Ok(log)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Catalog manager
|
|
|
|
|
pub struct CatalogManager {
|
|
|
|
|
/// Path to the catalog directory
|
|
|
|
|
catalog_dir: PathBuf,
|
2025-12-22 20:11:08 +01:00
|
|
|
|
2025-07-31 00:18:21 +02:00
|
|
|
/// Publisher name
|
|
|
|
|
publisher: String,
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Catalog attributes
|
|
|
|
|
attrs: CatalogAttrs,
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Catalog parts
|
|
|
|
|
parts: HashMap<String, CatalogPart>,
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Update logs
|
|
|
|
|
update_logs: HashMap<String, UpdateLog>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl CatalogManager {
|
|
|
|
|
/// Create a new catalog manager
|
2025-07-31 00:18:21 +02:00
|
|
|
pub fn new<P: AsRef<Path>>(base_dir: P, publisher: &str) -> Result<Self> {
|
2025-08-04 23:01:04 +02:00
|
|
|
let publisher_catalog_dir = base_dir.as_ref().to_path_buf();
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
// Create catalog directory if it doesn't exist
|
2025-07-31 00:18:21 +02:00
|
|
|
if !publisher_catalog_dir.exists() {
|
|
|
|
|
fs::create_dir_all(&publisher_catalog_dir)?;
|
2025-07-24 00:28:33 +02:00
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
// Try to load existing catalog attributes
|
2025-07-31 00:18:21 +02:00
|
|
|
let attrs_path = publisher_catalog_dir.join("catalog.attrs");
|
2025-07-24 00:28:33 +02:00
|
|
|
let attrs = if attrs_path.exists() {
|
|
|
|
|
CatalogAttrs::load(&attrs_path)?
|
|
|
|
|
} else {
|
|
|
|
|
CatalogAttrs::new()
|
|
|
|
|
};
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
Ok(CatalogManager {
|
2025-07-31 00:18:21 +02:00
|
|
|
catalog_dir: publisher_catalog_dir,
|
|
|
|
|
publisher: publisher.to_string(),
|
2025-07-24 00:28:33 +02:00
|
|
|
attrs,
|
|
|
|
|
parts: HashMap::new(),
|
|
|
|
|
update_logs: HashMap::new(),
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Get catalog attributes
|
|
|
|
|
pub fn attrs(&self) -> &CatalogAttrs {
|
|
|
|
|
&self.attrs
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Get mutable catalog attributes
|
|
|
|
|
pub fn attrs_mut(&mut self) -> &mut CatalogAttrs {
|
|
|
|
|
&mut self.attrs
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Get a catalog part
|
|
|
|
|
pub fn get_part(&self, name: &str) -> Option<&CatalogPart> {
|
|
|
|
|
self.parts.get(name)
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Get a mutable catalog part
|
|
|
|
|
pub fn get_part_mut(&mut self, name: &str) -> Option<&mut CatalogPart> {
|
|
|
|
|
self.parts.get_mut(name)
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Load a catalog part
|
|
|
|
|
pub fn load_part(&mut self, name: &str) -> Result<()> {
|
|
|
|
|
let part_path = self.catalog_dir.join(name);
|
|
|
|
|
if part_path.exists() {
|
|
|
|
|
let part = CatalogPart::load(&part_path)?;
|
|
|
|
|
self.parts.insert(name.to_string(), part);
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
2025-07-26 15:33:39 +02:00
|
|
|
Err(CatalogError::CatalogPartNotFound {
|
|
|
|
|
name: name.to_string(),
|
|
|
|
|
})
|
2025-07-24 00:28:33 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Save a catalog part
|
|
|
|
|
pub fn save_part(&self, name: &str) -> Result<()> {
|
|
|
|
|
if let Some(part) = self.parts.get(name) {
|
|
|
|
|
let part_path = self.catalog_dir.join(name);
|
|
|
|
|
part.save(&part_path)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
2025-07-26 15:33:39 +02:00
|
|
|
Err(CatalogError::CatalogPartNotLoaded {
|
|
|
|
|
name: name.to_string(),
|
|
|
|
|
})
|
2025-07-24 00:28:33 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Create a new catalog part
|
|
|
|
|
pub fn create_part(&mut self, name: &str) -> &mut CatalogPart {
|
2025-07-26 12:54:01 +02:00
|
|
|
self.parts
|
|
|
|
|
.entry(name.to_string())
|
|
|
|
|
.or_insert_with(CatalogPart::new)
|
2025-07-24 00:28:33 +02:00
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Save catalog attributes
|
|
|
|
|
pub fn save_attrs(&self) -> Result<()> {
|
|
|
|
|
let attrs_path = self.catalog_dir.join("catalog.attrs");
|
|
|
|
|
self.attrs.save(&attrs_path)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Create a new update log
|
|
|
|
|
pub fn create_update_log(&mut self, name: &str) -> &mut UpdateLog {
|
2025-07-26 12:54:01 +02:00
|
|
|
self.update_logs
|
|
|
|
|
.entry(name.to_string())
|
|
|
|
|
.or_insert_with(UpdateLog::new)
|
2025-07-24 00:28:33 +02:00
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Save an update log
|
|
|
|
|
pub fn save_update_log(&self, name: &str) -> Result<()> {
|
|
|
|
|
if let Some(log) = self.update_logs.get(name) {
|
|
|
|
|
let log_path = self.catalog_dir.join(name);
|
|
|
|
|
log.save(&log_path)?;
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
// Update catalog attributes
|
|
|
|
|
let now = SystemTime::now();
|
|
|
|
|
let timestamp = format_iso8601_basic(&now);
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
let mut attrs = self.attrs.clone();
|
2025-07-26 12:54:01 +02:00
|
|
|
attrs.updates.insert(
|
|
|
|
|
name.to_string(),
|
|
|
|
|
UpdateLogInfo {
|
|
|
|
|
last_modified: timestamp,
|
|
|
|
|
signature_sha1: None,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
let attrs_path = self.catalog_dir.join("catalog.attrs");
|
|
|
|
|
attrs.save(&attrs_path)?;
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
Ok(())
|
|
|
|
|
} else {
|
2025-07-26 15:33:39 +02:00
|
|
|
Err(CatalogError::UpdateLogNotLoaded {
|
|
|
|
|
name: name.to_string(),
|
|
|
|
|
})
|
2025-07-24 00:28:33 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Load an update log
|
|
|
|
|
pub fn load_update_log(&mut self, name: &str) -> Result<()> {
|
|
|
|
|
let log_path = self.catalog_dir.join(name);
|
|
|
|
|
if log_path.exists() {
|
|
|
|
|
let log = UpdateLog::load(&log_path)?;
|
|
|
|
|
self.update_logs.insert(name.to_string(), log);
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
2025-07-26 15:33:39 +02:00
|
|
|
Err(CatalogError::UpdateLogNotFound {
|
|
|
|
|
name: name.to_string(),
|
|
|
|
|
})
|
2025-07-24 00:28:33 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Get an update log
|
|
|
|
|
pub fn get_update_log(&self, name: &str) -> Option<&UpdateLog> {
|
|
|
|
|
self.update_logs.get(name)
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
|
2025-07-24 00:28:33 +02:00
|
|
|
/// Get a mutable update log
|
|
|
|
|
pub fn get_update_log_mut(&mut self, name: &str) -> Option<&mut UpdateLog> {
|
|
|
|
|
self.update_logs.get_mut(name)
|
|
|
|
|
}
|
2025-07-31 00:18:21 +02:00
|
|
|
|
|
|
|
|
/// Add a package to a catalog part using the stored publisher
|
|
|
|
|
pub fn add_package_to_part(
|
|
|
|
|
&mut self,
|
|
|
|
|
part_name: &str,
|
|
|
|
|
fmri: &Fmri,
|
|
|
|
|
actions: Option<Vec<String>>,
|
|
|
|
|
signature: Option<String>,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
if let Some(part) = self.parts.get_mut(part_name) {
|
|
|
|
|
part.add_package(&self.publisher, fmri, actions, signature);
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
|
|
|
|
Err(CatalogError::CatalogPartNotLoaded {
|
|
|
|
|
name: part_name.to_string(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Add an update to an update log using the stored publisher
|
|
|
|
|
pub fn add_update_to_log(
|
|
|
|
|
&mut self,
|
|
|
|
|
log_name: &str,
|
|
|
|
|
fmri: &Fmri,
|
|
|
|
|
op_type: CatalogOperationType,
|
|
|
|
|
catalog_parts: HashMap<String, HashMap<String, Vec<String>>>,
|
|
|
|
|
signature: Option<String>,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
if let Some(log) = self.update_logs.get_mut(log_name) {
|
|
|
|
|
log.add_update(&self.publisher, fmri, op_type, catalog_parts, signature);
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
|
|
|
|
Err(CatalogError::UpdateLogNotLoaded {
|
|
|
|
|
name: log_name.to_string(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-26 12:54:01 +02:00
|
|
|
}
|
2025-12-08 22:39:28 +01:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_load_sample_catalog() {
|
|
|
|
|
// Path is relative to the crate root (libips)
|
2025-12-22 20:11:08 +01:00
|
|
|
let path = PathBuf::from(
|
|
|
|
|
"../sample_data/sample-repo/publisher/openindiana.org/catalog/catalog.base.C",
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-08 22:39:28 +01:00
|
|
|
// Only run this test if the sample data exists
|
|
|
|
|
if path.exists() {
|
|
|
|
|
println!("Testing with sample catalog at {:?}", path);
|
|
|
|
|
match CatalogPart::load(&path) {
|
|
|
|
|
Ok(part) => {
|
|
|
|
|
println!("Successfully loaded catalog part");
|
2025-12-22 20:11:08 +01:00
|
|
|
|
2025-12-08 22:39:28 +01:00
|
|
|
// Verify we loaded the correct data structure
|
|
|
|
|
// The sample file has "openindiana.org" as a key
|
2025-12-22 20:11:08 +01:00
|
|
|
assert!(
|
|
|
|
|
part.packages.contains_key("openindiana.org"),
|
|
|
|
|
"Catalog should contain openindiana.org publisher"
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-08 22:39:28 +01:00
|
|
|
let packages = part.packages.get("openindiana.org").unwrap();
|
|
|
|
|
println!("Found {} packages for openindiana.org", packages.len());
|
|
|
|
|
assert!(packages.len() > 0, "Should have loaded packages");
|
2025-12-22 20:11:08 +01:00
|
|
|
}
|
2025-12-08 22:39:28 +01:00
|
|
|
Err(e) => panic!("Failed to load catalog part: {}", e),
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2025-12-22 20:11:08 +01:00
|
|
|
println!(
|
|
|
|
|
"Sample data not found at {:?}, skipping test. This is expected in some CI environments.",
|
|
|
|
|
path
|
|
|
|
|
);
|
2025-12-08 22:39:28 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|