Refactor repository info retrieval to return structured data, add detailed publisher information, and update tests accordingly

Signed-off-by: Till Wegmueller <toasterson@gmail.com>
This commit is contained in:
Till Wegmueller 2025-07-22 10:21:16 +02:00
parent a9584fa6d2
commit 7332e0f7b5
No known key found for this signature in database
5 changed files with 196 additions and 36 deletions

View file

@ -19,7 +19,7 @@ use crate::actions::{Manifest, File as FileAction};
use crate::digest::Digest; use crate::digest::Digest;
use crate::payload::{Payload, PayloadCompressionAlgorithm}; 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 /// Repository implementation that uses the local filesystem
pub struct FileBackend { pub struct FileBackend {
@ -27,6 +27,27 @@ pub struct FileBackend {
pub config: RepositoryConfig, 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 /// Transaction for publishing packages
pub struct Transaction { pub struct Transaction {
/// Unique ID for the transaction /// Unique ID for the transaction
@ -357,23 +378,62 @@ impl Repository for FileBackend {
} }
/// Get repository information /// Get repository information
fn get_info(&self) -> Result<Vec<(String, usize, String, String)>> { fn get_info(&self) -> Result<RepositoryInfo> {
let mut info = Vec::new(); let mut publishers = Vec::new();
for publisher in &self.config.publishers { for publisher_name in &self.config.publishers {
// Count packages (this is a placeholder, actual implementation would count packages) // Count packages by scanning the pkg/<publisher> directory
let package_count = 0; 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
let status = "online".to_string(); 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();
// Updated timestamp (placeholder) // Skip directories, only count files (package manifests)
let updated = "2025-07-21T18:46:00.000000Z".to_string(); if path.is_file() {
package_count += 1;
info.push((publisher.clone(), package_count, status, updated)); // 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;
}
}
}
}
}
}
} }
Ok(info) // Status is always "online" for file-based repositories
let status = "online".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)
};
// Create a PublisherInfo struct and add it to the list
publishers.push(PublisherInfo {
name: publisher_name.clone(),
package_count,
status,
updated,
});
}
// Create and return a RepositoryInfo struct
Ok(RepositoryInfo { publishers })
} }
/// Set a repository property /// Set a repository property

View file

@ -16,6 +16,26 @@ pub use rest_backend::RestBackend;
/// Repository configuration filename /// Repository configuration filename
pub const REPOSITORY_CONFIG_FILENAME: &str = "pkg6.repository"; 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<PublisherInfo>,
}
/// Repository version /// Repository version
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum RepositoryVersion { pub enum RepositoryVersion {
@ -77,7 +97,7 @@ pub trait Repository {
fn remove_publisher(&mut self, publisher: &str, dry_run: bool) -> Result<()>; fn remove_publisher(&mut self, publisher: &str, dry_run: bool) -> Result<()>;
/// Get repository information /// Get repository information
fn get_info(&self) -> Result<Vec<(String, usize, String, String)>>; fn get_info(&self) -> Result<RepositoryInfo>;
/// Set a repository property /// Set a repository property
fn set_property(&mut self, property: &str, value: &str) -> Result<()>; fn set_property(&mut self, property: &str, value: &str) -> Result<()>;

View file

@ -6,7 +6,7 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use super::{Repository, RepositoryConfig, RepositoryVersion}; use super::{Repository, RepositoryConfig, RepositoryVersion, PublisherInfo, RepositoryInfo};
/// Repository implementation that uses a REST API /// Repository implementation that uses a REST API
pub struct RestBackend { pub struct RestBackend {
@ -105,22 +105,29 @@ impl Repository for RestBackend {
} }
/// Get repository information /// Get repository information
fn get_info(&self) -> Result<Vec<(String, usize, String, String)>> { fn get_info(&self) -> Result<RepositoryInfo> {
// This is a stub implementation // This is a stub implementation
// In a real implementation, we would make a REST API call to get repository information // 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 // In a real implementation, we would get this information from the REST API
let package_count = 0; let package_count = 0;
let status = "online".to_string(); let status = "online".to_string();
let updated = "2025-07-21T18:46:00.000000Z".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 /// Set a repository property

View file

@ -3,7 +3,7 @@ use anyhow::{Result, anyhow};
use std::path::PathBuf; use std::path::PathBuf;
use std::convert::TryFrom; use std::convert::TryFrom;
use libips::repository::{Repository, RepositoryVersion, FileBackend}; use libips::repository::{Repository, RepositoryVersion, FileBackend, PublisherInfo, RepositoryInfo};
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@ -321,7 +321,7 @@ fn main() -> Result<()> {
let repo = FileBackend::open(repo_uri_or_path)?; let repo = FileBackend::open(repo_uri_or_path)?;
// Get repository info // Get repository info
let info = repo.get_info()?; let repo_info = repo.get_info()?;
// Print headers if not omitted // Print headers if not omitted
if !omit_headers { if !omit_headers {
@ -329,8 +329,13 @@ fn main() -> Result<()> {
} }
// Print repository info // Print repository info
for (pub_name, pkg_count, status, updated) in info { for publisher_info in repo_info.publishers {
println!("{:<10} {:<8} {:<6} {:<30}", pub_name, pkg_count, status, updated); println!("{:<10} {:<8} {:<6} {:<30}",
publisher_info.name,
publisher_info.package_count,
publisher_info.status,
publisher_info.updated
);
} }
Ok(()) Ok(())

View file

@ -1,13 +1,43 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use libips::repository::{Repository, RepositoryVersion, FileBackend, REPOSITORY_CONFIG_FILENAME}; use libips::repository::{Repository, RepositoryVersion, FileBackend, REPOSITORY_CONFIG_FILENAME, PublisherInfo, RepositoryInfo};
use tempfile::tempdir; 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] #[test]
fn test_create_repository() { fn test_create_repository() {
// Create a temporary directory for the test // Create a real test directory
let temp_dir = tempdir().unwrap(); let test_dir = create_test_dir("create_repository");
let repo_path = temp_dir.path().join("repo"); let repo_path = test_dir.join("repo");
// Create a repository // Create a repository
let _ = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap(); let _ = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap();
@ -20,13 +50,16 @@ mod tests {
assert!(repo_path.join("pkg").exists()); assert!(repo_path.join("pkg").exists());
assert!(repo_path.join("trans").exists()); assert!(repo_path.join("trans").exists());
assert!(repo_path.join(REPOSITORY_CONFIG_FILENAME).exists()); assert!(repo_path.join(REPOSITORY_CONFIG_FILENAME).exists());
// Clean up
cleanup_test_dir(&test_dir);
} }
#[test] #[test]
fn test_add_publisher() { fn test_add_publisher() {
// Create a temporary directory for the test // Create a real test directory
let temp_dir = tempdir().unwrap(); let test_dir = create_test_dir("add_publisher");
let repo_path = temp_dir.path().join("repo"); let repo_path = test_dir.join("repo");
// Create a repository // Create a repository
let mut repo = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap(); 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.config.publishers.contains(&"example.com".to_string()));
assert!(repo_path.join("catalog").join("example.com").exists()); assert!(repo_path.join("catalog").join("example.com").exists());
assert!(repo_path.join("pkg").join("example.com").exists()); assert!(repo_path.join("pkg").join("example.com").exists());
// Clean up
cleanup_test_dir(&test_dir);
} }
#[test] #[test]
fn test_remove_publisher() { fn test_remove_publisher() {
// Create a temporary directory for the test // Create a real test directory
let temp_dir = tempdir().unwrap(); let test_dir = create_test_dir("remove_publisher");
let repo_path = temp_dir.path().join("repo"); let repo_path = test_dir.join("repo");
// Create a repository // Create a repository
let mut repo = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap(); let mut repo = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap();
@ -60,13 +96,16 @@ mod tests {
// Check that the publisher was removed // Check that the publisher was removed
assert!(!repo.config.publishers.contains(&"example.com".to_string())); assert!(!repo.config.publishers.contains(&"example.com".to_string()));
// Clean up
cleanup_test_dir(&test_dir);
} }
#[test] #[test]
fn test_set_property() { fn test_set_property() {
// Create a temporary directory for the test // Create a real test directory
let temp_dir = tempdir().unwrap(); let test_dir = create_test_dir("set_property");
let repo_path = temp_dir.path().join("repo"); let repo_path = test_dir.join("repo");
// Create a repository // Create a repository
let mut repo = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap(); let mut repo = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap();
@ -76,5 +115,34 @@ mod tests {
// Check that the property was set // Check that the property was set
assert_eq!(repo.config.properties.get("publisher/prefix").unwrap(), "example.com"); 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);
} }
} }