diff --git a/.gitignore b/.gitignore index ea8c4bf..43aee9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +project.zip diff --git a/Cargo.lock b/Cargo.lock index e793124..1fd73ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,15 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "0.6.18" @@ -571,6 +580,7 @@ dependencies = [ "clap", "dialoguer", "encoding", + "regex", "serde", "serde_json", "tabled", @@ -732,6 +742,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rustc-demangle" version = "0.1.24" diff --git a/Cargo.toml b/Cargo.toml index e33a3ca..0f4a38c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,4 @@ async-trait = "0.1.48" encoding = "0.2" zip = "0.6" tokio-util = "0.7" +regex = "1.10" diff --git a/file.zip b/file.zip deleted file mode 100644 index 32eee4a..0000000 Binary files a/file.zip and /dev/null differ diff --git a/src/commands/add.rs b/src/commands/add.rs index 079be07..b47fcfc 100644 --- a/src/commands/add.rs +++ b/src/commands/add.rs @@ -5,7 +5,7 @@ use dialoguer::Select; use crate::{services::{instagram::Instagram, service::Service}, types::{participant::Participant, project::Project, user::User}}; pub async fn add(path: String) { - let mut raw_project = Project::load(path).await.unwrap_or_else(|e| { + let mut raw_project = Project::load(path.clone()).await.unwrap_or_else(|e| { eprintln!("Error while loading a project: {}", e); exit(1); }); @@ -59,5 +59,10 @@ pub async fn add(path: String) { } println!("Starting to merge messages..."); - final_service.merge_messages(&mut raw_project, &user_map) + final_service.merge_messages(&mut raw_project, &user_map).await; + + raw_project.project.sort_messages(); + + println!("Saving all messages to a project file."); + let _ = raw_project.save(&path).await; } diff --git a/src/commands/users.rs b/src/commands/users.rs index f1b5eb9..d193d9c 100644 --- a/src/commands/users.rs +++ b/src/commands/users.rs @@ -3,13 +3,13 @@ use std::process::exit; use dialoguer::{Input, Select}; use tabled::Table; -use crate::types::{project::Project, user::User}; +use crate::types::{project::{Project, ProjectRaw}, user::User}; pub async fn users(path: String) { let project = Project::load(path.clone()).await.unwrap_or_else(|e| { eprintln!("Error while loading a project: {}", e); exit(1); - }).project; + }); menu(project, path).await; } @@ -24,12 +24,13 @@ pub fn print_users(users: &Vec) { println!("{}", users); } -pub async fn menu(mut project: Project, path: String) { +pub async fn menu(mut project_raw: ProjectRaw, path: String) { + let project = &mut project_raw.project; println!("User management:"); print_users(&project.users); println!("\n"); - let options = vec!["Create a new user", "Remove a user", "Save and exit"]; + let options = vec!["Create a new user", "Remove a user", "Save and exit", "Exit without saving"]; let option = Select::new() .with_prompt("Choose a option") @@ -52,12 +53,12 @@ pub async fn menu(mut project: Project, path: String) { project.users.push(user); - Box::pin(menu(project, path)).await; + Box::pin(menu(project_raw, path)).await; }, 1 => { if project.users.is_empty() { println!("You do not have any users created! No users to be removed!"); - Box::pin(menu(project, path)).await; + Box::pin(menu(project_raw, path)).await; return; } @@ -74,15 +75,18 @@ pub async fn menu(mut project: Project, path: String) { project.users.remove(option_remove); - Box::pin(menu(project, path)).await; + Box::pin(menu(project_raw, path)).await; }, 2 => { // Save and exit - let _ = project.save(path).await; + let _ = project_raw.save(&path).await; }, + 3 => { + // Exit without saving + } _ => { println!("Error! Unknown option!"); - Box::pin(menu(project, path)).await; + Box::pin(menu(project_raw, path)).await; } } } diff --git a/src/main.rs b/src/main.rs index 3dff0b4..7b97371 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,7 +41,8 @@ async fn main() { timestamps: HashSet::new(), media_index: 0, }; - let _ = new_project.save(path).await; + let _ = new_project.save_new(path).await; + println!("New project was successfully created!"); } if let Some(path) = cli.users { diff --git a/src/services/instagram.rs b/src/services/instagram.rs index 69428b3..d8f7d38 100644 --- a/src/services/instagram.rs +++ b/src/services/instagram.rs @@ -1,5 +1,6 @@ use std::{collections::HashMap, fs, io, path::PathBuf, process::exit}; +use regex::Regex; use async_trait::async_trait; use dialoguer::{Input, Select}; use encoding::{all::ISO_8859_1, EncoderTrap, Encoding}; @@ -24,7 +25,6 @@ pub struct InstagramMessage { } impl InstagramMessage { - // TODO: Take into consideration the user_map pub fn conv(&self, media_index: &mut usize, project_path: PathBuf, user_map: &HashMap) -> Message { let text_content = if let Some(content) = &self.content { Some(decode_str(content)) @@ -61,14 +61,18 @@ impl InstagramPhoto { *media_index += 1; let img_path = (*media_index - 1).to_string(); - original_path.pop(); + for _ in 0..4 { + if !original_path.pop() { + break; + } + } original_path.push(self.uri.clone()); Photo { text_content: text_content.clone(), img_path, - original_img_path: original_path.display().to_string() + original_img_path: Some(original_path.display().to_string()) } } } @@ -142,12 +146,15 @@ impl Service for Instagram { let data = self.data.as_mut().unwrap(); let path = PathBuf::from(&data.project_path); + let pattern = r".*/message_.+\.json$"; + let re = Regex::new(pattern).unwrap(); + let paths: Vec = fs::read_dir(&path).unwrap_or_else(|_| { eprintln!("Folder not found!"); exit(1); }) .filter_map(|entry| entry.ok()) - .filter(|entry| entry.path().is_file()) + .filter(|entry| entry.path().is_file() && re.is_match(&entry.path().display().to_string())) .map(|entry| { entry.path() }).collect(); for (index, path) in paths.iter().enumerate() { @@ -168,7 +175,7 @@ impl Service for Instagram { let jsondata: InstagramDataJson = serde_json::from_str(&contents) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - if index == 0{ + if index == 0 { // Load participants for participant in jsondata.participants { data.participants.push(participant); @@ -193,9 +200,10 @@ impl Service for Instagram { }).collect() } - fn merge_messages(&self, raw_project: &mut ProjectRaw, user_map: &HashMap) { + async fn merge_messages(&self, raw_project: &mut ProjectRaw, user_map: &HashMap) { let data = self.data.as_ref().unwrap(); let path = PathBuf::from(data.project_path.clone()); + let mut media_files: Vec<(String, String)> = vec![]; for message in &data.messages { let mut media_index_clone = raw_project.project.media_index.clone(); @@ -205,13 +213,19 @@ impl Service for Instagram { if moved { // Move the file to the media folder - + for photo in msg.photos.clone() { + let original_path = photo.original_img_path.unwrap().clone(); + let path = format!("media/{}", photo.img_path); + let photovalues = (path, original_path); + media_files.push(photovalues); + } // Update the highest media index of the project if the message was merged raw_project.project.media_index = media_index_clone; } - println!("{:?}", msg); } + + let _ = raw_project.save_media_files(media_files).await; } } diff --git a/src/services/service.rs b/src/services/service.rs index 935b09b..5e3b578 100644 --- a/src/services/service.rs +++ b/src/services/service.rs @@ -11,5 +11,5 @@ pub trait Service { async fn load(&mut self) -> io::Result<()>; fn get_participants(&self) -> Vec; - fn merge_messages(&self, raw_project: &mut ProjectRaw, user_map: &HashMap); + async fn merge_messages(&self, raw_project: &mut ProjectRaw, user_map: &HashMap); } diff --git a/src/types/message.rs b/src/types/message.rs index dd18772..9955fb9 100644 --- a/src/types/message.rs +++ b/src/types/message.rs @@ -13,6 +13,6 @@ pub struct Photo { pub text_content: Option, pub img_path: String, - #[serde(skip_serializing)] - pub original_img_path: String, + #[serde(skip)] + pub original_img_path: Option, } diff --git a/src/types/project.rs b/src/types/project.rs index 0c3592a..e9211e0 100644 --- a/src/types/project.rs +++ b/src/types/project.rs @@ -52,7 +52,7 @@ impl Project { }) } - pub async fn save(&self, file_path: String) -> io::Result<()> { + pub async fn save_new(&self, file_path: String) -> io::Result<()> { let contents = serde_json::to_string(&self) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; @@ -89,4 +89,107 @@ impl Project { return true; } + + pub fn sort_messages(&mut self) { + self.messages.sort_by_key(|msg| msg.timestamp); + } +} + + +impl ProjectRaw { + /// Save multiple files to the zip archive. + /// + /// # Arguments + /// * `files` - A list of tuples where each tuple contains: + /// - `target_path`: The path inside the zip archive (e.g., `media/1`). + /// - `source_path`: The path to the file on the filesystem. + pub async fn save_media_files( + &mut self, + files: Vec<(String, String)>, + ) -> io::Result<()> { + let mut new_buffer = Cursor::new(Vec::new()); + { + let mut zip_writer = ZipWriter::new(&mut new_buffer); + let options = FileOptions::default().unix_permissions(0o644); + + let replace_files: HashSet<_> = files.iter().map(|(target, _)| target.clone()).collect(); + + for i in 0..self.zip.len() { + let mut file = self.zip.by_index(i)?; + let name = file.name().to_string(); + + if replace_files.contains(&name) { + continue; + } + + zip_writer.start_file(name, options)?; + let mut file_content = Vec::new(); + file.read_to_end(&mut file_content)?; + zip_writer.write_all(&file_content)?; + } + + for (target_path, source_path) in files { + let mut file = File::open(source_path).await?; + let mut file_content = Vec::new(); + file.read_to_end(&mut file_content).await?; + + zip_writer.start_file(target_path, options)?; + zip_writer.write_all(&file_content)?; + } + + zip_writer.finish()?; + } + + self.zip = ZipArchive::new(new_buffer)?; + + Ok(()) + } + + /// Save the `Project` to `project.json` and flush the zip archive to the specified file. + /// + /// # Arguments + /// * `output_path` - The path to save the zip archive to. + pub async fn save(&mut self, output_path: &str) -> io::Result<()> { + let project_json = serde_json::to_string(&self.project) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + let mut new_buffer = Cursor::new(Vec::new()); + { + let mut zip_writer = ZipWriter::new(&mut new_buffer); + let options = FileOptions::default().unix_permissions(0o644); + + let mut project_json_written = false; + + for i in 0..self.zip.len() { + let mut file = self.zip.by_index(i)?; + let name = file.name(); + + if name == "project.json" { + // Replace the existing `project.json`. + zip_writer.start_file("project.json", options)?; + zip_writer.write_all(project_json.as_bytes())?; + project_json_written = true; + } else { + // Copy other files as-is. + zip_writer.start_file(name, options)?; + let mut file_content = Vec::new(); + file.read_to_end(&mut file_content)?; + zip_writer.write_all(&file_content)?; + } + } + + if !project_json_written { + zip_writer.start_file("project.json", options)?; + zip_writer.write_all(project_json.as_bytes())?; + } + + zip_writer.finish()?; + } + + let mut output_file = File::create(output_path).await?; + output_file.write_all(&new_buffer.into_inner()).await?; + output_file.flush().await?; + + Ok(()) + } }