use anyhow::{Result, bail}; use chrono::{DateTime, Utc}; use std::{collections::HashMap, fs, path::PathBuf, str::FromStr}; pub enum Source { Url(String), File(PathBuf), } pub struct Nex { filename: String, pattern: String, time_format: String, users: HashMap, } impl Nex { pub fn init( target_dir: String, filename: String, time_format: String, pattern: String, user_names: &Vec, ) -> Result { // init data export location let target = PathBuf::from_str(&target_dir)?.canonicalize()?; if !target.is_dir() { bail!("Target location is not directory!"); } // init locations for each user let mut users = HashMap::with_capacity(user_names.len()); for u in user_names { let mut p = PathBuf::from(&target); p.push(u); fs::create_dir_all(&p)?; users.insert(u.clone(), p); } Ok(Self { filename, time_format, pattern, users, }) } // @TODO: This function requires the following improvements: // * validate attachments before updating // * apply the post time to the files /// Update destination with given data (typically received from Snac) pub fn sync( &self, name: &str, content: String, link: String, attachments: Option>, tags: Option>, (published, updated): (DateTime, Option>), ) -> Result { // prepare destination let mut p = PathBuf::from(self.users.get(name).unwrap()); p.push(published.format("%Y").to_string()); p.push(published.format("%m").to_string()); p.push(published.format("%d").to_string()); fs::create_dir_all(&p)?; // init storage directory for the post state file and attachments // * by the current implementation, its name starts with dot // as usually hidden but accessible (in servers like Nexy) let mut d = PathBuf::from(&p); d.push(format!(".{}", published.format(&self.filename))); fs::create_dir_all(&d)?; // create system meta file to track post time updated // if this meta file exists and has timestamp changed, also cleanup attachment files to update let mut s = PathBuf::from(&d); s.push(".state"); if !s.exists() || updated.is_some_and(|u| u.to_string() != fs::read_to_string(&s).unwrap()) { fs::remove_dir_all(&d)?; fs::create_dir_all(&d)?; fs::write(s, updated.unwrap_or(published).to_string())? } else { println!("\t\t\tpost is up to date."); return Ok(0); } // format content pattern let c = self .pattern .replace( "{content}", &if self.pattern.contains("{tags}") { content.replace("#", "") } else { content }, ) .replace( "{attachments}", &attachments .map(|a| { let mut b = Vec::with_capacity(a.len()); b.push("\n".to_string()); for (i, (name, media_type, source)) in a.into_iter().enumerate() { let mut t = Vec::with_capacity(3); t.push(format!( "=> {}", match source { Source::Url(url) => url, Source::File(from) => { let mut to = PathBuf::from(&d); let f = format!( "{}{}", i + 1, from.extension() .map(|e| format!(".{}", e.to_string_lossy())) .unwrap_or_default(), ); to.push(&f); if !to.exists() { fs::copy(&from, &to).unwrap(); println!( "\t\t\tcopy attachment file `{}`", to.to_string_lossy() ) } format!("{}/{f}", d.file_name().unwrap().to_string_lossy()) } } )); if !name.is_empty() { t.push(name) } if !media_type.is_empty() { t.push(format!("({media_type})")) } b.push(t.join(" ")) } b.join("\n") }) .unwrap_or_default(), ) .replace( "{tags}", &tags .map(|t| format!("\n\n{}", t.join(", "))) .unwrap_or_default(), ) .replace("{link}", &format!("\n\n=> {link}")) .replace( "{updated}", &updated .map(|t| format!("\n\n✏ {}", t.format(&self.time_format))) .unwrap_or_default(), ); // save post file p.push( published .format(self.filename.trim_matches(std::path::MAIN_SEPARATOR)) .to_string(), ); let f = p.to_string_lossy(); if fs::exists(&p)? { println!("\t\t\tpost file `{f}` update with new content.") } else { println!("\t\t\tcreate new post file `{f}`.") } fs::write(&p, c)?; Ok(1) } }