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
``` bash
-s, --source <SOURCE>
Path to the Snac2 profile directory
-s, --source <SOURCE>
Path to the Snac2 profile directory
-t, --target <TARGET>
Target directory for public data export
-t, --target <TARGET>
Target directory for public data export
-u, --user <USER>
Username to export
-u, --user <USER>
Username to export
-r, --rotate <ROTATE>
Keep running as the daemon, renew every `n` seconds
-b, --binary
Export binary files (attachments)
-f, --format-content <FORMAT_CONTENT>
Post template pattern
-r, --rotate <ROTATE>
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>
Post filenames format
[default: {content}{attachments}{link}{tags}{updated}]
* escaped with `%%`
--format-filename <FORMAT_FILENAME>
Post filenames format
[default: %H:%M:%S.txt]
* escaped with `%%`
--format-updated <FORMAT_UPDATED>
Post `{updated}` time format
[default: %H:%M:%S.txt]
* escaped with `%%`
--format-updated <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
```

View file

@ -15,6 +15,10 @@ pub struct Config {
#[arg(short, long)]
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
#[arg(short, long)]
pub rotate: Option<u64>,

View file

@ -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

View file

@ -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<Vec<(String, String, String)>>,
attachments: Option<Vec<(String, String, Source)>>,
tags: Option<Vec<String>>,
(published, updated): (DateTime<Utc>, Option<DateTime<Utc>>),
) -> 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(())
}

View file

@ -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<Self> {
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<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);
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());