implement optional binary files copy

This commit is contained in:
postscriptum 2025-07-01 20:13:44 +03:00
parent cf8c676732
commit ef1ba39589
5 changed files with 101 additions and 43 deletions

View file

@ -21,40 +21,43 @@ snac2nex -s /path/to/snac/storage -t /path/to/nex -u user1 -u user2
### Options ### Options
``` bash ``` bash
-s, --source <SOURCE> -s, --source <SOURCE>
Path to the Snac2 profile directory Path to the Snac2 profile directory
-t, --target <TARGET> -t, --target <TARGET>
Target directory for public data export Target directory for public data export
-u, --user <USER> -u, --user <USER>
Username to export Username to export
-r, --rotate <ROTATE> -b, --binary
Keep running as the daemon, renew every `n` seconds Export binary files (attachments)
-f, --format-content <FORMAT_CONTENT> -r, --rotate <ROTATE>
Post template pattern Keep running as the daemon, renew every `n` seconds
[default: {content}{attachments}{link}{tags}{updated}] -f, --format-content <FORMAT_CONTENT>
Post template pattern
--format-filename <FORMAT_FILENAME> [default: {content}{attachments}{link}{tags}{updated}]
Post filenames format
* escaped with `%%` --format-filename <FORMAT_FILENAME>
Post filenames format
[default: %H:%M:%S.txt] * escaped with `%%`
--format-updated <FORMAT_UPDATED> [default: %H:%M:%S.txt]
Post `{updated}` time format
* escaped with `%%` --format-updated <FORMAT_UPDATED>
Post `{updated}` time format
[default: "%Y/%m/%d %H:%M:%S"] * escaped with `%%`
-h, --help [default: "%Y/%m/%d %H:%M:%S"]
Print help (see a summary with '-h')
-V, --version -h, --help
Print version Print help (see a summary with '-h')
-V, --version
Print version
``` ```

View file

@ -15,6 +15,10 @@ pub struct Config {
#[arg(short, long)] #[arg(short, long)]
pub user: Vec<String>, pub user: Vec<String>,
/// Export binary files (attachments)
#[arg(short, long, default_value_t = false)]
pub binary: bool,
/// Keep running as the daemon, renew every `n` seconds /// Keep running as the daemon, renew every `n` seconds
#[arg(short, long)] #[arg(short, long)]
pub rotate: Option<u64>, pub rotate: Option<u64>,

View file

@ -21,12 +21,12 @@ fn main() -> Result<()> {
let s = Snac::init(c.source, c.user)?; let s = Snac::init(c.source, c.user)?;
println!("export begin..."); println!("export begin...");
let (mut u, mut t) = sync(&s, &n)?; let (mut u, mut t) = sync(&s, &n, c.binary)?;
match c.rotate { match c.rotate {
Some(r) => loop { Some(r) => loop {
println!("queue completed (updated: {u} / total: {t}), await {r} seconds to rotate..."); println!("queue completed (updated: {u} / total: {t}), await {r} seconds to rotate...");
std::thread::sleep(std::time::Duration::from_secs(r)); 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})."), None => println!("export completed (updated: {u} / total: {t})."),
} }
@ -34,7 +34,7 @@ fn main() -> Result<()> {
Ok(()) 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 t = 0; // total
let mut u = 0; // updated let mut u = 0; // updated
for user in &snac.users { for user in &snac.users {
@ -49,12 +49,20 @@ fn sync(snac: &Snac, nex: &Nex) -> Result<(usize, usize)> {
content, content,
post.url, post.url,
post.attachment.map(|a| { post.attachment.map(|a| {
use nex::Source;
let mut attachments = Vec::with_capacity(a.len()); let mut attachments = Vec::with_capacity(a.len());
for attachment in a { for attachment in a {
attachments.push(( attachments.push((
attachment.name, attachment.name,
attachment.media_type, 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 attachments

View file

@ -1,6 +1,11 @@
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use chrono::{DateTime, Utc}; 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 { pub struct Nex {
filename: String, filename: String,
@ -27,7 +32,7 @@ impl Nex {
for u in user_names { for u in user_names {
let mut p = PathBuf::from(&target); let mut p = PathBuf::from(&target);
p.push(u); p.push(u);
std::fs::create_dir_all(&p)?; fs::create_dir_all(&p)?;
users.insert(u.clone(), p); users.insert(u.clone(), p);
} }
@ -44,10 +49,17 @@ impl Nex {
name: &str, name: &str,
content: String, content: String,
link: String, link: String,
attachments: Option<Vec<(String, String, String)>>, attachments: Option<Vec<(String, String, Source)>>,
tags: Option<Vec<String>>, tags: Option<Vec<String>>,
(published, updated): (DateTime<Utc>, Option<DateTime<Utc>>), (published, updated): (DateTime<Utc>, Option<DateTime<Utc>>),
) -> Result<()> { ) -> 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 // format content pattern
let c = self let c = self
.pattern .pattern
@ -65,9 +77,30 @@ impl Nex {
.map(|a| { .map(|a| {
let mut b = Vec::with_capacity(a.len()); let mut b = Vec::with_capacity(a.len());
b.push("\n".to_string()); 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); 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() { if !name.is_empty() {
t.push(name) t.push(name)
} }
@ -94,16 +127,9 @@ impl Nex {
.unwrap_or_default(), .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 // write the data
p.push(published.format(&self.filename).to_string()); 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(()) Ok(())
} }

View file

@ -7,12 +7,14 @@ use std::path::PathBuf;
pub struct User { pub struct User {
pub name: String, pub name: String,
pub public: PathBuf, pub public: PathBuf,
pub root: PathBuf,
} }
impl User { impl User {
pub fn init(storage: &PathBuf, name: String) -> Result<Self> { pub fn init(storage: &PathBuf, name: String) -> Result<Self> {
Ok(Self { Ok(Self {
public: init(storage, &name, "public")?, public: init(storage, &name, Some("public"))?,
root: init(storage, &name, None)?,
name, name,
}) })
} }
@ -36,13 +38,28 @@ impl User {
Ok(posts) Ok(posts)
} }
pub fn file(&self, name: &str) -> Result<PathBuf> {
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<PathBuf> { fn init(storage: &PathBuf, name: &str, dir: Option<&str>) -> Result<PathBuf> {
let mut p = PathBuf::from(&storage); let mut p = PathBuf::from(&storage);
p.push("user"); p.push("user");
p.push(name); p.push(name);
p.push(target);
if let Some(d) = dir {
p.push(d);
}
if !p.exists() || !p.is_dir() { if !p.exists() || !p.is_dir() {
bail!("User data location `{}` not found!", p.to_string_lossy()); bail!("User data location `{}` not found!", p.to_string_lossy());