From ef1ba39589212a1a9711e575daebabfdd086dde8 Mon Sep 17 00:00:00 2001 From: postscriptum Date: Tue, 1 Jul 2025 20:13:44 +0300 Subject: [PATCH] implement optional binary files copy --- README.md | 49 ++++++++++++++++++++++++--------------------- src/config.rs | 4 ++++ src/main.rs | 16 +++++++++++---- src/nex.rs | 52 ++++++++++++++++++++++++++++++++++++------------ src/snac/user.rs | 23 ++++++++++++++++++--- 5 files changed, 101 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 9582706..3ce6e70 100644 --- a/README.md +++ b/README.md @@ -21,40 +21,43 @@ snac2nex -s /path/to/snac/storage -t /path/to/nex -u user1 -u user2 ### Options ``` bash --s, --source - Path to the Snac2 profile directory + -s, --source + Path to the Snac2 profile directory --t, --target - Target directory for public data export + -t, --target + Target directory for public data export --u, --user - Username to export + -u, --user + Username to export --r, --rotate - Keep running as the daemon, renew every `n` seconds + -b, --binary + Export binary files (attachments) --f, --format-content - Post template pattern + -r, --rotate + Keep running as the daemon, renew every `n` seconds - [default: {content}{attachments}{link}{tags}{updated}] + -f, --format-content + Post template pattern - --format-filename - Post filenames format + [default: {content}{attachments}{link}{tags}{updated}] - * escaped with `%%` + --format-filename + Post filenames format - [default: %H:%M:%S.txt] + * escaped with `%%` - --format-updated - Post `{updated}` time format + [default: %H:%M:%S.txt] - * escaped with `%%` + --format-updated + Post `{updated}` time format - [default: "%Y/%m/%d %H:%M:%S"] + * escaped with `%%` --h, --help - Print help (see a summary with '-h') + [default: "%Y/%m/%d %H:%M:%S"] --V, --version - Print version + -h, --help + Print help (see a summary with '-h') + + -V, --version + Print version ``` \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 0802428..e8b71a0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,6 +15,10 @@ pub struct Config { #[arg(short, long)] pub user: Vec, + /// Export binary files (attachments) + #[arg(short, long, default_value_t = false)] + pub binary: bool, + /// Keep running as the daemon, renew every `n` seconds #[arg(short, long)] pub rotate: Option, diff --git a/src/main.rs b/src/main.rs index 8990d2f..dd86d74 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,12 +21,12 @@ fn main() -> Result<()> { let s = Snac::init(c.source, c.user)?; println!("export begin..."); - let (mut u, mut t) = sync(&s, &n)?; + let (mut u, mut t) = sync(&s, &n, c.binary)?; 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)?; + (u, t) = sync(&s, &n, c.binary)?; }, None => println!("export completed (updated: {u} / total: {t})."), } @@ -34,7 +34,7 @@ fn main() -> Result<()> { Ok(()) } -fn sync(snac: &Snac, nex: &Nex) -> Result<(usize, usize)> { +fn sync(snac: &Snac, nex: &Nex, is_binary: bool) -> Result<(usize, usize)> { let mut t = 0; // total let mut u = 0; // updated for user in &snac.users { @@ -49,12 +49,20 @@ fn sync(snac: &Snac, nex: &Nex) -> Result<(usize, usize)> { content, post.url, post.attachment.map(|a| { + use nex::Source; let mut attachments = Vec::with_capacity(a.len()); for attachment in a { attachments.push(( attachment.name, attachment.media_type, - attachment.url, + if is_binary { + Source::File( + user.file(attachment.url.split('/').next_back().unwrap()) + .unwrap(), + ) + } else { + Source::Url(attachment.url) + }, )) } attachments diff --git a/src/nex.rs b/src/nex.rs index bc9350d..f1478c0 100644 --- a/src/nex.rs +++ b/src/nex.rs @@ -1,6 +1,11 @@ use anyhow::{Result, bail}; use chrono::{DateTime, Utc}; -use std::{collections::HashMap, path::PathBuf, str::FromStr}; +use std::{collections::HashMap, fs, path::PathBuf, str::FromStr}; + +pub enum Source { + Url(String), + File(PathBuf), +} pub struct Nex { filename: String, @@ -27,7 +32,7 @@ impl Nex { for u in user_names { let mut p = PathBuf::from(&target); p.push(u); - std::fs::create_dir_all(&p)?; + fs::create_dir_all(&p)?; users.insert(u.clone(), p); } @@ -44,10 +49,17 @@ impl Nex { name: &str, content: String, link: String, - attachments: Option>, + 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)?; + // format content pattern let c = self .pattern @@ -65,9 +77,30 @@ impl Nex { .map(|a| { let mut b = Vec::with_capacity(a.len()); b.push("\n".to_string()); - for (name, media_type, link) in a { + for (name, media_type, source) in a { let mut t = Vec::with_capacity(3); - t.push(format!("=> {link}")); + t.push(format!( + "=> {}", + match source { + Source::Url(url) => url, + Source::File(from) => { + let d = format!(".{}", published.format(&self.filename)); + let mut to = PathBuf::from(&p); + to.push(&d); + fs::create_dir_all(&to).unwrap(); + let f = from + .file_name() + .unwrap() + .to_string_lossy() + .replace("post-", ""); + to.push(&f); + if !to.exists() { + fs::copy(&from, &to).unwrap(); + } + format!("{d}/{f}") + } + } + )); if !name.is_empty() { t.push(name) } @@ -94,16 +127,9 @@ impl Nex { .unwrap_or_default(), ); - // 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()); - std::fs::create_dir_all(&p)?; - // write the data p.push(published.format(&self.filename).to_string()); - std::fs::write(p, c)?; // @TODO skip overwrite operations + fs::write(p, c)?; // @TODO skip overwrite operations Ok(()) } diff --git a/src/snac/user.rs b/src/snac/user.rs index 2e70678..bb827ae 100644 --- a/src/snac/user.rs +++ b/src/snac/user.rs @@ -7,12 +7,14 @@ use std::path::PathBuf; pub struct User { pub name: String, pub public: PathBuf, + pub root: PathBuf, } impl User { pub fn init(storage: &PathBuf, name: String) -> Result { Ok(Self { - public: init(storage, &name, "public")?, + public: init(storage, &name, Some("public"))?, + root: init(storage, &name, None)?, name, }) } @@ -36,13 +38,28 @@ impl User { Ok(posts) } + + pub fn file(&self, name: &str) -> Result { + let mut p = PathBuf::from(&self.root); + p.push("static"); + p.push(name); + + if !p.exists() || !p.is_file() { + bail!("File destination `{}` not found!", p.to_string_lossy()); + } + + Ok(p) + } } -fn init(storage: &PathBuf, name: &str, target: &str) -> Result { +fn init(storage: &PathBuf, name: &str, dir: Option<&str>) -> Result { let mut p = PathBuf::from(&storage); p.push("user"); p.push(name); - p.push(target); + + if let Some(d) = dir { + p.push(d); + } if !p.exists() || !p.is_dir() { bail!("User data location `{}` not found!", p.to_string_lossy());