@@ -11,6 +11,7 @@ pub use self::utils::OutputFormat;
1111use self :: operations:: cat:: OpenDalFileReader ;
1212use self :: operations:: copy:: OpenDalCopier ;
1313use self :: operations:: delete:: OpenDalDeleter ;
14+ use self :: operations:: diff:: OpenDalDiffer ;
1415use self :: operations:: download:: OpenDalDownloader ;
1516use self :: operations:: find:: OpenDalFinder ;
1617use self :: operations:: grep:: OpenDalGreper ;
@@ -23,8 +24,8 @@ use self::operations::tree::OpenDalTreer;
2324use self :: operations:: upload:: OpenDalUploader ;
2425use self :: operations:: usage:: OpenDalUsageCalculator ;
2526use self :: operations:: {
26- Cater , Copier , Deleter , Downloader , Greper , Header , Lister , Mkdirer , Mover , Stater , Tailer ,
27- Treer , Uploader , UsageCalculator ,
27+ Cater , Copier , Deleter , Differ , Downloader , Greper , Header , Lister , Mkdirer , Mover , Stater ,
28+ Tailer , Treer , Uploader , UsageCalculator ,
2829} ;
2930use crate :: storage:: utils:: error:: IntoStorifyError ;
3031use crate :: wrap_err;
@@ -673,4 +674,80 @@ impl StorageClient {
673674 } ,
674675 } )
675676 }
677+
678+ pub async fn diff_files (
679+ & self ,
680+ left : & str ,
681+ right : & str ,
682+ context : usize ,
683+ ignore_space : bool ,
684+ size_limit_mb : u64 ,
685+ force : bool ,
686+ ) -> Result < ( ) > {
687+ // Validate both paths are files
688+ let left_meta = self . operator . stat ( left) . await . map_err ( |e| {
689+ if e. kind ( ) == opendal:: ErrorKind :: NotFound {
690+ Error :: PathNotFound {
691+ path : std:: path:: PathBuf :: from ( left) ,
692+ }
693+ } else {
694+ Error :: InvalidArgument {
695+ message : format ! ( "Failed to stat '{}': {}" , left, e) ,
696+ }
697+ }
698+ } ) ?;
699+ let right_meta = self . operator . stat ( right) . await . map_err ( |e| {
700+ if e. kind ( ) == opendal:: ErrorKind :: NotFound {
701+ Error :: PathNotFound {
702+ path : std:: path:: PathBuf :: from ( right) ,
703+ }
704+ } else {
705+ Error :: InvalidArgument {
706+ message : format ! ( "Failed to stat '{}': {}" , right, e) ,
707+ }
708+ }
709+ } ) ?;
710+
711+ if !left_meta. mode ( ) . is_file ( ) || !right_meta. mode ( ) . is_file ( ) {
712+ return Err ( Error :: InvalidArgument {
713+ message : "diff only supports files; directories are not supported" . to_string ( ) ,
714+ } ) ;
715+ }
716+
717+ // Short-circuit: identical paths (after existence/type validation)
718+ if left == right {
719+ return Ok ( ( ) ) ;
720+ }
721+
722+ // Short-circuit when ETag and size match (content-identical for many providers)
723+ if left_meta. content_length ( ) == right_meta. content_length ( ) {
724+ let le = left_meta. etag ( ) ;
725+ let re = right_meta. etag ( ) ;
726+ match ( le, re) {
727+ ( Some ( le) , Some ( re) ) if le == re => return Ok ( ( ) ) ,
728+ _ => { }
729+ }
730+ }
731+
732+ // Size check (sum of both files)
733+ let total_mb =
734+ ( left_meta. content_length ( ) + right_meta. content_length ( ) ) . div_ceil ( 1024 * 1024 ) ;
735+ if size_limit_mb > 0 && total_mb > size_limit_mb && !force {
736+ return Err ( Error :: InvalidArgument {
737+ message : format ! (
738+ "Files too large ({}MB > {}MB). Use --force to override" ,
739+ total_mb, size_limit_mb
740+ ) ,
741+ } ) ;
742+ }
743+
744+ let differ = OpenDalDiffer :: new ( self . operator . clone ( ) ) ;
745+ wrap_err ! (
746+ differ. diff( left, right, context, ignore_space) . await ,
747+ DiffFailed {
748+ src_path: left. to_string( ) ,
749+ dest_path: right. to_string( )
750+ }
751+ )
752+ }
676753}
0 commit comments