use libc::{timespec, utimensat, AT_FDCWD, AT_SYMLINK_NOFOLLOW}; use log::{debug, error, trace}; use std::ffi::CString; use std::io::Write; use std::os::unix::ffi::OsStrExt; use std::path::{Component, Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; #[derive(Debug, thiserror::Error)] pub enum UtilError { #[error("failed to canonicalize path {0}")] Canonicalize(PathBuf, #[source] std::io::Error), #[error("failed to convert time to Unix time")] UnixTime(#[source] std::time::SystemTimeError), #[error("failed to create directory {0}")] CreateDir(PathBuf, #[source] std::io::Error), #[error("failed to copy {0} to {1}")] CopyFile(PathBuf, PathBuf, #[source] std::io::Error), /// Failed to create a file. #[error("failed to create file {0}")] CreateFile(PathBuf, #[source] std::io::Error), /// Failed to write to a file. #[error("failed to write to file {0}")] FileWrite(PathBuf, #[source] std::io::Error), #[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), } pub fn canonicalize(path: &Path) -> Result { path.canonicalize() .map_err(|e| UtilError::Canonicalize(path.into(), e)) } pub fn mkdir(path: &Path) -> Result<(), UtilError> { debug!("creating directory {}", path.display()); std::fs::create_dir_all(path).map_err(|e| UtilError::CreateDir(path.into(), e))?; Ok(()) } pub fn copy(src: &Path, dest: &Path) -> Result<(), UtilError> { trace!("copying: {} -> {}", src.display(), dest.display()); std::fs::copy(src, dest).map_err(|e| UtilError::CopyFile(src.into(), dest.into(), e))?; let mtime = get_mtime(src)?; set_mtime(dest, mtime)?; Ok(()) } pub fn write(filename: &Path, content: &str) -> Result<(), UtilError> { if let Some(parent) = filename.parent() { trace!("parent: {}", parent.display()); if !parent.exists() { debug!("creating directory {}", parent.display()); std::fs::create_dir_all(parent).map_err(|e| UtilError::CreateDir(parent.into(), e))?; } } trace!("writing HTML: {}", filename.display()); let mut f = std::fs::File::create(filename).map_err(|e| UtilError::CreateFile(filename.into(), e))?; f.write_all(content.as_bytes()) .map_err(|e| UtilError::FileWrite(filename.into(), e))?; Ok(()) } pub fn get_mtime(src: &Path) -> Result { let metadata = std::fs::metadata(src).map_err(|e| UtilError::FileMetadata(src.into(), e))?; let mtime = metadata .modified() .map_err(|e| UtilError::FileMtime(src.into(), e))?; Ok(mtime) } pub fn set_mtime(filename: &Path, mtime: SystemTime) -> Result<(), UtilError> { trace!( "set_mtime: filename={} mtime={:?}", filename.display(), mtime ); 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(UtilError::Utimensat(pathbuf, error)); } } Ok(()) } pub fn copy_file_from_source(filename: &Path, output: &Path) -> Result<(), UtilError> { debug!("copying {} -> {}", filename.display(), output.display()); if let Some(parent) = output.parent() { trace!("parent: {}", parent.display()); if !parent.exists() { trace!("create parent {}", parent.display()); std::fs::create_dir_all(parent).map_err(|e| UtilError::CreateDir(parent.into(), e))?; } } else { trace!("does not have parent: {}", output.display()); } copy(filename, output)?; Ok(()) } pub fn join_subpath(parent: &Path, sub: &Path) -> PathBuf { let sub: PathBuf = sub .components() .filter(|c| *c != Component::RootDir) .collect(); parent.join(sub) } pub fn make_relative_link>(page: P, target: P) -> PathBuf { let page = page.as_ref(); let target = target.as_ref().to_path_buf(); assert!(page.is_absolute()); assert!(target.is_absolute()); let mut relative = PathBuf::new(); let mut page = page; loop { if let Some(parent) = page.parent() { if let Ok(sub) = target.strip_prefix(parent) { let sub = sub.to_path_buf(); return join_subpath(&relative, &sub); } relative.push(".."); page = parent; } else { return join_subpath(&relative, &target); } } } pub fn make_path_relative_to(dir: &Path, path: &Path) -> PathBuf { path.strip_prefix(dir) .unwrap_or_else(|_| panic!("remove prefix {} from {}", dir.display(), path.display())) .into() } 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(UtilError::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::{ join_subpath, make_path_absolute, make_path_relative_to, make_relative_link, Path, PathBuf, }; #[test] fn joins_relative() { assert_eq!( join_subpath(Path::new("foo"), Path::new("bar")), PathBuf::from("foo/bar") ); } #[test] fn joins_absolute() { assert_eq!( join_subpath(Path::new("foo"), Path::new("/bar")), PathBuf::from("foo/bar") ); } #[test] fn makes_relative_link_to_child() { assert_eq!( make_relative_link("/foo/bar", "/foo/bar/yo"), PathBuf::from("bar/yo") ); } #[test] fn makes_relative_link_to_sibling() { assert_eq!( make_relative_link("/foo/bar", "/foo/yo"), PathBuf::from("yo") ); } #[test] fn makes_relative_link_to_cousin() { assert_eq!( make_relative_link("/foo/bar/yo", "/foo/baz/yoyo"), PathBuf::from("../baz/yoyo") ); } #[test] fn makes_relative_link_to_unrelated_page() { assert_eq!( make_relative_link("/foo/bar", "/yo/yoyo"), PathBuf::from("../yo/yoyo") ); } #[test] fn makes_relative_path() { assert_eq!( make_path_relative_to(Path::new("/foo"), Path::new("/foo/bar/yo.mdwn")), PathBuf::from("bar/yo.mdwn") ); } #[test] fn makes_absolute_path() { assert_eq!( make_path_absolute(Path::new("/foo/bar")), PathBuf::from("/foo/bar") ); } }