use super::metadata_io_failure;
use super::MetadataIoOperation;
use crate::RuntimeResult;
#[cfg(unix)]
use std::fs::File;
use std::fs::OpenOptions;
use std::io::Write;
#[cfg(windows)]
use std::iter;
#[cfg(windows)]
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use uuid::Uuid;
pub(super) fn persist_atomic_local(target: &Path, bytes: &[u8]) -> RuntimeResult<()> {
let parent = target
.parent()
.filter(|path| !path.as_os_str().is_empty())
.unwrap_or(Path::new("."));
std::fs::create_dir_all(parent)
.map_err(|source| metadata_io_failure(MetadataIoOperation::CreateParent, parent, source))?;
let file_name = target.file_name().and_then(|name| name.to_str()).unwrap_or("metadata");
let temporary = parent.join(format!(".{file_name}.{}.tmp", Uuid::new_v4()));
let result = persist_temporary_and_replace(&temporary, target, parent, bytes);
if result.is_err() {
if let Err(source) = std::fs::remove_file(&temporary) {
if source.kind() != std::io::ErrorKind::NotFound {
tracing::warn!(
path = %temporary.display(),
error = %source,
"failed to remove metadata temporary file after persistence failure"
);
}
}
}
result
}
fn persist_temporary_and_replace(temporary: &Path, target: &Path, parent: &Path, bytes: &[u8]) -> RuntimeResult<()> {
let mut file = OpenOptions::new()
.create_new(true)
.write(true)
.open(temporary)
.map_err(|source| metadata_io_failure(MetadataIoOperation::CreateTemporary, temporary, source))?;
file.write_all(bytes)
.map_err(|source| metadata_io_failure(MetadataIoOperation::WriteTemporary, temporary, source))?;
file.sync_all()
.map_err(|source| metadata_io_failure(MetadataIoOperation::SyncTemporary, temporary, source))?;
drop(file);
replace_file(temporary, target)
.map_err(|source| metadata_io_failure(MetadataIoOperation::ReplaceTarget, target, source))?;
sync_directory(parent).map_err(|source| metadata_io_failure(MetadataIoOperation::SyncParent, parent, source))
}
#[cfg(unix)]
fn sync_directory(path: &Path) -> std::io::Result<()> {
File::open(path)?.sync_all()
}
#[cfg(windows)]
fn sync_directory(path: &Path) -> std::io::Result<()> {
use std::os::windows::fs::OpenOptionsExt;
let directory = OpenOptions::new()
.read(true)
.write(true)
.share_mode(
windows_sys::Win32::Storage::FileSystem::FILE_SHARE_READ
| windows_sys::Win32::Storage::FileSystem::FILE_SHARE_WRITE
| windows_sys::Win32::Storage::FileSystem::FILE_SHARE_DELETE,
)
.custom_flags(windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS)
.open(path)?;
directory.sync_all()
}
#[cfg(not(any(unix, windows)))]
fn sync_directory(_path: &Path) -> std::io::Result<()> {
Ok(())
}
#[cfg(not(windows))]
fn replace_file(source: &Path, destination: &Path) -> std::io::Result<()> {
std::fs::rename(source, destination)
}
#[cfg(windows)]
fn replace_file(source: &Path, destination: &Path) -> std::io::Result<()> {
if !destination.exists() {
let source = wide_path(source);
let destination = wide_path(destination);
let moved = unsafe {
windows_sys::Win32::Storage::FileSystem::MoveFileExW(
source.as_ptr(),
destination.as_ptr(),
windows_sys::Win32::Storage::FileSystem::MOVEFILE_WRITE_THROUGH,
)
};
return if moved == 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(())
};
}
let source = wide_path(source);
let destination = wide_path(destination);
let replaced = unsafe {
windows_sys::Win32::Storage::FileSystem::ReplaceFileW(
destination.as_ptr(),
source.as_ptr(),
std::ptr::null(),
windows_sys::Win32::Storage::FileSystem::REPLACEFILE_WRITE_THROUGH,
std::ptr::null(),
std::ptr::null(),
)
};
if replaced == 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(())
}
}
#[cfg(windows)]
fn wide_path(path: &Path) -> Vec<u16> {
path.as_os_str().encode_wide().chain(iter::once(0)).collect()
}