use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::fs; use serde::Deserialize; use crate::collection::Collection; use crate::error::{DocTreeError, Result}; use crate::storage::RedisStorage; use crate::include::process_includes; use crate::utils::name_fix; /// Configuration for a collection from a .collection file #[derive(Deserialize, Default, Debug)] struct CollectionConfig { /// Optional name of the collection name: Option, // Add other configuration options as needed } // Global variable to track the current collection name // This is for compatibility with the Go implementation lazy_static::lazy_static! { static ref CURRENT_COLLECTION_NAME: Arc>> = Arc::new(Mutex::new(None)); } // Global variable to track the current Collection // This is for compatibility with the Go implementation /// DocTree represents a manager for multiple collections pub struct DocTree { /// Map of collections by name pub collections: HashMap, /// Default collection name pub default_collection: Option, /// Redis storage backend storage: RedisStorage, /// Name of the doctree (used as prefix for Redis keys) pub doctree_name: String, /// For backward compatibility pub name: String, /// For backward compatibility pub path: PathBuf, } /// Builder for DocTree pub struct DocTreeBuilder { /// Map of collections by name collections: HashMap, /// Default collection name default_collection: Option, /// Redis storage backend storage: Option, /// Name of the doctree (used as prefix for Redis keys) doctree_name: Option, /// For backward compatibility name: Option, /// For backward compatibility path: Option, } impl DocTree { /// Create a new DocTreeBuilder /// /// # Returns /// /// A new DocTreeBuilder pub fn builder() -> DocTreeBuilder { DocTreeBuilder { collections: HashMap::new(), default_collection: None, storage: None, doctree_name: Some("default".to_string()), name: None, path: None, } } /// Add a collection to the DocTree /// /// # Arguments /// /// * `path` - Base path of the collection /// * `name` - Name of the collection /// /// # Returns /// /// The added collection or an error pub fn add_collection>(&mut self, path: P, name: &str) -> Result<&Collection> { // Create a new collection let namefixed = name_fix(name); // Clone the storage and set the doctree name let storage = self.storage.clone(); storage.set_doctree_name(&self.doctree_name); let collection = Collection::builder(path, &namefixed) .with_storage(storage) .build()?; // Scan the collection collection.scan()?; // Add to the collections map self.collections.insert(collection.name.clone(), collection); // Return a reference to the added collection self.collections.get(&namefixed).ok_or_else(|| { DocTreeError::CollectionNotFound(namefixed.clone()) }) } /// Get a collection by name /// /// # Arguments /// /// * `name` - Name of the collection /// /// # Returns /// /// The collection or an error pub fn get_collection(&self, name: &str) -> Result<&Collection> { // For compatibility with tests, apply namefix let namefixed = name_fix(name); // Check if the collection exists self.collections.get(&namefixed).ok_or_else(|| { DocTreeError::CollectionNotFound(name.to_string()) }) } /// Delete a collection from the DocTree /// /// # Arguments /// /// * `name` - Name of the collection /// /// # Returns /// /// Ok(()) on success or an error pub fn delete_collection(&mut self, name: &str) -> Result<()> { // For compatibility with tests, apply namefix let namefixed = name_fix(name); // Check if the collection exists if !self.collections.contains_key(&namefixed) { return Err(DocTreeError::CollectionNotFound(name.to_string())); } // Delete from Redis self.storage.delete_collection(&namefixed)?; // Remove from the collections map self.collections.remove(&namefixed); Ok(()) } /// Delete all collections from the DocTree and Redis /// /// # Returns /// /// Ok(()) on success or an error pub fn delete_all_collections(&mut self) -> Result<()> { // Delete all collections from Redis self.storage.delete_all_collections()?; // Clear the collections map self.collections.clear(); // Reset the default collection self.default_collection = None; Ok(()) } /// List all collections /// /// # Returns /// /// A vector of collection names pub fn list_collections(&self) -> Vec { // First, try to get collections from the in-memory map let mut collections = self.collections.keys().cloned().collect::>(); // If no collections are found, try to get them from Redis if collections.is_empty() { // Get all collection keys from Redis if let Ok(keys) = self.storage.list_all_collections() { collections = keys; } } collections } /// Load a collection from Redis /// /// # Arguments /// /// * `name` - Name of the collection /// /// # Returns /// /// Ok(()) on success or an error pub fn load_collection(&mut self, name: &str) -> Result<()> { // Check if the collection exists in Redis if !self.storage.collection_exists(name)? { return Err(DocTreeError::CollectionNotFound(name.to_string())); } // Try to get the collection's path from Redis let path = match self.storage.get_collection_path(name) { Ok(path_str) => { println!("DEBUG: Found collection path in Redis: {}", path_str); PathBuf::from(path_str) }, Err(e) => { println!("DEBUG: Could not retrieve collection path from Redis: {}", e); PathBuf::new() // Fallback to empty path if not found } }; // Create a new collection let collection = Collection { path, name: name.to_string(), storage: self.storage.clone(), }; // Add to the collections map self.collections.insert(name.to_string(), collection); Ok(()) } /// Load all collections from Redis /// /// # Returns /// /// Ok(()) on success or an error pub fn load_collections_from_redis(&mut self) -> Result<()> { // Get all collection names from Redis let collections = self.storage.list_all_collections()?; // Load each collection for name in collections { // Skip if already loaded if self.collections.contains_key(&name) { continue; } // Try to get the collection's path from Redis let path = match self.storage.get_collection_path(&name) { Ok(path_str) => { println!("DEBUG: Found collection path in Redis: {}", path_str); PathBuf::from(path_str) }, Err(e) => { println!("DEBUG: Could not retrieve collection path from Redis: {}", e); PathBuf::new() // Fallback to empty path if not found } }; // Create a new collection let collection = Collection { path, name: name.clone(), storage: self.storage.clone(), }; // Add to the collections map self.collections.insert(name, collection); } Ok(()) } /// Get a page by name from a specific collection /// /// # Arguments /// /// * `collection_name` - Name of the collection (optional) /// * `page_name` - Name of the page /// /// # Returns /// /// The page content or an error pub fn page_get(&mut self, collection_name: Option<&str>, page_name: &str) -> Result { let (collection_name, page_name) = self.resolve_collection_and_page(collection_name, page_name)?; // Get the collection let collection = self.get_collection(&collection_name)?; // Get the page content let content = collection.page_get(page_name)?; // Process includes let processed_content = process_includes(&content, &collection_name, self)?; Ok(processed_content) } /// Get a page by name from a specific collection and return its HTML content /// /// # Arguments /// /// * `collection_name` - Name of the collection (optional) /// * `page_name` - Name of the page /// /// # Returns /// /// The HTML content or an error pub fn page_get_html(&self, collection_name: Option<&str>, page_name: &str) -> Result { let (collection_name, page_name) = self.resolve_collection_and_page(collection_name, page_name)?; // Get the collection let collection = self.get_collection(&collection_name)?; // Get the HTML collection.page_get_html(page_name, Some(self)) } /// Get the URL for a file in a specific collection /// /// # Arguments /// /// * `collection_name` - Name of the collection (optional) /// * `file_name` - Name of the file /// /// # Returns /// /// The URL for the file or an error pub fn file_get_url(&self, collection_name: Option<&str>, file_name: &str) -> Result { let (collection_name, file_name) = self.resolve_collection_and_page(collection_name, file_name)?; // Get the collection let collection = self.get_collection(&collection_name)?; // Get the URL collection.file_get_url(file_name) } /// Get the path to a page in the default collection /// /// # Arguments /// /// * `page_name` - Name of the page /// /// # Returns /// /// The path to the page or an error pub fn page_get_path(&self, page_name: &str) -> Result { // Check if a default collection is set let default_collection = self.default_collection.as_ref().ok_or_else(|| { DocTreeError::NoDefaultCollection })?; // Get the collection let collection = self.get_collection(default_collection)?; // Get the path collection.page_get_path(page_name) } /// Get information about the DocTree /// /// # Returns /// /// A map of information pub fn info(&self) -> HashMap { let mut info = HashMap::new(); info.insert("name".to_string(), self.name.clone()); info.insert("path".to_string(), self.path.to_string_lossy().to_string()); info.insert("collections".to_string(), self.collections.len().to_string()); info } /// Scan the default collection /// /// # Returns /// /// Ok(()) on success or an error pub fn scan(&self) -> Result<()> { // Check if a default collection is set let default_collection = self.default_collection.as_ref().ok_or_else(|| { DocTreeError::NoDefaultCollection })?; // Get the collection let collection = self.get_collection(default_collection)?; // Scan the collection collection.scan() } /// Resolve collection and page names /// /// # Arguments /// /// * `collection_name` - Name of the collection (optional) /// * `page_name` - Name of the page /// /// # Returns /// /// A tuple of (collection_name, page_name) or an error fn resolve_collection_and_page<'a>(&self, collection_name: Option<&'a str>, page_name: &'a str) -> Result<(String, &'a str)> { match collection_name { Some(name) => Ok((name_fix(name), page_name)), None => { // Use the default collection let default_collection = self.default_collection.as_ref().ok_or_else(|| { DocTreeError::NoDefaultCollection })?; Ok((default_collection.clone(), page_name)) } } } /// Recursively scan directories for .collection files and add them as collections /// /// # Arguments /// /// * `root_path` - The root path to start scanning from /// /// # Returns /// /// Ok(()) on success or an error pub fn scan_collections>(&mut self, root_path: P) -> Result<()> { let root_path = root_path.as_ref(); println!("DEBUG: Scanning for collections in directory: {:?}", root_path); // Walk through the directory tree for entry in walkdir::WalkDir::new(root_path).follow_links(true) { let entry = match entry { Ok(entry) => entry, Err(e) => { eprintln!("Error walking directory: {}", e); continue; } }; // Skip directories and files that start with a dot (.) let file_name = entry.file_name().to_string_lossy(); if file_name.starts_with(".") { continue; } // Skip non-directories if !entry.file_type().is_dir() { continue; } // Check if this directory contains a .collection file let collection_file_path = entry.path().join(".collection"); if collection_file_path.exists() { // Found a collection directory println!("DEBUG: Found .collection file at: {:?}", collection_file_path); let dir_path = entry.path(); // Get the directory name as a fallback collection name let dir_name = dir_path.file_name() .and_then(|name| name.to_str()) .unwrap_or("unnamed"); // Try to read and parse the .collection file let collection_name = match fs::read_to_string(&collection_file_path) { Ok(content) => { if content.trim().is_empty() { // Empty file, use directory name (name_fixed) dir_name.to_string() // We'll apply name_fix later at line 372 } else { // Parse as TOML match toml::from_str::(&content) { Ok(config) => { // Use the name from config if available, otherwise use directory name config.name.unwrap_or_else(|| dir_name.to_string()) }, Err(e) => { eprintln!("Error parsing .collection file at {:?}: {}", collection_file_path, e); dir_name.to_string() } } } }, Err(e) => { eprintln!("Error reading .collection file at {:?}: {}", collection_file_path, e); dir_name.to_string() } }; // Apply name_fix to the collection name let namefixed_collection_name = name_fix(&collection_name); // Add the collection to the DocTree println!("DEBUG: Adding collection '{}' from directory {:?}", namefixed_collection_name, dir_path); match self.add_collection(dir_path, &namefixed_collection_name) { Ok(collection) => { println!("DEBUG: Successfully added collection '{}' from {:?}", namefixed_collection_name, dir_path); println!("DEBUG: Collection stored in Redis key 'collections:{}'", collection.name); // Count documents and images let docs = collection.page_list().unwrap_or_default(); let files = collection.file_list().unwrap_or_default(); let images = files.iter().filter(|f| f.ends_with(".png") || f.ends_with(".jpg") || f.ends_with(".jpeg") || f.ends_with(".gif") || f.ends_with(".svg") ).count(); println!("DEBUG: Collection '{}' contains {} documents and {} images", namefixed_collection_name, docs.len(), images); }, Err(e) => { eprintln!("Error adding collection '{}' from {:?}: {}", namefixed_collection_name, dir_path, e); } } } } Ok(()) } /// Exports all collections to IPFS, encrypting their files and generating CSV manifests. /// /// # Arguments /// /// * `output_dir` - The directory to save the output CSV files. /// /// # Returns /// /// Ok(()) on success or an error. pub async fn export_collections_to_ipfs>(&self, output_dir: P) -> Result<()> { use tokio::fs; let output_dir = output_dir.as_ref(); // Create the output directory if it doesn't exist fs::create_dir_all(output_dir).await.map_err(DocTreeError::IoError)?; for (name, collection) in &self.collections { let csv_file_path = output_dir.join(format!("{}.csv", name)); println!("DEBUG: Exporting collection '{}' to IPFS and generating CSV at {:?}", name, csv_file_path); if let Err(e) = collection.export_to_ipfs(&csv_file_path) { eprintln!("Error exporting collection '{}': {}", name, e); // Continue with the next collection } } Ok(()) } /// Exports a specific collection to IPFS synchronously, encrypting its files and generating a CSV manifest. /// /// # Arguments /// /// * `collection_name` - The name of the collection to export. /// * `output_csv_path` - The path to save the output CSV file. /// /// # Returns /// /// Ok(()) on success or an error. pub fn export_collection_to_ipfs(&self, collection_name: &str, output_csv_path: &Path) -> Result<()> { // Get the collection let collection = self.get_collection(collection_name)?; // Create a new tokio runtime and block on the async export function let csv_file_path = output_csv_path.join(format!("{}.csv", collection_name)); collection.export_to_ipfs(&csv_file_path)?; Ok(()) } } impl DocTreeBuilder { /// Set the storage backend /// /// # Arguments /// /// * `storage` - Redis storage backend /// /// # Returns /// /// Self for method chaining /// Set the doctree name /// /// # Arguments /// /// * `name` - Name of the doctree /// /// # Returns /// /// Self for method chaining pub fn with_doctree_name(mut self, name: &str) -> Self { self.doctree_name = Some(name.to_string()); self } pub fn with_storage(mut self, storage: RedisStorage) -> Self { self.storage = Some(storage); self } /// Add a collection /// /// # Arguments /// /// * `path` - Base path of the collection /// * `name` - Name of the collection /// /// # Returns /// /// Self for method chaining or an error pub fn with_collection>(mut self, path: P, name: &str) -> Result { // Ensure storage is set let storage = self.storage.as_ref().ok_or_else(|| { DocTreeError::MissingParameter("storage".to_string()) })?; // Get the doctree name let doctree_name = self.doctree_name.clone().unwrap_or_else(|| "default".to_string()); // Create a new collection let namefixed = name_fix(name); // Clone the storage and set the doctree name let storage_clone = storage.clone(); storage_clone.set_doctree_name(&doctree_name); let collection = Collection::builder(path.as_ref(), &namefixed) .with_storage(storage_clone) .build()?; // Scan the collection collection.scan()?; // Add to the collections map self.collections.insert(collection.name.clone(), collection); // For backward compatibility if self.name.is_none() { self.name = Some(namefixed.clone()); } if self.path.is_none() { self.path = Some(path.as_ref().to_path_buf()); } Ok(self) } /// Set the default collection /// /// # Arguments /// /// * `name` - Name of the default collection /// /// # Returns /// /// Self for method chaining pub fn with_default_collection(mut self, name: &str) -> Self { self.default_collection = Some(name_fix(name)); self } /// Scan for collections in the given root path /// /// # Arguments /// /// * `root_path` - The root path to scan for collections /// /// # Returns /// /// Self for method chaining or an error pub fn scan_collections>(self, root_path: P) -> Result { // Ensure storage is set let storage = self.storage.as_ref().ok_or_else(|| { DocTreeError::MissingParameter("storage".to_string()) })?; // Get the doctree name let doctree_name = self.doctree_name.clone().unwrap_or_else(|| "default".to_string()); // Clone the storage and set the doctree name let storage_clone = storage.clone(); storage_clone.set_doctree_name(&doctree_name); // Create a temporary DocTree to scan collections let mut temp_doctree = DocTree { collections: HashMap::new(), default_collection: None, storage: storage_clone, doctree_name: doctree_name, name: self.name.clone().unwrap_or_default(), path: self.path.clone().unwrap_or_else(|| PathBuf::from("")), }; // Scan for collections temp_doctree.scan_collections(root_path)?; // Create a new builder with the scanned collections let mut new_builder = self; for (name, collection) in temp_doctree.collections { new_builder.collections.insert(name.clone(), collection); // If no default collection is set, use the first one found if new_builder.default_collection.is_none() { new_builder.default_collection = Some(name); } } Ok(new_builder) } /// Build the DocTree /// /// # Returns /// /// A new DocTree or an error pub fn build(self) -> Result { // Ensure storage is set let storage = self.storage.ok_or_else(|| { DocTreeError::MissingParameter("storage".to_string()) })?; // Get the doctree name let doctree_name = self.doctree_name.unwrap_or_else(|| "default".to_string()); // Set the doctree name in the storage let storage_clone = storage.clone(); storage_clone.set_doctree_name(&doctree_name); // Create the DocTree let mut doctree = DocTree { collections: self.collections, default_collection: self.default_collection, storage: storage_clone, doctree_name, name: self.name.unwrap_or_default(), path: self.path.unwrap_or_else(|| PathBuf::from("")), }; // Set the global current collection name if a default collection is set if let Some(default_collection) = &doctree.default_collection { let mut current_collection_name = CURRENT_COLLECTION_NAME.lock().unwrap(); *current_collection_name = Some(default_collection.clone()); } // Load all collections from Redis doctree.load_collections_from_redis()?; Ok(doctree) } } /// Create a new DocTree instance /// /// For backward compatibility, it also accepts path and name parameters /// to create a DocTree with a single collection /// /// # Arguments /// /// * `args` - Optional path and name for backward compatibility /// /// # Returns /// /// A new DocTree or an error pub fn new>(args: &[&str]) -> Result { let storage = RedisStorage::new("redis://localhost:6379")?; let mut builder = DocTree::builder().with_storage(storage); // If the first argument is a doctree name, use it if args.len() >= 1 && args[0].starts_with("--doctree=") { let doctree_name = args[0].trim_start_matches("--doctree="); builder = builder.with_doctree_name(doctree_name); } // For backward compatibility with existing code if args.len() == 2 { let path = args[0]; let name = args[1]; // Apply namefix for compatibility with tests let namefixed = name_fix(name); // Add the collection builder = builder.with_collection(path, &namefixed)?; // Set the default collection builder = builder.with_default_collection(&namefixed); } builder.build() } /// Create a new DocTree by scanning a directory for collections /// /// # Arguments /// /// * `root_path` - The root path to scan for collections /// * `doctree_name` - Optional name for the doctree (default: "default") /// /// # Returns /// /// A new DocTree or an error pub fn from_directory>(root_path: P, doctree_name: Option<&str>) -> Result { let storage = RedisStorage::new("redis://localhost:6379")?; let mut builder = DocTree::builder().with_storage(storage); // Set the doctree name if provided if let Some(name) = doctree_name { builder = builder.with_doctree_name(name); } builder.scan_collections(root_path)?.build() }