use crate::error::{Pkg6RepoError, Result}; use libips::actions::{File as FileAction, Manifest}; use libips::repository::{FileBackend, ReadableRepository, WritableRepository}; use std::fs::{self, File}; use std::io::{BufRead, BufReader, Read, Seek}; use std::path::{Path, PathBuf}; use tempfile::tempdir; use tracing::{debug, error, info, trace, warn}; /// Represents a pkg5 repository importer pub struct Pkg5Importer { /// Path to the pkg5 repository (directory or p5p archive) source_path: PathBuf, /// Path to the destination repository dest_path: PathBuf, /// Whether the source is a p5p archive is_p5p: bool, /// Temporary directory for extraction (if source is a p5p archive) temp_dir: Option, } impl Pkg5Importer { /// Creates a new Pkg5Importer pub fn new>(source_path: P, dest_path: P) -> Result { let source_path = source_path.as_ref().to_path_buf(); let dest_path = dest_path.as_ref().to_path_buf(); debug!("Creating Pkg5Importer with source: {}, destination: {}", source_path.display(), dest_path.display()); // Check if source path exists if !source_path.exists() { debug!("Source path does not exist: {}", source_path.display()); return Err(Pkg6RepoError::from(format!( "Source path does not exist: {}", source_path.display() ))); } debug!("Source path exists: {}", source_path.display()); // Determine if source is a p5p archive let is_p5p = source_path.is_file() && source_path.extension().map_or(false, |ext| ext == "p5p"); debug!("Source is p5p archive: {}", is_p5p); Ok(Self { source_path, dest_path, is_p5p, temp_dir: None, }) } /// Prepares the source repository for import fn prepare_source(&mut self) -> Result { if self.is_p5p { // Create a temporary directory for extraction let temp_dir = tempdir().map_err(|e| { Pkg6RepoError::from(format!("Failed to create temporary directory: {}", e)) })?; info!("Extracting p5p archive to temporary directory: {}", temp_dir.path().display()); // Extract the p5p archive to the temporary directory let status = std::process::Command::new("tar") .arg("-xf") .arg(&self.source_path) .arg("-C") .arg(temp_dir.path()) .status() .map_err(|e| { Pkg6RepoError::from(format!("Failed to extract p5p archive: {}", e)) })?; if !status.success() { return Err(Pkg6RepoError::from(format!( "Failed to extract p5p archive: {}", status ))); } // Store the temporary directory let source_path = temp_dir.path().to_path_buf(); self.temp_dir = Some(temp_dir); Ok(source_path) } else { // Source is already a directory Ok(self.source_path.clone()) } } /// Imports the pkg5 repository pub fn import(&mut self, publisher: Option<&str>) -> Result<()> { debug!("Starting import with publisher: {:?}", publisher); // Prepare the source repository debug!("Preparing source repository"); let source_path = self.prepare_source()?; debug!("Source repository prepared: {}", source_path.display()); // Check if this is a pkg5 repository let pkg5_repo_file = source_path.join("pkg5.repository"); let pkg5_index_file = source_path.join("pkg5.index.0.gz"); debug!("Checking if pkg5.repository exists: {}", pkg5_repo_file.exists()); debug!("Checking if pkg5.index.0.gz exists: {}", pkg5_index_file.exists()); if !pkg5_repo_file.exists() && !pkg5_index_file.exists() { debug!("Source does not appear to be a pkg5 repository: {}", source_path.display()); return Err(Pkg6RepoError::from(format!( "Source does not appear to be a pkg5 repository: {}", source_path.display() ))); } // Open or create the destination repository debug!("Checking if destination repository exists: {}", self.dest_path.exists()); let mut dest_repo = if self.dest_path.exists() { // Check if it's a valid repository by looking for the pkg6.repository file let repo_config_file = self.dest_path.join("pkg6.repository"); debug!("Checking if repository config file exists: {}", repo_config_file.exists()); if repo_config_file.exists() { // It's a valid repository, open it info!("Opening existing repository: {}", self.dest_path.display()); debug!("Attempting to open repository at: {}", self.dest_path.display()); FileBackend::open(&self.dest_path)? } else { // It's not a valid repository, create a new one info!("Destination exists but is not a valid repository, creating a new one: {}", self.dest_path.display()); debug!("Attempting to create repository at: {}", self.dest_path.display()); FileBackend::create(&self.dest_path, libips::repository::RepositoryVersion::V4)? } } else { // Destination doesn't exist, create a new repository info!("Creating new repository: {}", self.dest_path.display()); debug!("Attempting to create repository at: {}", self.dest_path.display()); FileBackend::create(&self.dest_path, libips::repository::RepositoryVersion::V4)? }; // Find publishers in the source repository let publishers = self.find_publishers(&source_path)?; if publishers.is_empty() { return Err(Pkg6RepoError::from( "No publishers found in source repository".to_string() )); } // Determine which publisher to import let publisher_to_import = match publisher { Some(pub_name) => { if !publishers.iter().any(|p| p == pub_name) { return Err(Pkg6RepoError::from(format!( "Publisher not found in source repository: {}", pub_name ))); } pub_name }, None => { // Use the first publisher if none specified &publishers[0] } }; info!("Importing from publisher: {}", publisher_to_import); // Ensure the publisher exists in the destination repository if !dest_repo.config.publishers.iter().any(|p| p == publisher_to_import) { info!("Adding publisher to destination repository: {}", publisher_to_import); dest_repo.add_publisher(publisher_to_import)?; // Set as default publisher if there isn't one already if dest_repo.config.default_publisher.is_none() { info!("Setting as default publisher: {}", publisher_to_import); dest_repo.set_default_publisher(publisher_to_import)?; } } // Import packages self.import_packages(&source_path, &mut dest_repo, publisher_to_import)?; // Rebuild catalog and search index info!("Rebuilding catalog and search index..."); dest_repo.rebuild(Some(publisher_to_import), false, false)?; info!("Import completed successfully"); Ok(()) } /// Finds publishers in the source repository fn find_publishers(&self, source_path: &Path) -> Result> { let publisher_dir = source_path.join("publisher"); if !publisher_dir.exists() || !publisher_dir.is_dir() { return Err(Pkg6RepoError::from(format!( "Publisher directory not found: {}", publisher_dir.display() ))); } let mut publishers = Vec::new(); for entry in fs::read_dir(&publisher_dir).map_err(|e| { Pkg6RepoError::IoError(e) })? { let entry = entry.map_err(|e| { Pkg6RepoError::IoError(e) })?; let path = entry.path(); if path.is_dir() { let publisher = path.file_name().unwrap().to_string_lossy().to_string(); publishers.push(publisher); } } Ok(publishers) } /// Imports packages from the source repository fn import_packages(&self, source_path: &Path, dest_repo: &mut FileBackend, publisher: &str) -> Result<()> { let pkg_dir = source_path.join("publisher").join(publisher).join("pkg"); if !pkg_dir.exists() || !pkg_dir.is_dir() { return Err(Pkg6RepoError::from(format!( "Package directory not found: {}", pkg_dir.display() ))); } // Create a temporary directory for extracted files let temp_proto_dir = tempdir().map_err(|e| { Pkg6RepoError::from(format!("Failed to create temporary prototype directory: {}", e)) })?; info!("Created temporary prototype directory: {}", temp_proto_dir.path().display()); // Find package directories let mut package_count = 0; for pkg_entry in fs::read_dir(&pkg_dir).map_err(|e| { Pkg6RepoError::IoError(e) })? { let pkg_entry = pkg_entry.map_err(|e| { Pkg6RepoError::IoError(e) })?; let pkg_path = pkg_entry.path(); if pkg_path.is_dir() { // This is a package directory let pkg_name = pkg_path.file_name().unwrap().to_string_lossy().to_string(); let decoded_pkg_name = url_decode(&pkg_name); debug!("Processing package: {}", decoded_pkg_name); // Find package versions for ver_entry in fs::read_dir(&pkg_path).map_err(|e| { Pkg6RepoError::IoError(e) })? { let ver_entry = ver_entry.map_err(|e| { Pkg6RepoError::IoError(e) })?; let ver_path = ver_entry.path(); if ver_path.is_file() { // This is a package version let ver_name = ver_path.file_name().unwrap().to_string_lossy().to_string(); let decoded_ver_name = url_decode(&ver_name); debug!("Processing version: {}", decoded_ver_name); // Import this package version self.import_package_version( source_path, dest_repo, publisher, &ver_path, &decoded_pkg_name, &decoded_ver_name, temp_proto_dir.path(), )?; package_count += 1; } } } } info!("Imported {} packages", package_count); Ok(()) } /// Imports a specific package version fn import_package_version( &self, source_path: &Path, dest_repo: &mut FileBackend, publisher: &str, manifest_path: &Path, pkg_name: &str, ver_name: &str, proto_dir: &Path, ) -> Result<()> { debug!("Importing package version from {}", manifest_path.display()); // Extract package name from FMRI debug!("Extracted package name from FMRI: {}", pkg_name); // Read the manifest file content debug!("Reading manifest file content from {}", manifest_path.display()); let manifest_content = fs::read_to_string(manifest_path).map_err(|e| { debug!("Error reading manifest file: {}", e); Pkg6RepoError::IoError(e) })?; // Parse the manifest using parse_string debug!("Parsing manifest content"); let manifest = Manifest::parse_string(manifest_content)?; // Begin a transaction debug!("Beginning transaction"); let mut transaction = dest_repo.begin_transaction()?; // Set the publisher for the transaction debug!("Using specified publisher: {}", publisher); transaction.set_publisher(publisher); // Debug the repository structure debug!("Publisher directory: {}", dest_repo.path.join("pkg").join(publisher).display()); // Extract files referenced in the manifest let file_dir = source_path.join("publisher").join(publisher).join("file"); if !file_dir.exists() || !file_dir.is_dir() { return Err(Pkg6RepoError::from(format!( "File directory not found: {}", file_dir.display() ))); } // Process file actions for file_action in manifest.files.iter() { // Extract the hash from the file action's payload if let Some(payload) = &file_action.payload { let hash = payload.primary_identifier.hash.clone(); // Determine the file path in the source repository let hash_prefix = &hash[0..2]; let file_path = file_dir.join(hash_prefix).join(&hash); if !file_path.exists() { warn!("File not found in source repository: {}", file_path.display()); continue; } // Extract the file to the prototype directory let proto_file_path = proto_dir.join(&file_action.path); // Create parent directories if they don't exist if let Some(parent) = proto_file_path.parent() { fs::create_dir_all(parent).map_err(|e| { Pkg6RepoError::IoError(e) })?; } // Extract the gzipped file let mut source_file = File::open(&file_path).map_err(|e| { Pkg6RepoError::IoError(e) })?; let mut dest_file = File::create(&proto_file_path).map_err(|e| { Pkg6RepoError::IoError(e) })?; // Check if the file is gzipped let mut header = [0; 2]; source_file.read_exact(&mut header).map_err(|e| { Pkg6RepoError::IoError(e) })?; // Reset file position source_file.seek(std::io::SeekFrom::Start(0)).map_err(|e| { Pkg6RepoError::IoError(e) })?; if header[0] == 0x1f && header[1] == 0x8b { // File is gzipped, decompress it let mut decoder = flate2::read::GzDecoder::new(source_file); std::io::copy(&mut decoder, &mut dest_file).map_err(|e| { Pkg6RepoError::IoError(e) })?; } else { // File is not gzipped, copy it as is std::io::copy(&mut source_file, &mut dest_file).map_err(|e| { Pkg6RepoError::IoError(e) })?; } // Add the file to the transaction transaction.add_file(file_action.clone(), &proto_file_path)?; } } // Update the manifest in the transaction transaction.update_manifest(manifest); // Create the parent directories for the package name let package_dir = dest_repo.path.join("pkg").join(publisher).join(pkg_name); debug!("Creating package directory: {}", package_dir.display()); fs::create_dir_all(&package_dir)?; // Commit the transaction transaction.commit()?; Ok(()) } } /// URL decodes a string fn url_decode(s: &str) -> String { let mut result = String::new(); let mut i = 0; while i < s.len() { if s[i..].starts_with("%") && i + 2 < s.len() { if let Ok(hex) = u8::from_str_radix(&s[i+1..i+3], 16) { result.push(hex as char); i += 3; } else { result.push('%'); i += 1; } } else { result.push(s[i..].chars().next().unwrap()); i += 1; } } result } #[cfg(test)] mod tests { use super::*; #[test] fn test_url_decode() { assert_eq!(url_decode("test"), "test"); assert_eq!(url_decode("test%20test"), "test test"); assert_eq!(url_decode("test%2Ftest"), "test/test"); assert_eq!(url_decode("test%2Ctest"), "test,test"); assert_eq!(url_decode("test%3Atest"), "test:test"); } }