diff --git a/src/bins.rs b/src/bins.rs index 76dfc9fc..d08cf15a 100644 --- a/src/bins.rs +++ b/src/bins.rs @@ -4,7 +4,7 @@ use cargo_toml::Product; use log::debug; use serde::Serialize; -use crate::{BinstallError, PkgFmt, PkgMeta, Template}; +use crate::{atomic_install, atomic_symlink_file, BinstallError, PkgFmt, PkgMeta, Template}; pub struct BinFile { pub base_name: String, @@ -80,18 +80,11 @@ impl BinFile { pub fn install_bin(&self) -> Result<(), BinstallError> { // TODO: check if file already exists debug!( - "Copy file from '{}' to '{}'", + "Atomically install file from '{}' to '{}'", self.source.display(), self.dest.display() ); - std::fs::copy(&self.source, &self.dest)?; - - #[cfg(target_family = "unix")] - { - use std::os::unix::fs::PermissionsExt; - debug!("Set permissions 755 on '{}'", self.dest.display()); - std::fs::set_permissions(&self.dest, std::fs::Permissions::from_mode(0o755))?; - } + atomic_install(&self.source, &self.dest)?; Ok(()) } @@ -110,21 +103,15 @@ impl BinFile { self.link.display(), dest.display() ); - #[cfg(target_family = "unix")] - std::os::unix::fs::symlink(dest, &self.link)?; - #[cfg(target_family = "windows")] - std::os::windows::fs::symlink_file(dest, &self.link)?; + atomic_symlink_file(dest, &self.link)?; Ok(()) } fn link_dest(&self) -> &Path { - #[cfg(target_family = "unix")] - { + if cfg!(target_family = "unix") { Path::new(self.dest.file_name().unwrap()) - } - #[cfg(target_family = "windows")] - { + } else { &self.dest } } diff --git a/src/helpers.rs b/src/helpers.rs index 4be08709..d48e97b6 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,4 +1,6 @@ use std::fmt::Debug; +use std::fs; +use std::io; use std::path::{Path, PathBuf}; use bytes::Bytes; @@ -8,6 +10,7 @@ use log::debug; use once_cell::sync::OnceCell; use reqwest::{tls, Client, ClientBuilder, Method, Response}; use serde::Serialize; +use tempfile::NamedTempFile; use tinytemplate::TinyTemplate; use tokio::task::block_in_place; use url::Url; @@ -188,6 +191,91 @@ pub fn get_install_path>(install_path: Option

) -> Option io::Result<()> { + debug!( + "Attempting to atomically rename from '{}' to '{}'", + src.display(), + dst.display() + ); + + if fs::rename(src, dst).is_err() { + debug!("Attempting at atomically failed, fallback to creating tempfile."); + // src and dst is not on the same filesystem/mountpoint. + // Fallback to creating NamedTempFile on the parent dir of + // dst. + + let mut src_file = fs::File::open(src)?; + + let parent = dst.parent().unwrap(); + debug!("Creating named tempfile at '{}'", parent.display()); + let mut tempfile = NamedTempFile::new_in(parent)?; + + debug!( + "Copying from '{}' to '{}'", + src.display(), + tempfile.path().display() + ); + io::copy(&mut src_file, tempfile.as_file_mut())?; + + debug!("Retrieving permissions of '{}'", src.display()); + let permissions = src_file.metadata()?.permissions(); + + debug!( + "Setting permissions of '{}' to '{permissions:#?}'", + tempfile.path().display() + ); + tempfile.as_file().set_permissions(permissions)?; + + debug!( + "Persisting '{}' to '{}'", + tempfile.path().display(), + dst.display() + ); + tempfile.persist(dst).map_err(io::Error::from)?; + } else { + debug!("Attempting at atomically succeeded."); + } + + Ok(()) +} + +fn symlink_file, Q: AsRef>(original: P, link: Q) -> io::Result<()> { + #[cfg(target_family = "unix")] + let f = std::os::unix::fs::symlink; + #[cfg(target_family = "windows")] + let f = std::os::windows::fs::symlink_file; + + f(original, link) +} + +/// Atomically install symlink "link" to a file "dst". +/// +/// This is a blocking function, must be called in `block_in_place` mode. +pub fn atomic_symlink_file(dest: &Path, link: &Path) -> io::Result<()> { + let parent = link.parent().unwrap(); + + debug!("Creating tempPath at '{}'", parent.display()); + let temp_path = NamedTempFile::new_in(parent)?.into_temp_path(); + fs::remove_file(&temp_path)?; + + debug!( + "Creating symlink '{}' to file '{}'", + temp_path.display(), + dest.display() + ); + symlink_file(dest, &temp_path)?; + + debug!( + "Persisting '{}' to '{}'", + temp_path.display(), + link.display() + ); + temp_path.persist(link).map_err(io::Error::from) +} + pub trait Template: Serialize { fn render(&self, template: &str) -> Result where