From 7332e0f7b59e9612a58b0ecb734c1ebc2deb38af Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Tue, 22 Jul 2025 10:21:16 +0200 Subject: [PATCH] Refactor repository info retrieval to return structured data, add detailed publisher information, and update tests accordingly Signed-off-by: Till Wegmueller --- libips/src/repository/file_backend.rs | 82 ++++++++++++++++++++--- libips/src/repository/mod.rs | 22 +++++- libips/src/repository/rest_backend.rs | 19 ++++-- pkg6repo/src/main.rs | 13 ++-- pkg6repo/src/tests.rs | 96 +++++++++++++++++++++++---- 5 files changed, 196 insertions(+), 36 deletions(-) diff --git a/libips/src/repository/file_backend.rs b/libips/src/repository/file_backend.rs index 6fad659..bf68e48 100644 --- a/libips/src/repository/file_backend.rs +++ b/libips/src/repository/file_backend.rs @@ -19,7 +19,7 @@ use crate::actions::{Manifest, File as FileAction}; use crate::digest::Digest; use crate::payload::{Payload, PayloadCompressionAlgorithm}; -use super::{Repository, RepositoryConfig, RepositoryVersion, REPOSITORY_CONFIG_FILENAME}; +use super::{Repository, RepositoryConfig, RepositoryVersion, REPOSITORY_CONFIG_FILENAME, PublisherInfo, RepositoryInfo}; /// Repository implementation that uses the local filesystem pub struct FileBackend { @@ -27,6 +27,27 @@ pub struct FileBackend { pub config: RepositoryConfig, } +/// Format a SystemTime as an ISO 8601 timestamp string +fn format_iso8601_timestamp(time: &SystemTime) -> String { + let duration = time.duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_else(|_| std::time::Duration::from_secs(0)); + + let secs = duration.as_secs(); + let micros = duration.subsec_micros(); + + // Format as ISO 8601 with microsecond precision + format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z", + // Convert seconds to date and time components + 1970 + secs / 31536000, // year (approximate) + (secs % 31536000) / 2592000 + 1, // month (approximate) + (secs % 2592000) / 86400 + 1, // day (approximate) + (secs % 86400) / 3600, // hour + (secs % 3600) / 60, // minute + secs % 60, // second + micros // microseconds + ) +} + /// Transaction for publishing packages pub struct Transaction { /// Unique ID for the transaction @@ -357,23 +378,62 @@ impl Repository for FileBackend { } /// Get repository information - fn get_info(&self) -> Result> { - let mut info = Vec::new(); + fn get_info(&self) -> Result { + let mut publishers = Vec::new(); - for publisher in &self.config.publishers { - // Count packages (this is a placeholder, actual implementation would count packages) - let package_count = 0; + for publisher_name in &self.config.publishers { + // Count packages by scanning the pkg/ directory + let publisher_pkg_dir = self.path.join("pkg").join(publisher_name); + let mut package_count = 0; + let mut latest_timestamp = SystemTime::UNIX_EPOCH; - // Status is always "online" for now + // Check if the publisher directory exists + if publisher_pkg_dir.exists() { + // Walk through the directory and count package manifests + if let Ok(entries) = fs::read_dir(&publisher_pkg_dir) { + for entry in entries.flatten() { + let path = entry.path(); + + // Skip directories, only count files (package manifests) + if path.is_file() { + package_count += 1; + + // Update the latest timestamp if this file is newer + if let Ok(metadata) = fs::metadata(&path) { + if let Ok(modified) = metadata.modified() { + if modified > latest_timestamp { + latest_timestamp = modified; + } + } + } + } + } + } + } + + // Status is always "online" for file-based repositories let status = "online".to_string(); - // Updated timestamp (placeholder) - let updated = "2025-07-21T18:46:00.000000Z".to_string(); + // Format the timestamp in ISO 8601 format + let updated = if latest_timestamp == SystemTime::UNIX_EPOCH { + // If no files were found, use current time + let now = SystemTime::now(); + format_iso8601_timestamp(&now) + } else { + format_iso8601_timestamp(&latest_timestamp) + }; - info.push((publisher.clone(), package_count, status, updated)); + // Create a PublisherInfo struct and add it to the list + publishers.push(PublisherInfo { + name: publisher_name.clone(), + package_count, + status, + updated, + }); } - Ok(info) + // Create and return a RepositoryInfo struct + Ok(RepositoryInfo { publishers }) } /// Set a repository property diff --git a/libips/src/repository/mod.rs b/libips/src/repository/mod.rs index 54740f3..425c13d 100644 --- a/libips/src/repository/mod.rs +++ b/libips/src/repository/mod.rs @@ -16,6 +16,26 @@ pub use rest_backend::RestBackend; /// Repository configuration filename pub const REPOSITORY_CONFIG_FILENAME: &str = "pkg6.repository"; +/// Information about a publisher in a repository +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PublisherInfo { + /// Name of the publisher + pub name: String, + /// Number of packages from this publisher + pub package_count: usize, + /// Status of the publisher (e.g., "online", "offline") + pub status: String, + /// Last updated timestamp in ISO 8601 format + pub updated: String, +} + +/// Information about a repository +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RepositoryInfo { + /// Information about publishers in the repository + pub publishers: Vec, +} + /// Repository version #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum RepositoryVersion { @@ -77,7 +97,7 @@ pub trait Repository { fn remove_publisher(&mut self, publisher: &str, dry_run: bool) -> Result<()>; /// Get repository information - fn get_info(&self) -> Result>; + fn get_info(&self) -> Result; /// Set a repository property fn set_property(&mut self, property: &str, value: &str) -> Result<()>; diff --git a/libips/src/repository/rest_backend.rs b/libips/src/repository/rest_backend.rs index 358a53b..5c7518c 100644 --- a/libips/src/repository/rest_backend.rs +++ b/libips/src/repository/rest_backend.rs @@ -6,7 +6,7 @@ use anyhow::{anyhow, Result}; use std::path::{Path, PathBuf}; -use super::{Repository, RepositoryConfig, RepositoryVersion}; +use super::{Repository, RepositoryConfig, RepositoryVersion, PublisherInfo, RepositoryInfo}; /// Repository implementation that uses a REST API pub struct RestBackend { @@ -105,22 +105,29 @@ impl Repository for RestBackend { } /// Get repository information - fn get_info(&self) -> Result> { + fn get_info(&self) -> Result { // This is a stub implementation // In a real implementation, we would make a REST API call to get repository information - let mut info = Vec::new(); + let mut publishers = Vec::new(); - for publisher in &self.config.publishers { + for publisher_name in &self.config.publishers { // In a real implementation, we would get this information from the REST API let package_count = 0; let status = "online".to_string(); let updated = "2025-07-21T18:46:00.000000Z".to_string(); - info.push((publisher.clone(), package_count, status, updated)); + // Create a PublisherInfo struct and add it to the list + publishers.push(PublisherInfo { + name: publisher_name.clone(), + package_count, + status, + updated, + }); } - Ok(info) + // Create and return a RepositoryInfo struct + Ok(RepositoryInfo { publishers }) } /// Set a repository property diff --git a/pkg6repo/src/main.rs b/pkg6repo/src/main.rs index 53ac337..0210a0b 100644 --- a/pkg6repo/src/main.rs +++ b/pkg6repo/src/main.rs @@ -3,7 +3,7 @@ use anyhow::{Result, anyhow}; use std::path::PathBuf; use std::convert::TryFrom; -use libips::repository::{Repository, RepositoryVersion, FileBackend}; +use libips::repository::{Repository, RepositoryVersion, FileBackend, PublisherInfo, RepositoryInfo}; #[cfg(test)] mod tests; @@ -321,7 +321,7 @@ fn main() -> Result<()> { let repo = FileBackend::open(repo_uri_or_path)?; // Get repository info - let info = repo.get_info()?; + let repo_info = repo.get_info()?; // Print headers if not omitted if !omit_headers { @@ -329,8 +329,13 @@ fn main() -> Result<()> { } // Print repository info - for (pub_name, pkg_count, status, updated) in info { - println!("{:<10} {:<8} {:<6} {:<30}", pub_name, pkg_count, status, updated); + for publisher_info in repo_info.publishers { + println!("{:<10} {:<8} {:<6} {:<30}", + publisher_info.name, + publisher_info.package_count, + publisher_info.status, + publisher_info.updated + ); } Ok(()) diff --git a/pkg6repo/src/tests.rs b/pkg6repo/src/tests.rs index 225d49f..abc74c2 100644 --- a/pkg6repo/src/tests.rs +++ b/pkg6repo/src/tests.rs @@ -1,13 +1,43 @@ #[cfg(test)] mod tests { - use libips::repository::{Repository, RepositoryVersion, FileBackend, REPOSITORY_CONFIG_FILENAME}; - use tempfile::tempdir; + use libips::repository::{Repository, RepositoryVersion, FileBackend, REPOSITORY_CONFIG_FILENAME, PublisherInfo, RepositoryInfo}; + use std::path::PathBuf; + use std::fs; + + // These tests interact with real repositories in a known location + // instead of using temporary directories. This allows for better + // debugging and inspection of the repositories during testing. + + // The base directory for all test repositories + const TEST_REPO_BASE_DIR: &str = "/tmp/pkg6repo_test"; + + // Helper function to create a unique test directory + fn create_test_dir(test_name: &str) -> PathBuf { + let test_dir = PathBuf::from(format!("{}/{}", TEST_REPO_BASE_DIR, test_name)); + + // Clean up any existing directory + if test_dir.exists() { + fs::remove_dir_all(&test_dir).unwrap(); + } + + // Create the directory + fs::create_dir_all(&test_dir).unwrap(); + + test_dir + } + + // Helper function to clean up test directory + fn cleanup_test_dir(test_dir: &PathBuf) { + if test_dir.exists() { + fs::remove_dir_all(test_dir).unwrap(); + } + } #[test] fn test_create_repository() { - // Create a temporary directory for the test - let temp_dir = tempdir().unwrap(); - let repo_path = temp_dir.path().join("repo"); + // Create a real test directory + let test_dir = create_test_dir("create_repository"); + let repo_path = test_dir.join("repo"); // Create a repository let _ = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap(); @@ -20,13 +50,16 @@ mod tests { assert!(repo_path.join("pkg").exists()); assert!(repo_path.join("trans").exists()); assert!(repo_path.join(REPOSITORY_CONFIG_FILENAME).exists()); + + // Clean up + cleanup_test_dir(&test_dir); } #[test] fn test_add_publisher() { - // Create a temporary directory for the test - let temp_dir = tempdir().unwrap(); - let repo_path = temp_dir.path().join("repo"); + // Create a real test directory + let test_dir = create_test_dir("add_publisher"); + let repo_path = test_dir.join("repo"); // Create a repository let mut repo = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap(); @@ -38,13 +71,16 @@ mod tests { assert!(repo.config.publishers.contains(&"example.com".to_string())); assert!(repo_path.join("catalog").join("example.com").exists()); assert!(repo_path.join("pkg").join("example.com").exists()); + + // Clean up + cleanup_test_dir(&test_dir); } #[test] fn test_remove_publisher() { - // Create a temporary directory for the test - let temp_dir = tempdir().unwrap(); - let repo_path = temp_dir.path().join("repo"); + // Create a real test directory + let test_dir = create_test_dir("remove_publisher"); + let repo_path = test_dir.join("repo"); // Create a repository let mut repo = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap(); @@ -60,13 +96,16 @@ mod tests { // Check that the publisher was removed assert!(!repo.config.publishers.contains(&"example.com".to_string())); + + // Clean up + cleanup_test_dir(&test_dir); } #[test] fn test_set_property() { - // Create a temporary directory for the test - let temp_dir = tempdir().unwrap(); - let repo_path = temp_dir.path().join("repo"); + // Create a real test directory + let test_dir = create_test_dir("set_property"); + let repo_path = test_dir.join("repo"); // Create a repository let mut repo = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap(); @@ -76,5 +115,34 @@ mod tests { // Check that the property was set assert_eq!(repo.config.properties.get("publisher/prefix").unwrap(), "example.com"); + + // Clean up + cleanup_test_dir(&test_dir); + } + + #[test] + fn test_get_info() { + // Create a real test directory + let test_dir = create_test_dir("get_info"); + let repo_path = test_dir.join("repo"); + + // Create a repository + let mut repo = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap(); + + // Add a publisher + repo.add_publisher("example.com").unwrap(); + + // Get repository information + let repo_info = repo.get_info().unwrap(); + + // Check that the information is correct + assert_eq!(repo_info.publishers.len(), 1); + let publisher_info = &repo_info.publishers[0]; + assert_eq!(publisher_info.name, "example.com"); + assert_eq!(publisher_info.package_count, 0); // No packages yet + assert_eq!(publisher_info.status, "online"); + + // Clean up + cleanup_test_dir(&test_dir); } } \ No newline at end of file