Skip to content

Commit

Permalink
Optionally preserve modified time of exported files
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Programmerino authored and zoni committed Aug 4, 2024
1 parent fc7ebd1 commit 21a5756
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 3 deletions.
32 changes: 30 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ serde_yaml = "0.9.34"
slug = "0.1.5"
snafu = "0.8.3"
unicode-normalization = "0.1.23"
filetime = "0.2.23"

[dev-dependencies]
pretty_assertions = "1.4.0"
Expand Down
6 changes: 6 additions & 0 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ skip = [
# result. We can then choose to again skip that version, or decide more
# drastic action is needed.
"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:<=v1.3.2",
]
wildcards = "deny"
allow-wildcard-paths = false
Expand Down
45 changes: 44 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use std::path::{Path, PathBuf};
use std::{fmt, str};

pub use context::Context;
use filetime::set_file_mtime;
use frontmatter::{frontmatter_from_str, frontmatter_to_str};
pub use frontmatter::{Frontmatter, FrontmatterStrategy};
use pathdiff::diff_paths;
Expand Down Expand Up @@ -160,6 +161,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 },
Expand Down Expand Up @@ -227,6 +242,7 @@ pub struct Exporter<'a> {
vault_contents: Option<Vec<PathBuf>>,
walk_options: WalkOptions<'a>,
process_embeds_recursively: bool,
preserve_mtime: bool,
postprocessors: Vec<&'a Postprocessor<'a>>,
embed_postprocessors: Vec<&'a Postprocessor<'a>>,
}
Expand All @@ -243,6 +259,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()),
Expand Down Expand Up @@ -270,6 +287,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![],
Expand Down Expand Up @@ -312,6 +330,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 Self {
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 Self {
Expand Down Expand Up @@ -392,7 +419,13 @@ impl<'a> Exporter<'a> {
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 {
copy_mtime(src, dest).context(FileExportSnafu { path: src })?;
}

Ok(())
}

fn parse_and_export_obsidian_note(&self, src: &Path, dest: &Path) -> Result<()> {
Expand Down Expand Up @@ -766,6 +799,16 @@ fn create_file(dest: &Path) -> Result<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<()> {
fs::copy(src, dest)
.or_else(|err| {
Expand Down
8 changes: 8 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,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",
Expand Down Expand Up @@ -97,6 +104,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 {
Expand Down
38 changes: 38 additions & 0 deletions tests/export_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
fn test_non_ascii_filenames() {
let tmp_dir = TempDir::new().expect("failed to make tempdir");
Expand Down

0 comments on commit 21a5756

Please sign in to comment.