diff --git a/Cargo.lock b/Cargo.lock index 2bff233..96e2072 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,7 +131,7 @@ checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" dependencies = [ "errno-dragonfly", "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -160,6 +160,18 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +[[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 0.52.0", +] + [[package]] name = "futures" version = "0.3.28" @@ -407,6 +419,7 @@ name = "obsidian-export" version = "23.12.0" dependencies = [ "eyre", + "filetime", "gumdrop", "ignore", "lazy_static", @@ -618,7 +631,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -754,7 +767,7 @@ dependencies = [ "fastrand", "redox_syscall", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -915,7 +928,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -924,13 +946,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -939,42 +976,84 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "yansi" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 50cebe0..92cc830 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ serde_yaml = "0.9.27" slug = "0.1.5" snafu = "0.7.5" unicode-normalization = "0.1.22" +filetime = "0.2.23" [dev-dependencies] pretty_assertions = "1.4.0" diff --git a/src/lib.rs b/src/lib.rs index bb3f7e5..ecd0a27 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ pub use context::Context; pub use frontmatter::{Frontmatter, FrontmatterStrategy}; pub use walker::{vault_contents, WalkOptions}; +use filetime::set_file_mtime; use frontmatter::{frontmatter_from_str, frontmatter_to_str}; use pathdiff::diff_paths; use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; @@ -165,6 +166,20 @@ pub enum ExportError { 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()))] /// This occurs when an operation is requested on a file or directory which does not exist. PathDoesNotExist { path: PathBuf }, @@ -231,6 +246,7 @@ pub struct Exporter<'a> { vault_contents: Option>, walk_options: WalkOptions<'a>, process_embeds_recursively: bool, + preserve_mtime: bool, postprocessors: Vec<&'a Postprocessor<'a>>, embed_postprocessors: Vec<&'a Postprocessor<'a>>, } @@ -247,6 +263,7 @@ impl<'a> fmt::Debug for Exporter<'a> { "process_embeds_recursively", &self.process_embeds_recursively, ) + .field("preserve_mtime", &self.preserve_mtime) .field( "postprocessors", &format!("<{} postprocessors active>", self.postprocessors.len()), @@ -273,6 +290,7 @@ impl<'a> Exporter<'a> { frontmatter_strategy: FrontmatterStrategy::Auto, walk_options: WalkOptions::default(), process_embeds_recursively: true, + preserve_mtime: false, vault_contents: None, postprocessors: vec![], embed_postprocessors: vec![], @@ -313,6 +331,15 @@ impl<'a> Exporter<'a> { 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 Exporter<'a> { + self.preserve_mtime = preserve; + self + } + /// Append a function to the chain of [postprocessors][Postprocessor] to run on exported Obsidian Markdown notes. pub fn add_postprocessor(&mut self, processor: &'a Postprocessor) -> &mut Exporter<'a> { self.postprocessors.push(processor); @@ -388,11 +415,19 @@ impl<'a> Exporter<'a> { } fn export_note(&self, src: &Path, dest: &Path) -> Result<()> { - match is_markdown_file(src) { + let result = match is_markdown_file(src) { true => self.parse_and_export_obsidian_note(src, dest), false => copy_file(src, dest), } - .context(FileExportSnafu { path: src }) + .context(FileExportSnafu { path: src }); + + if self.preserve_mtime { + if let Ok(()) = result { + copy_mtime(src, dest)?; + } + } + + result } fn parse_and_export_obsidian_note(&self, src: &Path, dest: &Path) -> Result<()> { @@ -762,6 +797,16 @@ fn create_file(dest: &Path) -> Result { 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<()> { std::fs::copy(src, dest) .or_else(|err| { diff --git a/src/main.rs b/src/main.rs index 1798d1b..0d7e9ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -54,6 +54,13 @@ struct Opts { #[options(no_short, help = "Don't process embeds recursively", default = "false")] no_recursive_embeds: bool, + #[options( + no_short, + help = "Preserve the mtime of exported files", + default = "false" + )] + preserve_mtime: bool, + #[options( no_short, help = "Convert soft line breaks to hard line breaks. This mimics Obsidian's 'Strict line breaks' setting", @@ -94,6 +101,7 @@ fn main() { let mut exporter = Exporter::new(root, destination); exporter.frontmatter_strategy(args.frontmatter_strategy); exporter.process_embeds_recursively(!args.no_recursive_embeds); + exporter.preserve_mtime(args.preserve_mtime); exporter.walk_options(walk_options); if args.hard_linebreaks { diff --git a/tests/export_test.rs b/tests/export_test.rs index 237bbd3..45a9c39 100644 --- a/tests/export_test.rs +++ b/tests/export_test.rs @@ -358,6 +358,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] fn test_non_ascii_filenames() { let tmp_dir = TempDir::new().expect("failed to make tempdir");