diff --git a/Cargo.toml b/Cargo.toml index 6cf89b2..8b5377d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,4 @@ chrono = "^0.4.20" clap = { version = "4.5", features = ["derive"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +walkdir = "2.5" diff --git a/src/main.rs b/src/main.rs index 46151e8..4f59eba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,11 @@ mod config; mod nex; +mod response; mod snac; use anyhow::Result; use nex::Nex; +use response::Response; use snac::Snac; fn main() -> Result<()> { @@ -22,26 +24,33 @@ fn main() -> Result<()> { let s = Snac::init(c.source, c.user)?; println!("export begin..."); - let (mut u, mut t) = sync(&s, &n)?; + let mut r = sync(&s, &n)?; match c.rotate { - Some(r) => loop { - println!("queue completed (updated: {u} / total: {t}), await {r} seconds to rotate..."); - std::thread::sleep(std::time::Duration::from_secs(r)); - (u, t) = sync(&s, &n)?; + Some(t) => loop { + println!( + "queue completed:\n\tcreated: {}\n\tupdated: {}\n\tdeleted:\n\t\tfiles: {}\n\t\tdirectories: {}\n\tignored: {}\n\ttotal: {}\nawait {t} seconds to rotate...", + r.created, r.updated, r.deleted.files, r.deleted.directories, r.ignored, r.total + ); + std::thread::sleep(std::time::Duration::from_secs(t)); + r = sync(&s, &n)?; }, - None => println!("export completed (updated: {u} / total: {t})."), + None => println!( + "export completed:\n\tcreated: {}\n\tupdated: {}\n\tdeleted:\n\t\tfiles: {}\n\t\tdirectories: {}\n\tignored: {}\n\ttotal: {}", + r.created, r.updated, r.deleted.files, r.deleted.directories, r.ignored, r.total + ), } Ok(()) } -fn sync(snac: &Snac, nex: &Nex) -> Result<(usize, usize)> { - let mut t = 0; // total - let mut u = 0; // updated +fn sync(snac: &Snac, nex: &Nex) -> Result { + use std::collections::HashSet; + let mut r = Response::default(); for user in &snac.users { println!("\tsync profile for `{}`...", user.name); + let mut keep = HashSet::with_capacity(100); // @TODO estimated for post in user.public()? { - t += 1; + r.total += 1; // make sure post entry has expected content type if !post.is_note() { todo!() @@ -49,12 +58,12 @@ fn sync(snac: &Snac, nex: &Nex) -> Result<(usize, usize)> { // skip non authorized content if let Some(content) = post.source_content { println!("\t\tsync post `{}`...", post.id); - u += nex.sync( + let response = nex.sync( &user.name, content, post.url, post.attachment.map(|a| { - use nex::Source; + use nex::source::Source; let mut attachments = Vec::with_capacity(a.len()); for attachment in a { attachments.push(( @@ -81,8 +90,31 @@ fn sync(snac: &Snac, nex: &Nex) -> Result<(usize, usize)> { }), (post.published, post.updated), )?; + use nex::response::change::Change; + let f = ""; //response.file.to_string_lossy(); + match response.change { + Change::Created => { + r.created += 1; + println!("\t\t\tcreate new post file `{f}`.") + } + Change::Ignored => { + r.ignored += 1; + println!("\t\t\tpost file `{f}` is up to date.") + } + Change::Updated => { + r.updated += 1; + println!("\t\t\tpost file `{f}` update with new content.") + } + } + for i in response.keep { + keep.insert(i); + } } } + // cleanup removed post entries + let d = nex.clean(&user.name, keep)?; + r.deleted.directories += d.directories; + r.deleted.files += d.files; } - Ok((u, t)) + Ok(r) } diff --git a/src/nex.rs b/src/nex.rs index 210b01a..5a3c3e0 100644 --- a/src/nex.rs +++ b/src/nex.rs @@ -1,14 +1,18 @@ mod attachment; +pub mod response; +pub mod source; use anyhow::{Result, bail}; use attachment::Attachment; use chrono::{DateTime, Utc}; -use std::{collections::HashMap, fs, path::PathBuf, str::FromStr}; - -pub enum Source { - Url(String), - File(PathBuf), -} +use response::{Clean, Sync, change::Change}; +use source::Source; +use std::{ + collections::{HashMap, HashSet}, + fs, + path::PathBuf, + str::FromStr, +}; pub struct Nex { attachment: Attachment, @@ -66,19 +70,38 @@ impl Nex { attachments: Option>, tags: Option>, (published, updated): (DateTime, Option>), - ) -> Result { + ) -> Result { + // collect existing entries to ignore on cleanup + let mut i = Vec::with_capacity(10); // includes est. attachments len + // prepare destination - let mut p = PathBuf::from(self.users.get(name).unwrap()); + let root = PathBuf::from(self.users.get(name).unwrap()); + let mut p = PathBuf::from(&root); + i.push(root); + p.push(published.format("%Y").to_string()); + i.push(p.clone()); + p.push(published.format("%m").to_string()); + i.push(p.clone()); + p.push(published.format("%d").to_string()); + i.push(p.clone()); + fs::create_dir_all(&p)?; + // init shared post ID once + let id = published.format(&self.filename).to_string(); + + // init post filepath + let mut f = PathBuf::from(&p); + f.push(&id); + // 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))); + d.push(format!(".{id}")); fs::create_dir_all(&d)?; // create system meta file to track post time updated @@ -89,10 +112,13 @@ impl Nex { { fs::remove_dir_all(&d)?; fs::create_dir_all(&d)?; - fs::write(s, updated.unwrap_or(published).to_string())? + fs::write(&s, updated.unwrap_or(published).to_string())? } else { - println!("\t\t\tpost is up to date."); - return Ok(0); + i.extend([d, f, p, s]); // move all paths processed to cleanup ignore + return Ok(Sync { + change: Change::Ignored, + keep: i, + }); } // format content pattern @@ -112,7 +138,7 @@ impl Nex { .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() { + for (n, (name, media_type, source)) in a.into_iter().enumerate() { let mut t = Vec::with_capacity(3); t.push(format!( "=> {}", @@ -122,7 +148,7 @@ impl Nex { let mut to = PathBuf::from(&d); let f = format!( "{}{}", - i + 1, + n + 1, from.extension() .map(|e| format!(".{}", e.to_string_lossy())) .unwrap_or_default(), @@ -131,6 +157,7 @@ impl Nex { if !to.exists() { self.attachment.sync(&from, &to).unwrap() } + i.push(to); format!("{}/{f}", d.file_name().unwrap().to_string_lossy()) } } @@ -161,17 +188,46 @@ impl Nex { .unwrap_or_default(), ); - // save post file - p.push(published.format(&self.filename).to_string()); - let f = p.to_string_lossy(); - if fs::exists(&p)? { - println!("\t\t\tpost file `{f}` update with new content.") + // save post file, set its change status + let change = if fs::exists(&f)? { + Change::Updated } else { - println!("\t\t\tcreate new post file `{f}`.") - } - fs::write(&p, c)?; + Change::Created + }; + fs::write(&f, c)?; - Ok(1) + // move all paths processed to cleanup ignore + i.extend([d, f, p, s]); + + Ok(Sync { change, keep: i }) + } + + pub fn clean(&self, name: &str, ignore: HashSet) -> Result { + let mut r = Clean::default(); + for entry in + walkdir::WalkDir::new(PathBuf::from(self.users.get(name).unwrap())).follow_links(false) + { + let e = entry?; + let p = e.path(); + let s = p.to_string_lossy(); + println!("check `{s}`..."); + if ignore.contains(p) { + continue; + } + let m = fs::metadata(p)?; + if m.is_file() { + fs::remove_file(p)?; + r.files += 1; + println!("\t\t\tdelete file `{s}`"); + } else if m.is_dir() { + fs::remove_dir_all(p)?; + r.directories += 1; + println!("\t\t\tdelete directory `{s}`"); + } else { + panic!() + } + } + Ok(r) } pub fn is_attachments_disabled(&self) -> bool { diff --git a/src/nex/response.rs b/src/nex/response.rs new file mode 100644 index 0000000..b364438 --- /dev/null +++ b/src/nex/response.rs @@ -0,0 +1,12 @@ +pub mod change; + +pub struct Sync { + pub change: change::Change, + pub keep: Vec, +} + +#[derive(Default)] +pub struct Clean { + pub directories: usize, + pub files: usize, +} diff --git a/src/nex/response/change.rs b/src/nex/response/change.rs new file mode 100644 index 0000000..c3b7013 --- /dev/null +++ b/src/nex/response/change.rs @@ -0,0 +1,5 @@ +pub enum Change { + Created, + Ignored, + Updated, +} diff --git a/src/nex/source.rs b/src/nex/source.rs new file mode 100644 index 0000000..ffdd555 --- /dev/null +++ b/src/nex/source.rs @@ -0,0 +1,4 @@ +pub enum Source { + Url(String), + File(std::path::PathBuf), +} diff --git a/src/response.rs b/src/response.rs new file mode 100644 index 0000000..fbea9c1 --- /dev/null +++ b/src/response.rs @@ -0,0 +1,14 @@ +#[derive(Default)] +pub struct Deleted { + pub files: usize, + pub directories: usize, +} + +#[derive(Default)] +pub struct Response { + pub created: usize, + pub deleted: Deleted, + pub ignored: usize, + pub total: usize, + pub updated: usize, +}