Optionally preserve modified time of exported files
Add a new argument --preserve-mtime to keep the original modified time attribute of notes being exported, instead of setting them to the current time.
This commit is contained in:
parent
fc7ebd111a
commit
564bee1d92
32
Cargo.lock
generated
32
Cargo.lock
generated
@ -17,6 +17,12 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.6.0"
|
version = "2.6.0"
|
||||||
@ -120,6 +126,18 @@ version = "2.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
|
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filetime"
|
||||||
|
version = "0.2.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.30"
|
version = "0.3.30"
|
||||||
@ -358,6 +376,7 @@ name = "obsidian-export"
|
|||||||
version = "23.12.0"
|
version = "23.12.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"eyre",
|
"eyre",
|
||||||
|
"filetime",
|
||||||
"gumdrop",
|
"gumdrop",
|
||||||
"ignore",
|
"ignore",
|
||||||
"matter",
|
"matter",
|
||||||
@ -441,7 +460,7 @@ version = "0.9.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
|
checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.6.0",
|
||||||
"getopts",
|
"getopts",
|
||||||
"memchr",
|
"memchr",
|
||||||
"unicase",
|
"unicase",
|
||||||
@ -485,6 +504,15 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.10.6"
|
version = "1.10.6"
|
||||||
@ -565,7 +593,7 @@ version = "0.38.34"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
|
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.6.0",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
|
@ -39,6 +39,7 @@ serde_yaml = "0.9.34"
|
|||||||
slug = "0.1.5"
|
slug = "0.1.5"
|
||||||
snafu = "0.8.3"
|
snafu = "0.8.3"
|
||||||
unicode-normalization = "0.1.23"
|
unicode-normalization = "0.1.23"
|
||||||
|
filetime = "0.2.23"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = "1.4.0"
|
pretty_assertions = "1.4.0"
|
||||||
|
@ -62,6 +62,12 @@ skip = [
|
|||||||
# result. We can then choose to again skip that version, or decide more
|
# result. We can then choose to again skip that version, or decide more
|
||||||
# drastic action is needed.
|
# drastic action is needed.
|
||||||
"syn:<=1.0.109",
|
"syn:<=1.0.109",
|
||||||
|
# filetime depends on redox_syscall which depends on bitflags 1.x, whereas
|
||||||
|
# other dependencies in our tree depends on bitflags 2.x. This should solve
|
||||||
|
# itself when a new release is made for filetime, as redox_syscall is
|
||||||
|
# deprecated and already replaced by libredox anyway
|
||||||
|
# (https://github.com/alexcrichton/filetime/pull/103)
|
||||||
|
"bitflags:<=1.3.2",
|
||||||
]
|
]
|
||||||
wildcards = "deny"
|
wildcards = "deny"
|
||||||
allow-wildcard-paths = false
|
allow-wildcard-paths = false
|
||||||
|
45
src/lib.rs
45
src/lib.rs
@ -14,6 +14,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::{fmt, str};
|
use std::{fmt, str};
|
||||||
|
|
||||||
pub use context::Context;
|
pub use context::Context;
|
||||||
|
use filetime::set_file_mtime;
|
||||||
use frontmatter::{frontmatter_from_str, frontmatter_to_str};
|
use frontmatter::{frontmatter_from_str, frontmatter_to_str};
|
||||||
pub use frontmatter::{Frontmatter, FrontmatterStrategy};
|
pub use frontmatter::{Frontmatter, FrontmatterStrategy};
|
||||||
use pathdiff::diff_paths;
|
use pathdiff::diff_paths;
|
||||||
@ -160,6 +161,20 @@ pub enum ExportError {
|
|||||||
source: ignore::Error,
|
source: ignore::Error,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[snafu(display("Failed to read the mtime of '{}'", path.display()))]
|
||||||
|
/// This occurs when a file's modified time cannot be read
|
||||||
|
ModTimeReadError {
|
||||||
|
path: PathBuf,
|
||||||
|
source: std::io::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[snafu(display("Failed to set the mtime of '{}'", path.display()))]
|
||||||
|
/// This occurs when a file's modified time cannot be set
|
||||||
|
ModTimeSetError {
|
||||||
|
path: PathBuf,
|
||||||
|
source: std::io::Error,
|
||||||
|
},
|
||||||
|
|
||||||
#[snafu(display("No such file or directory: {}", path.display()))]
|
#[snafu(display("No such file or directory: {}", path.display()))]
|
||||||
/// This occurs when an operation is requested on a file or directory which does not exist.
|
/// This occurs when an operation is requested on a file or directory which does not exist.
|
||||||
PathDoesNotExist { path: PathBuf },
|
PathDoesNotExist { path: PathBuf },
|
||||||
@ -227,6 +242,7 @@ pub struct Exporter<'a> {
|
|||||||
vault_contents: Option<Vec<PathBuf>>,
|
vault_contents: Option<Vec<PathBuf>>,
|
||||||
walk_options: WalkOptions<'a>,
|
walk_options: WalkOptions<'a>,
|
||||||
process_embeds_recursively: bool,
|
process_embeds_recursively: bool,
|
||||||
|
preserve_mtime: bool,
|
||||||
postprocessors: Vec<&'a Postprocessor<'a>>,
|
postprocessors: Vec<&'a Postprocessor<'a>>,
|
||||||
embed_postprocessors: Vec<&'a Postprocessor<'a>>,
|
embed_postprocessors: Vec<&'a Postprocessor<'a>>,
|
||||||
}
|
}
|
||||||
@ -243,6 +259,7 @@ impl<'a> fmt::Debug for Exporter<'a> {
|
|||||||
"process_embeds_recursively",
|
"process_embeds_recursively",
|
||||||
&self.process_embeds_recursively,
|
&self.process_embeds_recursively,
|
||||||
)
|
)
|
||||||
|
.field("preserve_mtime", &self.preserve_mtime)
|
||||||
.field(
|
.field(
|
||||||
"postprocessors",
|
"postprocessors",
|
||||||
&format!("<{} postprocessors active>", self.postprocessors.len()),
|
&format!("<{} postprocessors active>", self.postprocessors.len()),
|
||||||
@ -270,6 +287,7 @@ impl<'a> Exporter<'a> {
|
|||||||
frontmatter_strategy: FrontmatterStrategy::Auto,
|
frontmatter_strategy: FrontmatterStrategy::Auto,
|
||||||
walk_options: WalkOptions::default(),
|
walk_options: WalkOptions::default(),
|
||||||
process_embeds_recursively: true,
|
process_embeds_recursively: true,
|
||||||
|
preserve_mtime: false,
|
||||||
vault_contents: None,
|
vault_contents: None,
|
||||||
postprocessors: vec![],
|
postprocessors: vec![],
|
||||||
embed_postprocessors: vec![],
|
embed_postprocessors: vec![],
|
||||||
@ -312,6 +330,15 @@ impl<'a> Exporter<'a> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set whether the modified time of exported files should be preserved.
|
||||||
|
///
|
||||||
|
/// When `preserve` is true, the modified time of exported files will be set to the modified
|
||||||
|
/// time of the source file.
|
||||||
|
pub fn preserve_mtime(&mut self, preserve: bool) -> &mut Self {
|
||||||
|
self.preserve_mtime = preserve;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Append a function to the chain of [postprocessors][Postprocessor] to run on exported
|
/// Append a function to the chain of [postprocessors][Postprocessor] to run on exported
|
||||||
/// Obsidian Markdown notes.
|
/// Obsidian Markdown notes.
|
||||||
pub fn add_postprocessor(&mut self, processor: &'a Postprocessor<'_>) -> &mut Self {
|
pub fn add_postprocessor(&mut self, processor: &'a Postprocessor<'_>) -> &mut Self {
|
||||||
@ -392,7 +419,13 @@ impl<'a> Exporter<'a> {
|
|||||||
true => self.parse_and_export_obsidian_note(src, dest),
|
true => self.parse_and_export_obsidian_note(src, dest),
|
||||||
false => copy_file(src, dest),
|
false => copy_file(src, dest),
|
||||||
}
|
}
|
||||||
.context(FileExportSnafu { path: src })
|
.context(FileExportSnafu { path: src })?;
|
||||||
|
|
||||||
|
if self.preserve_mtime {
|
||||||
|
copy_mtime(src, dest).context(FileExportSnafu { path: src })?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_and_export_obsidian_note(&self, src: &Path, dest: &Path) -> Result<()> {
|
fn parse_and_export_obsidian_note(&self, src: &Path, dest: &Path) -> Result<()> {
|
||||||
@ -766,6 +799,16 @@ fn create_file(dest: &Path) -> Result<File> {
|
|||||||
Ok(file)
|
Ok(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn copy_mtime(src: &Path, dest: &Path) -> Result<()> {
|
||||||
|
let metadata = fs::metadata(src).context(ModTimeReadSnafu { path: src })?;
|
||||||
|
let modified_time = metadata
|
||||||
|
.modified()
|
||||||
|
.context(ModTimeReadSnafu { path: src })?;
|
||||||
|
|
||||||
|
set_file_mtime(dest, modified_time.into()).context(ModTimeSetSnafu { path: dest })?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn copy_file(src: &Path, dest: &Path) -> Result<()> {
|
fn copy_file(src: &Path, dest: &Path) -> Result<()> {
|
||||||
fs::copy(src, dest)
|
fs::copy(src, dest)
|
||||||
.or_else(|err| {
|
.or_else(|err| {
|
||||||
|
@ -57,6 +57,13 @@ struct Opts {
|
|||||||
#[options(no_short, help = "Don't process embeds recursively", default = "false")]
|
#[options(no_short, help = "Don't process embeds recursively", default = "false")]
|
||||||
no_recursive_embeds: bool,
|
no_recursive_embeds: bool,
|
||||||
|
|
||||||
|
#[options(
|
||||||
|
no_short,
|
||||||
|
help = "Preserve the mtime of exported files",
|
||||||
|
default = "false"
|
||||||
|
)]
|
||||||
|
preserve_mtime: bool,
|
||||||
|
|
||||||
#[options(
|
#[options(
|
||||||
no_short,
|
no_short,
|
||||||
help = "Convert soft line breaks to hard line breaks. This mimics Obsidian's 'Strict line breaks' setting",
|
help = "Convert soft line breaks to hard line breaks. This mimics Obsidian's 'Strict line breaks' setting",
|
||||||
@ -97,6 +104,7 @@ fn main() {
|
|||||||
let mut exporter = Exporter::new(root, destination);
|
let mut exporter = Exporter::new(root, destination);
|
||||||
exporter.frontmatter_strategy(args.frontmatter_strategy);
|
exporter.frontmatter_strategy(args.frontmatter_strategy);
|
||||||
exporter.process_embeds_recursively(!args.no_recursive_embeds);
|
exporter.process_embeds_recursively(!args.no_recursive_embeds);
|
||||||
|
exporter.preserve_mtime(args.preserve_mtime);
|
||||||
exporter.walk_options(walk_options);
|
exporter.walk_options(walk_options);
|
||||||
|
|
||||||
if args.hard_linebreaks {
|
if args.hard_linebreaks {
|
||||||
|
@ -360,6 +360,44 @@ fn test_no_recursive_embeds() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_preserve_mtime() {
|
||||||
|
let tmp_dir = TempDir::new().expect("failed to make tempdir");
|
||||||
|
|
||||||
|
let mut exporter = Exporter::new(
|
||||||
|
PathBuf::from("tests/testdata/input/main-samples/"),
|
||||||
|
tmp_dir.path().to_path_buf(),
|
||||||
|
);
|
||||||
|
exporter.preserve_mtime(true);
|
||||||
|
exporter.run().expect("exporter returned error");
|
||||||
|
|
||||||
|
let src = "tests/testdata/input/main-samples/obsidian-wikilinks.md";
|
||||||
|
let dest = tmp_dir.path().join(PathBuf::from("obsidian-wikilinks.md"));
|
||||||
|
let src_meta = std::fs::metadata(src).unwrap();
|
||||||
|
let dest_meta = std::fs::metadata(dest).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(src_meta.modified().unwrap(), dest_meta.modified().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_preserve_mtime() {
|
||||||
|
let tmp_dir = TempDir::new().expect("failed to make tempdir");
|
||||||
|
|
||||||
|
let mut exporter = Exporter::new(
|
||||||
|
PathBuf::from("tests/testdata/input/main-samples/"),
|
||||||
|
tmp_dir.path().to_path_buf(),
|
||||||
|
);
|
||||||
|
exporter.preserve_mtime(false);
|
||||||
|
exporter.run().expect("exporter returned error");
|
||||||
|
|
||||||
|
let src = "tests/testdata/input/main-samples/obsidian-wikilinks.md";
|
||||||
|
let dest = tmp_dir.path().join(PathBuf::from("obsidian-wikilinks.md"));
|
||||||
|
let src_meta = std::fs::metadata(src).unwrap();
|
||||||
|
let dest_meta = std::fs::metadata(dest).unwrap();
|
||||||
|
|
||||||
|
assert_ne!(src_meta.modified().unwrap(), dest_meta.modified().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_non_ascii_filenames() {
|
fn test_non_ascii_filenames() {
|
||||||
let tmp_dir = TempDir::new().expect("failed to make tempdir");
|
let tmp_dir = TempDir::new().expect("failed to make tempdir");
|
||||||
|
Loading…
Reference in New Issue
Block a user