implement filesystem_file_permissions argument option, add some tests, optimize init arguments

This commit is contained in:
postscriptum 2025-07-05 22:55:35 +03:00
parent 182d6892c3
commit fabc11f9cb
7 changed files with 152 additions and 142 deletions

View file

@ -67,6 +67,9 @@ snac2nex -s /path/to/snac/storage -t /path/to/nex -u user1 -u user2
--filesystem-sync --filesystem-sync
Sync filesystem meta (e.g. time modified) Sync filesystem meta (e.g. time modified)
--filesystem-file-permissions <FILESYSTEM_FILE_PERMISSIONS>
Set new file permissions (macos, linux only)
-k, --keep -k, --keep
Keep Nex entry on Snac post was removed Keep Nex entry on Snac post was removed

View file

@ -52,15 +52,10 @@ pub struct Config {
#[arg(long, default_value_t = false)] #[arg(long, default_value_t = false)]
pub filesystem_sync: bool, pub filesystem_sync: bool,
/* @TODO /// Set new file permissions (macos, linux only)
/// Set directory permissions (macos, linux only) #[arg(long, value_parser = permissions)]
#[arg(long, value_parser = chmod, default_value_t = 0o755)] pub filesystem_file_permissions: Option<u32>,
pub chmod_dir: u32,
/// 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 /// Keep Nex entry on Snac post was removed
#[arg(short, long, default_value_t = false)] #[arg(short, long, default_value_t = false)]
pub keep: bool, pub keep: bool,
@ -70,10 +65,23 @@ pub struct Config {
pub daemon: bool, pub daemon: bool,
} }
/* @TODO fn permissions(value: &str) -> Result<u32, std::num::ParseIntError> {
fn chmod(chmod: &str) -> Result<u32, std::num::ParseIntError> { if value.len() != 4 {
if chmod.len() != 3 { todo!(
todo!("Expected 3 digits as the Unix value!") "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))
}

View file

@ -4,27 +4,18 @@ mod response;
mod snac; mod snac;
use anyhow::Result; use anyhow::Result;
use config::Config;
use nex::Nex; use nex::Nex;
use response::Response; use response::Response;
use snac::Snac; use snac::Snac;
fn main() -> Result<()> { fn main() -> Result<()> {
use clap::Parser; use clap::Parser;
use config::Config;
let c = Config::parse(); 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( println!("Sync `{}` for users {:?}...", &c.source, &c.user);
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)?;
let mut r = sync(&s, &n, !c.daemon)?; let mut r = sync(&s, &n, !c.daemon)?;
match c.rotate { match c.rotate {

View file

@ -10,7 +10,7 @@ use response::{Clean, Sync, change::Change};
use source::Source; use source::Source;
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
fs, fs::{self, Permissions},
path::{MAIN_SEPARATOR, PathBuf}, path::{MAIN_SEPARATOR, PathBuf},
str::FromStr, str::FromStr,
}; };
@ -18,6 +18,7 @@ use template::Template;
pub struct Nex { pub struct Nex {
attachment: Attachment, attachment: Attachment,
file_permissions: Option<Permissions>,
filename: String, filename: String,
is_cleanup: bool, is_cleanup: bool,
is_debug: bool, is_debug: bool,
@ -27,44 +28,50 @@ pub struct Nex {
} }
impl Nex { impl Nex {
pub fn init( pub fn init(config: &crate::Config) -> Result<Self> {
target_dir: String,
filename: String,
time_format: String,
pattern: String,
attachment_mode: Option<String>,
(is_cleanup, is_debug, is_filesystem_sync): (bool, bool, bool),
user_names: &Vec<String>,
) -> Result<Self> {
// validate filename // validate filename
if filename if config
.format_content
.trim_matches(MAIN_SEPARATOR) .trim_matches(MAIN_SEPARATOR)
.contains(MAIN_SEPARATOR) .contains(MAIN_SEPARATOR)
{ {
bail!("File name should not contain subdir `{MAIN_SEPARATOR}` separator!") bail!("File name should not contain subdir `{MAIN_SEPARATOR}` separator!")
} }
// init data export location // init data export location
let target = PathBuf::from_str(&target_dir)?.canonicalize()?; let target = PathBuf::from_str(&config.target)?.canonicalize()?;
if !target.is_dir() { if !target.is_dir() {
bail!("Target location is not directory!") bail!("Target location is not directory!")
} }
// init locations for each user // init locations for each user
let mut users = HashMap::with_capacity(user_names.len()); let mut users = HashMap::with_capacity(config.user.len());
for u in user_names { for u in &config.user {
let mut p = PathBuf::from(&target); let mut p = PathBuf::from(&target);
p.push(u); p.push(u);
fs::create_dir_all(&p)?; fs::create_dir_all(&p)?;
users.insert(u.clone(), p); users.insert(u.clone(), p);
} }
// init document template formatter // init document template formatter
let template = Template::init(&filename, pattern, time_format); let template = Template::init(config);
Ok(Self { Ok(Self {
attachment: Attachment::init(attachment_mode)?, attachment: Attachment::init(config.attachment.as_ref())?,
filename, file_permissions: {
is_cleanup, #[cfg(any(target_os = "linux", target_os = "macos"))]
is_debug, {
is_filesystem_sync, 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, template,
users, users,
}) })
@ -136,11 +143,7 @@ impl Nex {
if !fs::read_to_string(&s).is_ok_and(|this| this == state) { if !fs::read_to_string(&s).is_ok_and(|this| this == state) {
fs::remove_dir_all(&d)?; fs::remove_dir_all(&d)?;
fs::create_dir_all(&d)?; fs::create_dir_all(&d)?;
if self.is_filesystem_sync { self.persist(&s, &timestamp, state.as_bytes())?;
sync(&s, timestamp.into(), state.as_bytes())?
} else {
fs::write(&s, state)?
}
} else { } else {
if let Some(ref mut i) = index { if let Some(ref mut i) = index {
if let Some(ref a) = attachments { if let Some(ref a) = attachments {
@ -160,70 +163,67 @@ impl Nex {
} }
// save post file, set its change status // save post file, set its change status
let change = if fs::exists(&f)? { let change = self.persist(
Change::Updated &f,
} else { &timestamp,
Change::Created self.template
}; .build(
let template = self.template.build( updated,
updated, content,
content, link,
link, tags,
tags, attachments.map(|a| {
attachments.map(|a| { let mut b = Vec::with_capacity(a.len());
let mut b = Vec::with_capacity(a.len()); for (n, (name, media_type, source)) in a.into_iter().enumerate() {
for (n, (name, media_type, source)) in a.into_iter().enumerate() { b.push((
b.push(( match source {
match source { Source::Url(url) => url,
Source::Url(url) => url, Source::File(from) => {
Source::File(from) => { let to = attachment::filepath(&d, &from, n);
let to = attachment::filepath(&d, &from, n); let uri = format!(
let uri = format!( "{}/{}",
"{}/{}", d.file_name().unwrap().to_string_lossy(),
d.file_name().unwrap().to_string_lossy(), to.file_name().unwrap().to_string_lossy()
to.file_name().unwrap().to_string_lossy() );
); self.attachment
self.attachment .sync(
.sync( &from,
&from, &to,
&to, if self.is_filesystem_sync {
if self.is_filesystem_sync { Some(timestamp.into())
Some(timestamp.into()) } else {
} else { None
None },
}, self.file_permissions.clone(),
) )
.unwrap(); .unwrap();
if let Some(ref mut i) = index { if let Some(ref mut i) = index {
i.push(to); i.push(to);
} }
uri uri
} }
}, },
{ {
let mut alt = Vec::with_capacity(2); let mut alt = Vec::with_capacity(2);
if !name.is_empty() { if !name.is_empty() {
alt.push(name) alt.push(name)
} }
if !media_type.is_empty() { if !media_type.is_empty() {
alt.push(format!("({media_type})")) alt.push(format!("({media_type})"))
} }
if alt.is_empty() { if alt.is_empty() {
None None
} else { } else {
Some(alt.join(" ")) Some(alt.join(" "))
} }
}, },
)) ))
} }
b b
}), }),
); )
if self.is_filesystem_sync { .as_bytes(),
sync(&f, timestamp.into(), template.as_bytes())? )?;
} else {
fs::write(&f, template)?
}
// move all paths processed to cleanup ignore // move all paths processed to cleanup ignore
if let Some(ref mut i) = index { if let Some(ref mut i) = index {
i.extend([d, f, p, s]); i.extend([d, f, p, s]);
@ -279,13 +279,23 @@ impl Nex {
pub fn is_cleanup(&self) -> bool { pub fn is_cleanup(&self) -> bool {
self.is_cleanup self.is_cleanup
} }
}
/// Wrapper function to change time updated for new file, according to the Snac time. /// 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<()> { fn persist(&self, path: &PathBuf, modified: &DateTime<Utc>, data: &[u8]) -> Result<Change> {
use std::io::Write; use std::io::Write;
let mut f = fs::File::create(path)?; let status = if fs::exists(path)? {
f.write_all(data)?; Change::Updated
f.set_modified(modified)?; // it's important to call after write } else {
Ok(()) 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)
}
} }

View file

@ -1,5 +1,6 @@
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use std::{ use std::{
fs::Permissions,
path::{Path, PathBuf}, path::{Path, PathBuf},
time::SystemTime, time::SystemTime,
}; };
@ -12,7 +13,7 @@ pub enum Attachment {
} }
impl Attachment { impl Attachment {
pub fn init(method: Option<String>) -> Result<Self> { pub fn init(method: Option<&String>) -> Result<Self> {
Ok(match method { Ok(match method {
Some(m) => match m.to_lowercase().as_str() { Some(m) => match m.to_lowercase().as_str() {
"c" | "copy" => Self::Copy, "c" | "copy" => Self::Copy,
@ -28,19 +29,14 @@ impl Attachment {
source: &PathBuf, source: &PathBuf,
target: &PathBuf, target: &PathBuf,
modified: Option<SystemTime>, modified: Option<SystemTime>,
file_permissions: Option<Permissions>,
) -> Result<()> { ) -> Result<()> {
use std::{fs, os}; use std::{fs, os};
match self { match self {
Attachment::Copy => { Attachment::Copy => {
fs::copy(source, target)?; fs::copy(source, target)?;
#[cfg(any(target_os = "linux", target_os = "macos"))] if let Some(p) = file_permissions {
{ fs::set_permissions(target, p)?
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")
} }
println!( println!(
"\t\t\tcopied attachment file `{}`", "\t\t\tcopied attachment file `{}`",
@ -49,6 +45,7 @@ impl Attachment {
} }
Attachment::HardLink => { Attachment::HardLink => {
fs::hard_link(source, target)?; fs::hard_link(source, target)?;
// @TODO set_permissions
println!( println!(
"\t\t\tcreated hard link `{}` to attachment {}", "\t\t\tcreated hard link `{}` to attachment {}",
target.to_string_lossy(), target.to_string_lossy(),
@ -68,6 +65,7 @@ impl Attachment {
{ {
todo!("Platform yet not supported") todo!("Platform yet not supported")
} }
// @TODO set_permissions
println!( println!(
"\t\t\tcreated soft link `{}` to attachment {}", "\t\t\tcreated soft link `{}` to attachment {}",
target.to_string_lossy(), target.to_string_lossy(),

View file

@ -10,11 +10,11 @@ pub struct Template {
} }
impl Template { impl Template {
pub fn init(filename: &str, pattern: String, time_format: String) -> Self { pub fn init(config: &crate::Config) -> Self {
Self { Self {
format: Format::init(filename), format: Format::init(&config.format_filename),
pattern, pattern: config.format_content.clone(),
time_format, time_format: config.format_updated.clone(),
} }
} }
pub fn build( pub fn build(

View file

@ -9,15 +9,15 @@ pub struct Snac {
} }
impl Snac { impl Snac {
pub fn init(storage_dir: String, user_names: Vec<String>) -> Result<Self> { pub fn init(config: &crate::Config) -> Result<Self> {
let storage = PathBuf::from_str(&storage_dir)?.canonicalize()?; let storage = PathBuf::from_str(&config.source)?.canonicalize()?;
if !storage.is_dir() { if !storage.is_dir() {
bail!("Target location is not directory!"); bail!("Target location is not directory!");
} }
let mut users = Vec::with_capacity(user_names.len()); let mut users = Vec::with_capacity(config.user.len());
for name in user_names { for name in &config.user {
users.push(User::init(&storage, name)?) users.push(User::init(&storage, name.clone())?)
} }
Ok(Self { users }) Ok(Self { users })