use crate::ipfs::ipfs::IpfsService; use crate::types::comment::{CommentError, CommentMetadata}; use crate::types::ipfs::IpfsResult; use crate::utils::ipfs::{ create_file_path, create_storage_directory, read_json_via_cat, retrieve_content_hash, save_json_file, DEFAULT_MAX_JSON_SIZE, list_directory_file_hashes, }; use ipfs_api_backend_actix::IpfsClient; const STORAGE_DIR: &str = "/puffpastry/comments"; const FILE_EXTENSION: &str = "json"; pub struct CommentService { client: IpfsClient, } // Implement per-proposal subdirectory saving. The input is (proposal_cid, comments_batch) impl IpfsService<(String, Vec)> for CommentService { type Err = CommentError; async fn save(&mut self, item: (String, Vec)) -> IpfsResult { let (proposal_cid, comments) = item; // Allow batch save within the proposal's subdirectory. if comments.is_empty() { return Err(CommentError::from(std::io::Error::new( std::io::ErrorKind::InvalidInput, "Failed to store comment to IPFS: empty comments batch", ))); } let mut last_cid: Option = None; for comment in comments { let res = self .store_comment_in_proposal_dir_and_publish(&proposal_cid, comment) .await; match res { Ok(cid) => last_cid = Some(cid), Err(e) => return Err(e), } } match last_cid { Some(cid) => Ok(cid), None => Err(CommentError::from(std::io::Error::new( std::io::ErrorKind::Other, "Failed to store comment to IPFS", ))), } } async fn read(&mut self, hash: String) -> IpfsResult<(String, Vec), Self::Err> { // For reading, the caller should pass a directory hash (CID). We return only the comments vector here. // Since IpfsService requires returning the same T, include an empty proposal id (unknown in read-by-hash). // Alternatively, callers should not use the proposal id from this return value. let comments = self.read_all_comments_in_dir(&hash).await?; Ok((String::new(), comments)) } } impl CommentService { pub fn new(client: IpfsClient) -> Self { Self { client } } // Writes a single comment JSON file into the MFS comments subdirectory for the proposal, // then publishes the subdirectory snapshot (by retrieving the directory CID). Returns the comment file CID. async fn store_comment_in_proposal_dir_and_publish( &self, proposal_cid: &str, comment: CommentMetadata, ) -> IpfsResult { // Ensure the per-proposal storage subdirectory exists let proposal_dir = format!("{}/{}", STORAGE_DIR, proposal_cid); create_storage_directory::(&self.client, &proposal_dir).await?; // Create a unique file path within the proposal subdirectory and save the JSON content let file_path = create_file_path(&proposal_dir, FILE_EXTENSION); save_json_file::(&self.client, &file_path, &comment).await?; // Retrieve the file's CID to return to the caller let file_cid = retrieve_content_hash::(&self.client, &file_path).await?; // Publish a new snapshot of the proposal's comments subdirectory by retrieving its CID let _dir_cid = retrieve_content_hash::(&self.client, &proposal_dir).await?; Ok(file_cid) } async fn read_comment_file(&self, hash: &str) -> IpfsResult { read_json_via_cat::(&self.client, hash, DEFAULT_MAX_JSON_SIZE).await } // Read all comment files within a directory identified by the given hash (directory CID) async fn read_all_comments_in_dir(&self, dir_hash: &str) -> IpfsResult, CommentError> { let file_hashes = list_directory_file_hashes::(&self.client, dir_hash).await?; let mut comments: Vec = Vec::with_capacity(file_hashes.len()); for fh in file_hashes { let comment = self.read_comment_file(&fh).await?; comments.push(comment); } Ok(comments) } }