use crate::error_report::ErrorReportHandle;
use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::SystemTime;
static METADATA_WARNING_COUNT: AtomicUsize = AtomicUsize::new(0);
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[derive(Debug, Clone)]
pub struct CopyFlags {
pub data: bool, pub attributes: bool, pub timestamps: bool, pub security: bool, pub owner: bool, pub auditing: bool, }
impl Default for CopyFlags {
fn default() -> Self {
Self::from_string("DAT")
}
}
impl CopyFlags {
pub fn from_string(flags: &str) -> Self {
let flags_upper = flags.to_uppercase();
Self {
data: flags_upper.contains('D'),
attributes: flags_upper.contains('A'),
timestamps: flags_upper.contains('T'),
security: flags_upper.contains('S'),
owner: flags_upper.contains('O'),
auditing: false,
}
}
pub fn all() -> Self {
Self::from_string("DATSOU")
}
}
pub fn copy_file_with_metadata(
source: &Path,
destination: &Path,
flags: &CopyFlags,
) -> Result<u64> {
copy_file_with_metadata_and_reporter(source, destination, flags, None)
}
pub fn copy_file_with_metadata_and_reporter(
source: &Path,
destination: &Path,
flags: &CopyFlags,
error_reporter: Option<&ErrorReportHandle>,
) -> Result<u64> {
let source_metadata = fs::symlink_metadata(source)
.with_context(|| format!("Failed to read source metadata: {}", source.display()))?;
if source_metadata.is_symlink() {
return copy_symlink_with_metadata(source, destination, flags);
}
let bytes_copied = if flags.data {
streaming_copy_optimized(source, destination)?
} else {
return Err(anyhow::anyhow!(
"Data flag (D) must be set for file copying"
));
};
let metadata = fs::metadata(source)
.with_context(|| format!("Failed to read source metadata: {}", source.display()))?;
if flags.timestamps {
if let Err(e) = copy_timestamps(source, destination, &metadata) {
if error_reporter.is_none() {
METADATA_WARNING_COUNT.fetch_add(1, Ordering::Relaxed);
} else {
let msg = format!("Failed to preserve timestamps: {e}");
if let Some(reporter) = error_reporter {
reporter.add_warning(destination, &msg);
}
}
}
}
if flags.security {
if let Err(e) = copy_permissions(source, destination, &metadata) {
if error_reporter.is_none() {
METADATA_WARNING_COUNT.fetch_add(1, Ordering::Relaxed);
} else {
let msg = format!("Failed to preserve permissions: {e}");
if let Some(reporter) = error_reporter {
reporter.add_warning(destination, &msg);
}
}
}
}
if flags.attributes {
if let Err(e) = copy_attributes(source, destination, &metadata) {
if error_reporter.is_none() {
METADATA_WARNING_COUNT.fetch_add(1, Ordering::Relaxed);
} else {
let msg = format!("Failed to preserve attributes: {e}");
if let Some(reporter) = error_reporter {
reporter.add_warning(destination, &msg);
}
}
}
}
#[cfg(unix)]
if flags.owner {
if let Err(e) = copy_ownership(source, destination, &metadata) {
if error_reporter.is_none() {
METADATA_WARNING_COUNT.fetch_add(1, Ordering::Relaxed);
} else {
let msg =
format!("Failed to preserve ownership: {e} (requires appropriate privileges)");
if let Some(reporter) = error_reporter {
reporter.add_warning(destination, &msg);
}
}
}
}
Ok(bytes_copied)
}
pub fn get_and_reset_metadata_warning_count() -> usize {
METADATA_WARNING_COUNT.swap(0, Ordering::Relaxed)
}
pub fn copy_file_data_only(source: &Path, destination: &Path) -> Result<u64> {
streaming_copy_optimized(source, destination)
}
pub fn copy_file_with_metadata_with_warnings(
source: &Path,
destination: &Path,
flags: &CopyFlags,
_warnings: &std::sync::Arc<std::sync::Mutex<Vec<String>>>,
) -> Result<u64> {
copy_file_with_metadata(source, destination, flags)
}
pub fn copy_symlink_with_metadata(
source: &Path,
destination: &Path,
#[cfg_attr(not(unix), allow(unused_variables))] flags: &CopyFlags,
) -> Result<u64> {
let target = fs::read_link(source)
.with_context(|| format!("Failed to read symlink target: {}", source.display()))?;
create_symlink_cross_platform(&target, destination)?;
#[cfg_attr(not(unix), allow(unused_variables))]
let source_metadata = fs::symlink_metadata(source).with_context(|| {
format!(
"Failed to read source symlink metadata: {}",
source.display()
)
})?;
#[cfg(unix)]
{
if flags.owner {
let _ = copy_symlink_ownership(source, destination, &source_metadata);
}
}
Ok(0)
}
fn create_symlink_cross_platform(target: &Path, destination: &Path) -> Result<()> {
if let Some(parent) = destination.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create parent directory: {}", parent.display()))?;
}
#[cfg(unix)]
std::os::unix::fs::symlink(target, destination).with_context(|| {
format!(
"Failed to create symlink: {} -> {}",
destination.display(),
target.display()
)
})?;
#[cfg(windows)]
{
crate::windows_symlinks::create_symlink(destination, target)?;
}
Ok(())
}
#[cfg(unix)]
fn copy_symlink_ownership(
_source: &Path,
destination: &Path,
source_metadata: &fs::Metadata,
) -> Result<()> {
use std::ffi::CString;
use std::os::unix::fs::MetadataExt;
let uid = source_metadata.uid();
let gid = source_metadata.gid();
let dest_cstring = CString::new(destination.as_os_str().to_string_lossy().as_ref())
.with_context(|| {
format!(
"Failed to convert path to CString: {}",
destination.display()
)
})?;
unsafe {
if libc::lchown(dest_cstring.as_ptr(), uid, gid) != 0 {
return Err(anyhow::anyhow!(
"Failed to change symlink ownership: {}",
destination.display()
));
}
}
Ok(())
}
pub fn copy_timestamps(
_source: &Path,
destination: &Path,
source_metadata: &fs::Metadata,
) -> Result<()> {
let modified = source_metadata
.modified()
.context("Failed to get source modification time")?;
let accessed = source_metadata
.accessed()
.context("Failed to get source access time")?;
set_file_mtime(destination, modified)
.with_context(|| format!("Failed to set modification time: {}", destination.display()))?;
let _ = accessed;
Ok(())
}
pub fn copy_permissions(
_source: &Path,
destination: &Path,
source_metadata: &fs::Metadata,
) -> Result<()> {
let permissions = source_metadata.permissions();
fs::set_permissions(destination, permissions)
.with_context(|| format!("Failed to set permissions: {}", destination.display()))?;
Ok(())
}
pub fn copy_attributes(
_source: &Path,
_destination: &Path,
_source_metadata: &fs::Metadata,
) -> Result<()> {
Ok(())
}
#[cfg(unix)]
pub fn copy_ownership(
_source: &Path,
destination: &Path,
source_metadata: &fs::Metadata,
) -> Result<()> {
use std::fs::File;
use std::os::unix::fs::fchown;
let uid = source_metadata.uid();
let gid = source_metadata.gid();
let file = File::open(destination).with_context(|| {
format!(
"Failed to open destination for ownership change: {}",
destination.display()
)
})?;
fchown(&file, Some(uid), Some(gid))
.with_context(|| format!("Failed to change ownership: {}", destination.display()))?;
Ok(())
}
fn set_file_mtime(path: &Path, mtime: SystemTime) -> Result<()> {
let filetime_mtime = filetime::FileTime::from(mtime);
let metadata =
fs::metadata(path).context("Failed to read file metadata for timestamp update")?;
let atime = metadata.accessed().context("Failed to get access time")?;
let filetime_atime = filetime::FileTime::from(atime);
#[cfg(windows)]
{
let permissions = metadata.permissions();
if permissions.readonly() {
let mut new_permissions = permissions.clone();
#[allow(clippy::permissions_set_readonly_false)]
new_permissions.set_readonly(false);
fs::set_permissions(path, new_permissions)
.context("Failed to temporarily remove readonly attribute")?;
let result = filetime::set_file_times(path, filetime_atime, filetime_mtime);
fs::set_permissions(path, permissions)
.context("Failed to restore readonly attribute")?;
result.context("Failed to set file times")?;
return Ok(());
}
}
filetime::set_file_times(path, filetime_atime, filetime_mtime)
.context("Failed to set file times")?;
Ok(())
}
#[derive(Debug, Clone, PartialEq)]
pub enum FilesystemType {
Local,
Network,
Tmpfs,
Unknown,
}
#[derive(Debug, Clone)]
pub struct FilesystemCapabilities {
pub supports_ownership: bool,
pub supports_permissions: bool,
pub supports_timestamps: bool,
pub supports_extended_attributes: bool,
pub filesystem_type: FilesystemType,
}
impl Default for FilesystemCapabilities {
fn default() -> Self {
Self {
supports_ownership: true,
supports_permissions: true,
supports_timestamps: true,
supports_extended_attributes: true,
filesystem_type: FilesystemType::Local,
}
}
}
pub fn detect_filesystem_capabilities(path: &Path) -> Result<FilesystemCapabilities> {
#[cfg(unix)]
{
detect_unix_filesystem_capabilities(path)
}
#[cfg(windows)]
{
detect_windows_filesystem_capabilities(path)
}
}
#[cfg(unix)]
fn detect_unix_filesystem_capabilities(path: &Path) -> Result<FilesystemCapabilities> {
let mount_info = get_mount_info(path)?;
let mut caps = FilesystemCapabilities::default();
caps.filesystem_type = classify_unix_filesystem(&mount_info.fstype, &mount_info.mount_point);
match mount_info.fstype.as_str() {
"nfs" | "nfs4" | "cifs" | "smb" | "smbfs" | "fuse.sshfs" => {
caps.supports_ownership = false; caps.supports_extended_attributes = false;
caps.filesystem_type = FilesystemType::Network;
}
"tmpfs" | "ramfs" => {
caps.filesystem_type = FilesystemType::Tmpfs;
}
"vfat" | "fat32" | "msdos" => {
caps.supports_ownership = false;
caps.supports_permissions = false; caps.supports_extended_attributes = false;
}
"ntfs" | "fuseblk" => {
caps.supports_ownership = false; caps.supports_extended_attributes = false;
}
_ => {
}
}
Ok(caps)
}
#[cfg(unix)]
fn get_mount_info(path: &Path) -> Result<MountInfo> {
use std::fs::File;
use std::io::{BufRead, BufReader};
let canonical_path = std::fs::canonicalize(path)
.with_context(|| format!("Failed to canonicalize path: {}", path.display()))?;
let file = File::open("/proc/mounts").context("Failed to open /proc/mounts")?;
let reader = BufReader::new(file);
let mut best_match = MountInfo {
mount_point: "/".to_string(),
fstype: "unknown".to_string(),
};
let mut best_match_len = 0;
for line in reader.lines() {
let line = line.context("Failed to read line from /proc/mounts")?;
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
let mount_point = parts[1];
let fstype = parts[2];
if canonical_path.starts_with(mount_point) && mount_point.len() > best_match_len {
best_match = MountInfo {
mount_point: mount_point.to_string(),
fstype: fstype.to_string(),
};
best_match_len = mount_point.len();
}
}
}
Ok(best_match)
}
#[cfg(unix)]
struct MountInfo {
mount_point: String,
fstype: String,
}
#[cfg(unix)]
fn classify_unix_filesystem(fstype: &str, mount_point: &str) -> FilesystemType {
match fstype {
"nfs" | "nfs4" | "cifs" | "smb" | "smbfs" | "fuse.sshfs" => FilesystemType::Network,
"tmpfs" | "ramfs" => FilesystemType::Tmpfs,
_ => {
if mount_point.starts_with("/mnt/")
|| mount_point.starts_with("/media/")
|| mount_point.starts_with("/net/")
|| mount_point.starts_with("/smb/")
{
FilesystemType::Network
} else {
FilesystemType::Local
}
}
}
}
#[cfg(windows)]
fn detect_windows_filesystem_capabilities(path: &Path) -> Result<FilesystemCapabilities> {
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
let mut caps = FilesystemCapabilities::default();
if let Some(path_str) = path.to_str() {
if path_str.starts_with("\\\\") {
caps.filesystem_type = FilesystemType::Network;
caps.supports_ownership = false; caps.supports_extended_attributes = false;
return Ok(caps);
}
}
let root_path = get_volume_root(path)?;
let root_wide: Vec<u16> = OsStr::new(&root_path)
.encode_wide()
.chain(std::iter::once(0))
.collect();
let mut fs_name = [0u16; 256];
let mut volume_flags = 0u32;
unsafe {
let success = winapi::um::fileapi::GetVolumeInformationW(
root_wide.as_ptr(),
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut volume_flags,
fs_name.as_mut_ptr(),
fs_name.len() as u32,
);
if success != 0 {
let fs_name_str = String::from_utf16_lossy(&fs_name);
let fs_name_clean = fs_name_str.trim_end_matches('\0').to_lowercase();
match fs_name_clean.as_str() {
"fat" | "fat32" | "exfat" => {
caps.supports_ownership = false;
caps.supports_permissions = false;
caps.supports_extended_attributes = false;
}
_ => {
}
}
}
}
Ok(caps)
}
#[cfg(windows)]
fn get_volume_root(path: &Path) -> Result<String> {
let path_str = path.to_str().context("Invalid path encoding")?;
if path_str.len() >= 2 && path_str.chars().nth(1) == Some(':') {
if let Some(drive_letter) = path_str.chars().nth(0) {
Ok(format!("{}:\\", drive_letter.to_uppercase()))
} else {
Ok("\\".to_string())
}
} else {
Ok("\\".to_string())
}
}
pub fn filter_copy_flags_for_filesystem(
flags: &CopyFlags,
caps: &FilesystemCapabilities,
) -> CopyFlags {
CopyFlags {
data: flags.data, attributes: flags.attributes && caps.supports_extended_attributes,
timestamps: flags.timestamps && caps.supports_timestamps,
security: flags.security && caps.supports_permissions,
owner: flags.owner && caps.supports_ownership,
auditing: false, }
}
pub fn is_network_path(path: &Path) -> bool {
match detect_filesystem_capabilities(path) {
Ok(caps) => caps.filesystem_type == FilesystemType::Network,
Err(_) => {
#[cfg(windows)]
{
if let Some(path_str) = path.to_str() {
return path_str.starts_with("\\\\");
}
}
#[cfg(unix)]
{
if let Some(path_str) = path.to_str() {
return path_str.starts_with("/mnt/")
|| path_str.starts_with("/media/")
|| path_str.starts_with("/net/")
|| path_str.starts_with("/smb/");
}
}
false
}
}
}
fn streaming_copy_optimized(source: &Path, destination: &Path) -> Result<u64> {
#[cfg(windows)]
{
match windows_native_copy(source, destination) {
Ok(bytes) => return Ok(bytes),
Err(_) => {
}
}
}
use std::fs::File;
use std::io::{Read, Write};
const NETWORK_BUFFER_SIZE: usize = 1 * 1024 * 1024;
let mut source_file = File::open(source)
.with_context(|| format!("Failed to open source file: {}", source.display()))?;
let mut dest_file = File::create(destination).with_context(|| {
format!(
"Failed to create destination file: {}",
destination.display()
)
})?;
if let Ok(metadata) = source_file.metadata() {
let _ = dest_file.set_len(metadata.len());
}
let mut buffer = vec![0u8; NETWORK_BUFFER_SIZE];
let mut total_bytes = 0u64;
loop {
let bytes_read = source_file
.read(&mut buffer)
.with_context(|| format!("Failed to read from source: {}", source.display()))?;
if bytes_read == 0 {
break;
}
dest_file
.write_all(&buffer[..bytes_read])
.with_context(|| {
format!("Failed to write to destination: {}", destination.display())
})?;
total_bytes += bytes_read as u64;
}
Ok(total_bytes)
}
#[cfg(windows)]
fn windows_native_copy(source: &Path, destination: &Path) -> Result<u64> {
fs::copy(source, destination).with_context(|| {
format!(
"Failed to copy file: {} -> {}",
source.display(),
destination.display()
)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_copy_flags_parsing() {
let flags = CopyFlags::from_string("DAT");
assert!(flags.data);
assert!(flags.attributes);
assert!(flags.timestamps);
assert!(!flags.security);
assert!(!flags.owner);
assert!(!flags.auditing);
let all_flags = CopyFlags::from_string("DATSOU");
assert!(all_flags.data);
assert!(all_flags.attributes);
assert!(all_flags.timestamps);
assert!(all_flags.security);
assert!(all_flags.owner);
assert!(!all_flags.auditing); }
#[test]
fn test_default_flags() {
let flags = CopyFlags::default();
assert!(flags.data);
assert!(flags.attributes);
assert!(flags.timestamps);
assert!(!flags.security);
assert!(!flags.owner);
assert!(!flags.auditing);
}
#[test]
fn test_all_flags() {
let flags = CopyFlags::all();
assert!(flags.data);
assert!(flags.attributes);
assert!(flags.timestamps);
assert!(flags.security);
assert!(flags.owner);
assert!(!flags.auditing); }
}