use std::fs::{ self, File };
use std::io::{ self, Read };
use std::path::Path;
use std::sync::atomic::{ AtomicBool, Ordering };
use std::time::Duration;
use super::native::Pending;
use super::CHUNK;
use crate::time::Instant;
const PROGRESS_EVERY: Duration = Duration::from_millis( 100 );
pub( super ) fn copy( from: &Path, to: &Path, all: bool, cancelled: &AtomicBool, progress: &dyn Fn( u64, u64 ) ) -> io::Result<()>
{
if fs::symlink_metadata( to ).is_ok() { return Err( io::ErrorKind::AlreadyExists.into() ); }
let meta = fs::metadata( from )?;
if meta.is_dir() && !all { return Err( io::ErrorKind::IsADirectory.into() ); }
let total = if meta.is_dir() { tree_size( from )? } else { meta.len() };
let mut meter = Meter { copied: 0, total, last: None, cancelled, progress };
if !meta.is_dir() { return copy_file( from, to, &mut meter ); }
let result = copy_tree( from, to, &mut meter );
if result.is_err() { let _ = fs::remove_dir_all( to ); }
result
}
struct Meter<'a>
{
copied: u64,
total: u64,
last: Option<Instant>,
cancelled: &'a AtomicBool,
progress: &'a dyn Fn( u64, u64 ),
}
impl Meter<'_>
{
fn advance( &mut self, bytes: u64 ) -> io::Result<()>
{
if self.cancelled.load( Ordering::Relaxed ) { return Err( io::Error::new( io::ErrorKind::Interrupted, "cancelled" ) ); }
self.copied += bytes;
let now = Instant::now();
if self.last.is_none_or( |last| now.duration_since( last ) >= PROGRESS_EVERY ) || self.copied == self.total
{
self.last = Some( now );
( self.progress )( self.copied, self.total );
}
Ok( () )
}
}
fn tree_size( dir: &Path ) -> io::Result<u64>
{
let mut size = 0;
for entry in fs::read_dir( dir )?
{
let entry = entry?;
let kind = entry.file_type()?;
if kind.is_dir() { size += tree_size( &entry.path() )?; } else if kind.is_file() { size += entry.metadata()?.len(); }
}
Ok( size )
}
fn copy_tree( from: &Path, to: &Path, meter: &mut Meter<'_> ) -> io::Result<()>
{
fs::create_dir( to )?;
for entry in fs::read_dir( from )?
{
let entry = entry?;
let kind = entry.file_type()?;
let target = to.join( entry.file_name() );
if kind.is_dir()
{
copy_tree( &entry.path(), &target, meter )?;
} else if kind.is_symlink() {
copy_link( &entry.path(), &target )?;
} else {
copy_file( &entry.path(), &target, meter )?;
}
}
let _ = fs::set_permissions( to, fs::metadata( from )?.permissions() );
Ok( () )
}
fn copy_file( from: &Path, to: &Path, meter: &mut Meter<'_> ) -> io::Result<()>
{
let mut source = File::open( from )?;
let mut target = Pending::create( to )?;
let mut buffer = vec![ 0; CHUNK ];
loop
{
let read = match source.read( &mut buffer )
{
Ok( 0 ) => break,
Ok( read ) => read,
Err( e ) if e.kind() == io::ErrorKind::Interrupted => continue,
Err( e ) => return Err( e ),
};
target.write( &buffer[ ..read ] )?;
meter.advance( read as u64 )?;
}
meter.advance( 0 )?;
target.commit()?;
let _ = fs::set_permissions( to, source.metadata()?.permissions() );
Ok( () )
}
#[ cfg( unix ) ]
fn copy_link( from: &Path, to: &Path ) -> io::Result<()>
{
std::os::unix::fs::symlink( fs::read_link( from )?, to )
}
#[ cfg( not( unix ) ) ]
fn copy_link( _from: &Path, _to: &Path ) -> io::Result<()>
{
Ok( () )
}
#[ cfg( test ) ]
mod tests
{
use super::*;
use std::cell::Cell;
use std::path::PathBuf;
fn scratch( name: &str ) -> PathBuf
{
let dir = std::env::temp_dir().join( format!( "ltk-copy-{name}-{}", std::process::id() ) );
let _ = fs::remove_dir_all( &dir );
fs::create_dir_all( &dir ).unwrap();
dir
}
#[ test ]
fn a_tree_is_copied_whole_with_its_progress()
{
let dir = scratch( "tree" );
let src = dir.join( "src" );
fs::create_dir_all( src.join( "sub" ) ).unwrap();
fs::write( src.join( "a" ), vec![ 1; CHUNK + 10 ] ).unwrap();
fs::write( src.join( "sub/b" ), b"bb" ).unwrap();
let last = Cell::new( ( 0, 0 ) );
copy( &src, &dir.join( "dst" ), true, &AtomicBool::new( false ), &|copied, total| last.set( ( copied, total ) ) ).unwrap();
assert_eq!( fs::read( dir.join( "dst/a" ) ).unwrap().len(), CHUNK + 10 );
assert_eq!( fs::read( dir.join( "dst/sub/b" ) ).unwrap(), b"bb" );
assert_eq!( last.get(), ( CHUNK as u64 + 12, CHUNK as u64 + 12 ) );
fs::remove_dir_all( &dir ).unwrap();
}
#[ test ]
fn a_cancelled_copy_leaves_nothing()
{
let dir = scratch( "cancel" );
let src = dir.join( "src" );
fs::create_dir( &src ).unwrap();
fs::write( src.join( "a" ), b"aa" ).unwrap();
let result = copy( &src, &dir.join( "dst" ), true, &AtomicBool::new( true ), &|_, _| {} );
assert_eq!( result.unwrap_err().kind(), io::ErrorKind::Interrupted );
assert!( !dir.join( "dst" ).exists() );
fs::remove_dir_all( &dir ).unwrap();
}
#[ test ]
fn a_copy_never_replaces()
{
let dir = scratch( "exists" );
fs::write( dir.join( "a" ), b"a" ).unwrap();
fs::write( dir.join( "b" ), b"b" ).unwrap();
let result = copy( &dir.join( "a" ), &dir.join( "b" ), false, &AtomicBool::new( false ), &|_, _| {} );
assert_eq!( result.unwrap_err().kind(), io::ErrorKind::AlreadyExists );
assert_eq!( fs::read( dir.join( "b" ) ).unwrap(), b"b" );
fs::remove_dir_all( &dir ).unwrap();
}
}