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"
|
||||
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.6.0"
|
||||
@ -120,6 +126,18 @@ version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "futures"
|
||||
version = "0.3.30"
|
||||
@ -358,6 +376,7 @@ name = "obsidian-export"
|
||||
version = "23.12.0"
|
||||
dependencies = [
|
||||
"eyre",
|
||||
"filetime",
|
||||
"gumdrop",
|
||||
"ignore",
|
||||
"matter",
|
||||
@ -441,7 +460,7 @@ version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.6.0",
|
||||
"getopts",
|
||||
"memchr",
|
||||
"unicase",
|
||||
@ -485,6 +504,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "regex"
|
||||
version = "1.10.6"
|
||||
@ -565,7 +593,7 @@ version = "0.38.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.6.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
|
@ -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"
|
||||
|
@ -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:<=1.3.2",
|
||||
]
|
||||
wildcards = "deny"
|
||||
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};
|
||||
|
||||
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;
|
||||
@ -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 },
|
||||
@ -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>>,
|
||||
}
|
||||
@ -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()),
|
||||
@ -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![],
|
||||
@ -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 {
|
||||
@ -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<()> {
|
||||
@ -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| {
|
||||
|
@ -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",
|
||||
@ -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 {
|
||||
|
@ -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");
|
||||
|
Loading…
Reference in New Issue
Block a user