From fabc11f9cbc108ef9e33739ca7bbc3c7f21d4514 Mon Sep 17 00:00:00 2001 From: postscriptum Date: Sat, 5 Jul 2025 22:55:35 +0300 Subject: [PATCH] implement `filesystem_file_permissions` argument option, add some tests, optimize init arguments --- README.md | 3 + src/config.rs | 36 +++++--- src/main.rs | 17 +--- src/nex.rs | 204 ++++++++++++++++++++++-------------------- src/nex/attachment.rs | 16 ++-- src/nex/template.rs | 8 +- src/snac.rs | 10 +-- 7 files changed, 152 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index 5f7c18d..e8b9325 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,9 @@ snac2nex -s /path/to/snac/storage -t /path/to/nex -u user1 -u user2 --filesystem-sync Sync filesystem meta (e.g. time modified) + --filesystem-file-permissions + Set new file permissions (macos, linux only) + -k, --keep Keep Nex entry on Snac post was removed diff --git a/src/config.rs b/src/config.rs index a171107..bb5ff20 100644 --- a/src/config.rs +++ b/src/config.rs @@ -52,15 +52,10 @@ pub struct Config { #[arg(long, default_value_t = false)] pub filesystem_sync: bool, - /* @TODO - /// Set directory permissions (macos, linux only) - #[arg(long, value_parser = chmod, default_value_t = 0o755)] - pub chmod_dir: u32, + /// Set new file permissions (macos, linux only) + #[arg(long, value_parser = permissions)] + pub filesystem_file_permissions: Option, - /// Set file permissions (macos, linux only) - #[arg(long, value_parser = chmod, default_value_t = 0o644)] - pub chmod_file: u32, - */ /// Keep Nex entry on Snac post was removed #[arg(short, long, default_value_t = false)] pub keep: bool, @@ -70,10 +65,23 @@ pub struct Config { pub daemon: bool, } -/* @TODO -fn chmod(chmod: &str) -> Result { - if chmod.len() != 3 { - todo!("Expected 3 digits as the Unix value!") +fn permissions(value: &str) -> Result { + if value.len() != 4 { + todo!( + "Expected four octal digits as the Unix value {}!", + value.len() + ) } - u32::from_str_radix(chmod, 8) -} */ + u32::from_str_radix(value, 8) +} + +#[test] +fn test() { + let a: Config = clap::Parser::parse_from([ + "cmd", + "--source=p", + "--target=p", + "--filesystem-file-permissions=0644", + ]); + assert!(a.filesystem_file_permissions.is_some_and(|p| p == 0o644)) +} diff --git a/src/main.rs b/src/main.rs index a4b95f9..96ef1d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,27 +4,18 @@ mod response; mod snac; use anyhow::Result; +use config::Config; use nex::Nex; use response::Response; use snac::Snac; fn main() -> Result<()> { use clap::Parser; - use config::Config; - let c = Config::parse(); - println!("Sync `{}` for users {:?}...", &c.source, &c.user); + let n = Nex::init(&c)?; + let s = Snac::init(&c)?; - let n = Nex::init( - c.target, - c.format_filename, - c.format_updated, - c.format_content, - c.attachment, - (!c.keep, !c.daemon, c.filesystem_sync), - &c.user, - )?; - let s = Snac::init(c.source, c.user)?; + println!("Sync `{}` for users {:?}...", &c.source, &c.user); let mut r = sync(&s, &n, !c.daemon)?; match c.rotate { diff --git a/src/nex.rs b/src/nex.rs index 713950e..a29d64d 100644 --- a/src/nex.rs +++ b/src/nex.rs @@ -10,7 +10,7 @@ use response::{Clean, Sync, change::Change}; use source::Source; use std::{ collections::{HashMap, HashSet}, - fs, + fs::{self, Permissions}, path::{MAIN_SEPARATOR, PathBuf}, str::FromStr, }; @@ -18,6 +18,7 @@ use template::Template; pub struct Nex { attachment: Attachment, + file_permissions: Option, filename: String, is_cleanup: bool, is_debug: bool, @@ -27,44 +28,50 @@ pub struct Nex { } impl Nex { - pub fn init( - target_dir: String, - filename: String, - time_format: String, - pattern: String, - attachment_mode: Option, - (is_cleanup, is_debug, is_filesystem_sync): (bool, bool, bool), - user_names: &Vec, - ) -> Result { + pub fn init(config: &crate::Config) -> Result { // validate filename - if filename + if config + .format_content .trim_matches(MAIN_SEPARATOR) .contains(MAIN_SEPARATOR) { bail!("File name should not contain subdir `{MAIN_SEPARATOR}` separator!") } // init data export location - let target = PathBuf::from_str(&target_dir)?.canonicalize()?; + let target = PathBuf::from_str(&config.target)?.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 users = HashMap::with_capacity(config.user.len()); + for u in &config.user { let mut p = PathBuf::from(&target); p.push(u); fs::create_dir_all(&p)?; users.insert(u.clone(), p); } // init document template formatter - let template = Template::init(&filename, pattern, time_format); + let template = Template::init(config); Ok(Self { - attachment: Attachment::init(attachment_mode)?, - filename, - is_cleanup, - is_debug, - is_filesystem_sync, + attachment: Attachment::init(config.attachment.as_ref())?, + file_permissions: { + #[cfg(any(target_os = "linux", target_os = "macos"))] + { + use std::{fs::Permissions, os::unix::fs::PermissionsExt}; + Some(Permissions::from_mode( + config.filesystem_file_permissions.unwrap_or(0o644), + )) + } + #[cfg(not(any(target_os = "linux", target_os = "macos",)))] + { + None // @TODO + } + }, + filename: config.format_filename.clone(), + is_cleanup: !config.keep, + is_debug: !config.daemon, + is_filesystem_sync: config.filesystem_sync, template, users, }) @@ -136,11 +143,7 @@ impl Nex { if !fs::read_to_string(&s).is_ok_and(|this| this == state) { fs::remove_dir_all(&d)?; fs::create_dir_all(&d)?; - if self.is_filesystem_sync { - sync(&s, timestamp.into(), state.as_bytes())? - } else { - fs::write(&s, state)? - } + self.persist(&s, ×tamp, state.as_bytes())?; } else { if let Some(ref mut i) = index { if let Some(ref a) = attachments { @@ -160,70 +163,67 @@ impl Nex { } // save post file, set its change status - let change = if fs::exists(&f)? { - Change::Updated - } else { - Change::Created - }; - let template = self.template.build( - updated, - content, - link, - tags, - attachments.map(|a| { - let mut b = Vec::with_capacity(a.len()); - for (n, (name, media_type, source)) in a.into_iter().enumerate() { - b.push(( - match source { - Source::Url(url) => url, - Source::File(from) => { - let to = attachment::filepath(&d, &from, n); - let uri = format!( - "{}/{}", - d.file_name().unwrap().to_string_lossy(), - to.file_name().unwrap().to_string_lossy() - ); - self.attachment - .sync( - &from, - &to, - if self.is_filesystem_sync { - Some(timestamp.into()) - } else { - None - }, - ) - .unwrap(); - if let Some(ref mut i) = index { - i.push(to); - } - uri - } - }, - { - let mut alt = Vec::with_capacity(2); - if !name.is_empty() { - alt.push(name) - } - if !media_type.is_empty() { - alt.push(format!("({media_type})")) - } - if alt.is_empty() { - None - } else { - Some(alt.join(" ")) - } - }, - )) - } - b - }), - ); - if self.is_filesystem_sync { - sync(&f, timestamp.into(), template.as_bytes())? - } else { - fs::write(&f, template)? - } + let change = self.persist( + &f, + ×tamp, + self.template + .build( + updated, + content, + link, + tags, + attachments.map(|a| { + let mut b = Vec::with_capacity(a.len()); + for (n, (name, media_type, source)) in a.into_iter().enumerate() { + b.push(( + match source { + Source::Url(url) => url, + Source::File(from) => { + let to = attachment::filepath(&d, &from, n); + let uri = format!( + "{}/{}", + d.file_name().unwrap().to_string_lossy(), + to.file_name().unwrap().to_string_lossy() + ); + self.attachment + .sync( + &from, + &to, + if self.is_filesystem_sync { + Some(timestamp.into()) + } else { + None + }, + self.file_permissions.clone(), + ) + .unwrap(); + if let Some(ref mut i) = index { + i.push(to); + } + uri + } + }, + { + let mut alt = Vec::with_capacity(2); + if !name.is_empty() { + alt.push(name) + } + if !media_type.is_empty() { + alt.push(format!("({media_type})")) + } + if alt.is_empty() { + None + } else { + Some(alt.join(" ")) + } + }, + )) + } + b + }), + ) + .as_bytes(), + )?; // move all paths processed to cleanup ignore if let Some(ref mut i) = index { i.extend([d, f, p, s]); @@ -279,13 +279,23 @@ impl Nex { pub fn is_cleanup(&self) -> bool { self.is_cleanup } -} -/// Wrapper function to change time updated for new file, according to the Snac time. -fn sync(path: &PathBuf, modified: std::time::SystemTime, data: &[u8]) -> Result<()> { - use std::io::Write; - let mut f = fs::File::create(path)?; - f.write_all(data)?; - f.set_modified(modified)?; // it's important to call after write - Ok(()) + /// Wrapper function to change time updated for new file, according to the Snac time. + fn persist(&self, path: &PathBuf, modified: &DateTime, data: &[u8]) -> Result { + use std::io::Write; + let status = if fs::exists(path)? { + Change::Updated + } else { + Change::Created + }; + let mut f = fs::File::create(path)?; + f.write_all(data)?; + if self.is_filesystem_sync { + f.set_modified((*modified).into())?; // it's important to call after write + } + if let Some(ref p) = self.file_permissions { + f.set_permissions(p.clone())? + } + Ok(status) + } } diff --git a/src/nex/attachment.rs b/src/nex/attachment.rs index ef838e8..2a2e9d3 100644 --- a/src/nex/attachment.rs +++ b/src/nex/attachment.rs @@ -1,5 +1,6 @@ use anyhow::{Result, bail}; use std::{ + fs::Permissions, path::{Path, PathBuf}, time::SystemTime, }; @@ -12,7 +13,7 @@ pub enum Attachment { } impl Attachment { - pub fn init(method: Option) -> Result { + pub fn init(method: Option<&String>) -> Result { Ok(match method { Some(m) => match m.to_lowercase().as_str() { "c" | "copy" => Self::Copy, @@ -28,19 +29,14 @@ impl Attachment { source: &PathBuf, target: &PathBuf, modified: Option, + file_permissions: Option, ) -> Result<()> { use std::{fs, os}; match self { Attachment::Copy => { fs::copy(source, target)?; - #[cfg(any(target_os = "linux", target_os = "macos"))] - { - use std::{fs::Permissions, os::unix::fs::PermissionsExt}; - fs::set_permissions(target, Permissions::from_mode(0o644))?; // @TODO optional - } - #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos",)))] - { - todo!("Platform yet not supported") + if let Some(p) = file_permissions { + fs::set_permissions(target, p)? } println!( "\t\t\tcopied attachment file `{}`", @@ -49,6 +45,7 @@ impl Attachment { } Attachment::HardLink => { fs::hard_link(source, target)?; + // @TODO set_permissions println!( "\t\t\tcreated hard link `{}` to attachment {}", target.to_string_lossy(), @@ -68,6 +65,7 @@ impl Attachment { { todo!("Platform yet not supported") } + // @TODO set_permissions println!( "\t\t\tcreated soft link `{}` to attachment {}", target.to_string_lossy(), diff --git a/src/nex/template.rs b/src/nex/template.rs index 54d7f4d..4e3c16c 100644 --- a/src/nex/template.rs +++ b/src/nex/template.rs @@ -10,11 +10,11 @@ pub struct Template { } impl Template { - pub fn init(filename: &str, pattern: String, time_format: String) -> Self { + pub fn init(config: &crate::Config) -> Self { Self { - format: Format::init(filename), - pattern, - time_format, + format: Format::init(&config.format_filename), + pattern: config.format_content.clone(), + time_format: config.format_updated.clone(), } } pub fn build( diff --git a/src/snac.rs b/src/snac.rs index a3fb8e4..47be364 100644 --- a/src/snac.rs +++ b/src/snac.rs @@ -9,15 +9,15 @@ pub struct Snac { } impl Snac { - pub fn init(storage_dir: String, user_names: Vec) -> Result { - let storage = PathBuf::from_str(&storage_dir)?.canonicalize()?; + pub fn init(config: &crate::Config) -> Result { + let storage = PathBuf::from_str(&config.source)?.canonicalize()?; if !storage.is_dir() { bail!("Target location is not directory!"); } - let mut users = Vec::with_capacity(user_names.len()); - for name in user_names { - users.push(User::init(&storage, name)?) + let mut users = Vec::with_capacity(config.user.len()); + for name in &config.user { + users.push(User::init(&storage, name.clone())?) } Ok(Self { users })