mirror of
https://codeberg.org/postscriptum/snac2nex.git
synced 2026-04-01 13:45:28 +00:00
init separated format controller, detect content pattern by format_filename extension or trailing slash availability
This commit is contained in:
parent
776e929b01
commit
678f0c6d57
4 changed files with 174 additions and 78 deletions
|
|
@ -50,8 +50,10 @@ snac2nex -s /path/to/snac/storage -t /path/to/nex -u user1 -u user2
|
||||||
--format-filename <FORMAT_FILENAME>
|
--format-filename <FORMAT_FILENAME>
|
||||||
Post filenames format
|
Post filenames format
|
||||||
|
|
||||||
* escaped with `%%`
|
* append trailing slash to parse documents as the directory
|
||||||
* be careful when removing seconds, as it may cause overwrite collisions
|
* append `.gmi` or `.gemtext` extension to parse documents as the Gemtext
|
||||||
|
* escape with `%%` when using in `systemd` context
|
||||||
|
* be careful when removing the seconds part, as it may cause overwrite collisions!
|
||||||
|
|
||||||
[default: %H-%M-%S]
|
[default: %H-%M-%S]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,10 @@ pub struct Config {
|
||||||
|
|
||||||
/// Post filenames format
|
/// Post filenames format
|
||||||
///
|
///
|
||||||
/// * escaped with `%%`
|
/// * append trailing slash to parse documents as the directory
|
||||||
/// * be careful when removing seconds, as it may cause overwrite collisions
|
/// * append `.gmi` or `.gemtext` extension to parse documents as the Gemtext
|
||||||
|
/// * escape with `%%` when using in `systemd` context
|
||||||
|
/// * be careful when removing the seconds part, as it may cause overwrite collisions!
|
||||||
#[arg(long, default_value_t = String::from("%H-%M-%S"))]
|
#[arg(long, default_value_t = String::from("%H-%M-%S"))]
|
||||||
pub format_filename: String,
|
pub format_filename: String,
|
||||||
|
|
||||||
|
|
|
||||||
130
src/nex.rs
130
src/nex.rs
|
|
@ -1,10 +1,12 @@
|
||||||
mod attachment;
|
mod attachment;
|
||||||
|
mod format;
|
||||||
pub mod response;
|
pub mod response;
|
||||||
pub mod source;
|
pub mod source;
|
||||||
|
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use attachment::Attachment;
|
use attachment::Attachment;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use format::Format;
|
||||||
use response::{Clean, Sync, change::Change};
|
use response::{Clean, Sync, change::Change};
|
||||||
use source::Source;
|
use source::Source;
|
||||||
use std::{
|
use std::{
|
||||||
|
|
@ -17,10 +19,9 @@ use std::{
|
||||||
pub struct Nex {
|
pub struct Nex {
|
||||||
attachment: Attachment,
|
attachment: Attachment,
|
||||||
filename: String,
|
filename: String,
|
||||||
|
format: Format,
|
||||||
is_cleanup: bool,
|
is_cleanup: bool,
|
||||||
is_debug: bool,
|
is_debug: bool,
|
||||||
pattern: String,
|
|
||||||
time_format: String,
|
|
||||||
users: HashMap<String, PathBuf>,
|
users: HashMap<String, PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,10 +35,6 @@ impl Nex {
|
||||||
(is_cleanup, is_debug): (bool, bool),
|
(is_cleanup, is_debug): (bool, bool),
|
||||||
user_names: &Vec<String>,
|
user_names: &Vec<String>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
use std::path::MAIN_SEPARATOR;
|
|
||||||
if filename.contains(MAIN_SEPARATOR) {
|
|
||||||
bail!("Filename pattern should not contain `{MAIN_SEPARATOR}` characters!")
|
|
||||||
}
|
|
||||||
// init data export location
|
// init data export location
|
||||||
let target = PathBuf::from_str(&target_dir)?.canonicalize()?;
|
let target = PathBuf::from_str(&target_dir)?.canonicalize()?;
|
||||||
if !target.is_dir() {
|
if !target.is_dir() {
|
||||||
|
|
@ -51,14 +48,15 @@ impl Nex {
|
||||||
fs::create_dir_all(&p)?;
|
fs::create_dir_all(&p)?;
|
||||||
users.insert(u.clone(), p);
|
users.insert(u.clone(), p);
|
||||||
}
|
}
|
||||||
|
// init document format
|
||||||
|
let format = Format::init(&filename, pattern, time_format);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
attachment: Attachment::init(attachment_mode)?,
|
attachment: Attachment::init(attachment_mode)?,
|
||||||
filename,
|
filename,
|
||||||
|
format,
|
||||||
is_cleanup,
|
is_cleanup,
|
||||||
is_debug,
|
is_debug,
|
||||||
pattern,
|
|
||||||
time_format,
|
|
||||||
users,
|
users,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -106,7 +104,9 @@ impl Nex {
|
||||||
fs::create_dir_all(&p)?;
|
fs::create_dir_all(&p)?;
|
||||||
|
|
||||||
// init shared post ID once
|
// init shared post ID once
|
||||||
let id = published.format(&self.filename).to_string();
|
let id = published
|
||||||
|
.format(self.filename.trim_matches('/'))
|
||||||
|
.to_string();
|
||||||
|
|
||||||
// init post filepath
|
// init post filepath
|
||||||
let mut f = PathBuf::from(&p);
|
let mut f = PathBuf::from(&p);
|
||||||
|
|
@ -146,77 +146,59 @@ impl Nex {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// format content pattern
|
|
||||||
let c = self
|
|
||||||
.pattern
|
|
||||||
.replace(
|
|
||||||
"{content}",
|
|
||||||
&if self.pattern.contains("{tags}") {
|
|
||||||
content.replace("#", "")
|
|
||||||
} else {
|
|
||||||
content
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
"{attachments}",
|
|
||||||
&attachments
|
|
||||||
.map(|a| {
|
|
||||||
let mut b = Vec::with_capacity(a.len());
|
|
||||||
b.push("\n".to_string());
|
|
||||||
for (n, (name, media_type, source)) in a.into_iter().enumerate() {
|
|
||||||
let mut t = Vec::with_capacity(3);
|
|
||||||
t.push(format!(
|
|
||||||
"=> {}",
|
|
||||||
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).unwrap();
|
|
||||||
if let Some(ref mut i) = index {
|
|
||||||
i.push(to);
|
|
||||||
}
|
|
||||||
uri
|
|
||||||
}
|
|
||||||
}
|
|
||||||
));
|
|
||||||
if !name.is_empty() {
|
|
||||||
t.push(name)
|
|
||||||
}
|
|
||||||
if !media_type.is_empty() {
|
|
||||||
t.push(format!("({media_type})"))
|
|
||||||
}
|
|
||||||
b.push(t.join(" "))
|
|
||||||
}
|
|
||||||
b.join("\n")
|
|
||||||
})
|
|
||||||
.unwrap_or_default(),
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
"{tags}",
|
|
||||||
&tags
|
|
||||||
.map(|t| format!("\n\n{}", t.join(", ")))
|
|
||||||
.unwrap_or_default(),
|
|
||||||
)
|
|
||||||
.replace("{link}", &format!("\n\n=> {link}"))
|
|
||||||
.replace(
|
|
||||||
"{updated}",
|
|
||||||
&updated
|
|
||||||
.map(|t| format!("\n\n✏ {}", t.format(&self.time_format)))
|
|
||||||
.unwrap_or_default(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// save post file, set its change status
|
// save post file, set its change status
|
||||||
let change = if fs::exists(&f)? {
|
let change = if fs::exists(&f)? {
|
||||||
Change::Updated
|
Change::Updated
|
||||||
} else {
|
} else {
|
||||||
Change::Created
|
Change::Created
|
||||||
};
|
};
|
||||||
fs::write(&f, c)?;
|
fs::write(
|
||||||
|
&f,
|
||||||
|
self.format.to_string(
|
||||||
|
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).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
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
|
||||||
// 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 {
|
||||||
|
|
|
||||||
110
src/nex/format.rs
Normal file
110
src/nex/format.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
pub enum Format {
|
||||||
|
/// Format as [Gemtext](https://geminiprotocol.net/docs/gemtext.gmi)
|
||||||
|
/// * detected in case, when the `format_filename` option has `.gmi` or `.gemtext` suffix in pattern
|
||||||
|
Gemtext {
|
||||||
|
pattern: String,
|
||||||
|
time_format: String,
|
||||||
|
},
|
||||||
|
/// It is useful to enable clickable links
|
||||||
|
/// * detected in case, when the `format_filename` option has trailing slash in pattern
|
||||||
|
/// * the server should support appending a trailing slash in this case
|
||||||
|
Dir {
|
||||||
|
pattern: String,
|
||||||
|
time_format: String,
|
||||||
|
},
|
||||||
|
/// Ignore markdown
|
||||||
|
Plain {
|
||||||
|
pattern: String,
|
||||||
|
time_format: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Format {
|
||||||
|
pub fn init(filename: &str, pattern: String, time_format: String) -> Self {
|
||||||
|
if filename.ends_with('/') {
|
||||||
|
Format::Dir {
|
||||||
|
pattern,
|
||||||
|
time_format,
|
||||||
|
}
|
||||||
|
} else if filename.ends_with(".gmi") | filename.ends_with(".gemtext") {
|
||||||
|
Format::Gemtext {
|
||||||
|
pattern,
|
||||||
|
time_format,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Format::Plain {
|
||||||
|
pattern,
|
||||||
|
time_format,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_string(
|
||||||
|
&self,
|
||||||
|
updated: Option<DateTime<Utc>>,
|
||||||
|
content: String,
|
||||||
|
link: String,
|
||||||
|
tags: Option<Vec<String>>,
|
||||||
|
attachments: Option<Vec<(String, Option<String>)>>,
|
||||||
|
) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Dir {
|
||||||
|
pattern,
|
||||||
|
time_format,
|
||||||
|
}
|
||||||
|
| Self::Gemtext {
|
||||||
|
pattern,
|
||||||
|
time_format,
|
||||||
|
}
|
||||||
|
| Self::Plain {
|
||||||
|
pattern,
|
||||||
|
time_format,
|
||||||
|
} => pattern
|
||||||
|
.replace(
|
||||||
|
"{content}",
|
||||||
|
&if pattern.contains("{tags}") {
|
||||||
|
content.replace("#", "")
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"{attachments}",
|
||||||
|
&attachments
|
||||||
|
.map(|a| {
|
||||||
|
let mut b = Vec::with_capacity(a.len());
|
||||||
|
b.push("\n".to_string());
|
||||||
|
for (link, alt) in a {
|
||||||
|
let mut t = Vec::with_capacity(2);
|
||||||
|
t.push(if matches!(self, Self::Dir { .. }) {
|
||||||
|
format!("=> ../{link}")
|
||||||
|
} else {
|
||||||
|
format!("=> {link}")
|
||||||
|
});
|
||||||
|
if let Some(text) = alt {
|
||||||
|
t.push(text)
|
||||||
|
}
|
||||||
|
b.push(t.join(" "))
|
||||||
|
}
|
||||||
|
b.join("\n")
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"{tags}",
|
||||||
|
&tags
|
||||||
|
.map(|t| format!("\n\n{}", t.join(", ")))
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
.replace("{link}", &format!("\n\n=> {link}"))
|
||||||
|
.replace(
|
||||||
|
"{updated}",
|
||||||
|
&updated
|
||||||
|
.map(|t| format!("\n\n✏ {}", t.format(time_format)))
|
||||||
|
.unwrap_or_default(),
|
||||||
|
),
|
||||||
|
} // @TODO implement separated templates
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue