diff options
author | Lars Wirzenius <liw@liw.fi> | 2022-08-03 04:56:28 +0000 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2022-08-03 04:56:28 +0000 |
commit | 3b951869505b7f1515b9c4b78f19455a235246bd (patch) | |
tree | e46e55b6a97aed6931e01da4695512735006d532 | |
parent | 65c453b9b8499eb4a273bfd6f013608b9af23844 (diff) | |
parent | 6b39def739b0f5174d6c842bd798df179541db60 (diff) | |
download | riki-3b951869505b7f1515b9c4b78f19455a235246bd.tar.gz |
Merge branch 'mtime' into 'main'
feat: set output file modification times
See merge request larswirzenius/riki!30
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | riki.md | 21 | ||||
-rw-r--r-- | src/bin/riki.rs | 3 | ||||
-rw-r--r-- | src/error.rs | 12 | ||||
-rw-r--r-- | src/page.rs | 36 | ||||
-rw-r--r-- | src/site.rs | 11 | ||||
-rw-r--r-- | src/util.rs | 51 |
7 files changed, 125 insertions, 10 deletions
@@ -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" @@ -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<String>, path: PathBuf, + mtime: SystemTime, } impl PageMeta { - fn new(name: String, title: Option<String>, path: PathBuf) -> Self { + fn new(name: String, title: Option<String>, 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<String>, path: Option<PathBuf>, + mtime: Option<SystemTime>, } 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<PathBuf, SiteError> { 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<SystemTime, SiteError> { + 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<timespec, SiteError> { + 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::{ |