From 6b39def739b0f5174d6c842bd798df179541db60 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Mon, 1 Aug 2022 07:26:37 +0300 Subject: feat: set output file modification times Sponsored-by: author --- Cargo.toml | 1 + riki.md | 21 +++++++++++++++++++++ src/bin/riki.rs | 3 ++- src/error.rs | 12 ++++++++++++ src/page.rs | 36 ++++++++++++++++++++++++++++++------ src/site.rs | 11 +++++++++-- src/util.rs | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 7 files changed, 125 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5346b03..b6a610b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ license = "GPL-3.0-or-later" anyhow = "1.0.52" clap = { version = "3.2.10", features = ["derive"] } html-escape = "0.2.11" +libc = "0.2.126" log = "0.4.17" pretty_env_logger = "0.4.0" pulldown-cmark = "0.9.0" diff --git a/riki.md b/riki.md index e208baa..7adb3ff 100644 --- a/riki.md +++ b/riki.md @@ -529,3 +529,24 @@ then stdout doesn't contain ".git" then stdout doesn't contain "index.mdwn~" then stdout doesn't contain "#index.mdwn#" ~~~ + +## Output directory tree + +### Output files have source file modification times + +_Requirement: Files in the output directory have the same time stamp +as the corresponding files in the source directory._ + +Note that due to limitations in the Subplot `lib/files` library, our +check for modification times is imprecise. + +~~~scenario +given an installed riki +given file site/index.mdwn from empty +given file site/index.mdwn has modification time 1970-01-01 00:00:00 +given file site/index.jpg from empty +given file site/index.jpg has modification time 1970-01-01 00:00:00 +when I run riki build site output +then file output/index.html has a very old modification time +then file output/index.jpg has a very old modification time +~~~ diff --git a/src/bin/riki.rs b/src/bin/riki.rs index afd43ef..6a9744e 100644 --- a/src/bin/riki.rs +++ b/src/bin/riki.rs @@ -2,7 +2,7 @@ use clap::Parser; use log::{debug, error, info}; use riki::error::SiteError; use riki::site::Site; -use riki::util::{canonicalize, copy_file_from_source, mkdir}; +use riki::util::{canonicalize, copy_file_from_source, mkdir, set_mtime}; use std::error::Error; use std::path::PathBuf; @@ -83,6 +83,7 @@ impl Build { let output = page.meta().destination_filename(&destdir); debug!("writing: {}", output.display()); htmlpage.write(&output)?; + set_mtime(&output, page.meta().mtime())?; } for file in site.files() { diff --git a/src/error.rs b/src/error.rs index 756d7f0..aee8252 100644 --- a/src/error.rs +++ b/src/error.rs @@ -52,4 +52,16 @@ pub enum SiteError { #[error("attempt to use definition lists in Markdown: {0:?}")] DefinitionList(String), + + #[error("failed to get file metadata: {0}")] + FileMetadata(PathBuf, #[source] std::io::Error), + + #[error("failed to get file modification time: {0}")] + FileMtime(PathBuf, #[source] std::io::Error), + + #[error("failed to set modification time for file {0}")] + Utimensat(PathBuf, #[source] std::io::Error), + + #[error("failed to convert time to Unix time")] + UnixTime(#[source] std::time::SystemTimeError), } diff --git a/src/page.rs b/src/page.rs index d2f144a..41a8584 100644 --- a/src/page.rs +++ b/src/page.rs @@ -2,10 +2,11 @@ use crate::error::SiteError; use crate::html::{parse, Content, Element, ElementTag, HtmlPage}; use crate::parser::WikitextParser; use crate::site::Site; -use crate::util::{join_subpath, make_path_relative_to}; +use crate::util::{get_mtime, join_subpath, make_path_relative_to}; use crate::wikitext::Snippet; use log::{info, trace}; use std::path::{Path, PathBuf}; +use std::time::SystemTime; #[derive(Debug, Eq, PartialEq)] pub struct WikitextPage { @@ -29,7 +30,12 @@ impl WikitextPage { .to_string(); let data = std::fs::read(filename).map_err(|e| SiteError::FileRead(filename.into(), e))?; let wikitext = String::from_utf8(data).map_err(|e| SiteError::Utf8(filename.into(), e))?; - let meta = MetaBuilder::default().name(name).path(absolute).build(); + let mtime = get_mtime(filename)?; + let meta = MetaBuilder::default() + .name(name) + .path(absolute) + .mtime(mtime) + .build(); Ok(Self::new(meta, wikitext)) } @@ -122,17 +128,24 @@ pub struct PageMeta { name: String, title: Option, path: PathBuf, + mtime: SystemTime, } impl PageMeta { - fn new(name: String, title: Option, path: PathBuf) -> Self { + fn new(name: String, title: Option, path: PathBuf, mtime: SystemTime) -> Self { trace!( - "PageMeta: name={:?} title={:?} path={:?}", + "PageMeta: name={:?} title={:?} path={:?} mtime={:?}", name, title, - path + path, + mtime, ); - Self { name, title, path } + Self { + name, + title, + path, + mtime, + } } pub fn destination_filename(&self, destdir: &Path) -> PathBuf { @@ -159,6 +172,10 @@ impl PageMeta { pub fn path(&self) -> &Path { &self.path } + + pub fn mtime(&self) -> SystemTime { + self.mtime + } } #[derive(Debug, Default)] @@ -166,6 +183,7 @@ pub struct MetaBuilder { name: String, title: Option, path: Option, + mtime: Option, } impl MetaBuilder { @@ -174,6 +192,7 @@ impl MetaBuilder { self.name, self.title, self.path.expect("path set on MetaBuilder"), + self.mtime.expect("mtime set on MetaBuilder"), ) } @@ -191,4 +210,9 @@ impl MetaBuilder { self.path = Some(path); self } + + pub fn mtime(mut self, mtime: SystemTime) -> Self { + self.mtime = Some(mtime); + self + } } diff --git a/src/site.rs b/src/site.rs index d48d94c..fd28a2b 100644 --- a/src/site.rs +++ b/src/site.rs @@ -258,10 +258,17 @@ impl PageSet { mod test { use super::{Site, SiteError, WikitextPage}; use crate::page::MetaBuilder; - use std::path::{Path, PathBuf}; + use std::{ + path::{Path, PathBuf}, + time::SystemTime, + }; fn page(path: &str) -> WikitextPage { - let meta = MetaBuilder::default().path(PathBuf::from(path)).build(); + let mtime = SystemTime::now(); + let meta = MetaBuilder::default() + .path(PathBuf::from(path)) + .mtime(mtime) + .build(); WikitextPage::new(meta, "".into()) } diff --git a/src/util.rs b/src/util.rs index e1f82b3..1f69523 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,6 +1,10 @@ use crate::error::SiteError; -use log::{debug, trace}; +use libc::{timespec, utimensat, AT_FDCWD, AT_SYMLINK_NOFOLLOW}; +use log::{debug, error, trace}; +use std::ffi::CString; +use std::os::unix::ffi::OsStrExt; use std::path::{Component, Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; pub fn canonicalize(path: &Path) -> Result { path.canonicalize() @@ -16,6 +20,36 @@ pub fn mkdir(path: &Path) -> Result<(), SiteError> { pub fn copy(src: &Path, dest: &Path) -> Result<(), SiteError> { trace!("copying: {} -> {}", src.display(), dest.display()); std::fs::copy(src, dest).map_err(|e| SiteError::CopyFile(src.into(), dest.into(), e))?; + let mtime = get_mtime(src)?; + set_mtime(dest, mtime)?; + Ok(()) +} + +pub fn get_mtime(src: &Path) -> Result { + let metadata = std::fs::metadata(src).map_err(|e| SiteError::FileMetadata(src.into(), e))?; + let mtime = metadata + .modified() + .map_err(|e| SiteError::FileMtime(src.into(), e))?; + Ok(mtime) +} + +pub fn set_mtime(filename: &Path, mtime: SystemTime) -> Result<(), SiteError> { + let mtime = timespec(mtime)?; + let times = [mtime, mtime]; + let times: *const timespec = ×[0]; + + let pathbuf = filename.to_path_buf(); + let path = path_to_cstring(filename); + + // We have to use unsafe here to be able call the libc functions + // below. + unsafe { + if utimensat(AT_FDCWD, path.as_ptr(), times, AT_SYMLINK_NOFOLLOW) == -1 { + let error = std::io::Error::last_os_error(); + error!("utimensat failed on {:?}", path); + return Err(SiteError::Utimensat(pathbuf, error)); + } + } Ok(()) } @@ -76,6 +110,21 @@ pub fn make_path_absolute(path: &Path) -> PathBuf { Path::new("/").join(&path) } +fn timespec(time: SystemTime) -> Result { + let dur = time + .duration_since(UNIX_EPOCH) + .map_err(SiteError::UnixTime)?; + let tv_sec = dur.as_secs() as libc::time_t; + let tv_nsec = dur.subsec_nanos() as libc::c_long; + Ok(timespec { tv_sec, tv_nsec }) +} + +fn path_to_cstring(path: &Path) -> CString { + let path = path.as_os_str(); + let path = path.as_bytes(); + CString::new(path).unwrap() +} + #[cfg(test)] mod test { use super::{ -- cgit v1.2.1