@@ -645,6 +645,36 @@ private static function extractWithType(string $source_type, string $filename, s
645645 };
646646 }
647647
648+ /**
649+ * Move file or directory, handling cross-device scenarios
650+ * Uses rename() if possible, falls back to copy+delete for cross-device moves
651+ *
652+ * @param string $source Source path
653+ * @param string $dest Destination path
654+ */
655+ private static function moveFileOrDir (string $ source , string $ dest ): void
656+ {
657+ $ source = self ::convertPath ($ source );
658+ $ dest = self ::convertPath ($ dest );
659+
660+ // Try rename first (fast, atomic)
661+ if (@rename ($ source , $ dest )) {
662+ return ;
663+ }
664+
665+ if (is_dir ($ source )) {
666+ self ::copyDir ($ source , $ dest );
667+ self ::removeDir ($ source );
668+ } else {
669+ if (!copy ($ source , $ dest )) {
670+ throw new FileSystemException ("Failed to copy file from {$ source } to {$ dest }" );
671+ }
672+ if (!unlink ($ source )) {
673+ throw new FileSystemException ("Failed to remove source file: {$ source }" );
674+ }
675+ }
676+ }
677+
648678 /**
649679 * Unzip file with stripping top-level directory
650680 */
@@ -675,10 +705,10 @@ private static function unzipWithStrip(string $zip_file, string $extract_path):
675705 if (is_dir ($ extract_path )) {
676706 self ::removeDir ($ extract_path );
677707 }
678- // if only one dir, move its contents to extract_path using rename
708+ // if only one dir, move its contents to extract_path
679709 $ subdir = self ::convertPath ("{$ temp_dir }/ {$ contents [0 ]}" );
680710 if (count ($ contents ) === 1 && is_dir ($ subdir )) {
681- rename ($ subdir , $ extract_path );
711+ self :: moveFileOrDir ($ subdir , $ extract_path );
682712 } else {
683713 // else, if it contains only one dir, strip dir and copy other files
684714 $ dircount = 0 ;
@@ -701,17 +731,20 @@ private static function unzipWithStrip(string $zip_file, string $extract_path):
701731 throw new FileSystemException ("Cannot scan unzip temp sub-dir: {$ dir [0 ]}" );
702732 }
703733 foreach ($ sub_contents as $ sub_item ) {
704- rename (self ::convertPath ("{$ temp_dir }/ {$ dir [0 ]}/ {$ sub_item }" ), self ::convertPath ("{$ extract_path }/ {$ sub_item }" ));
734+ self :: moveFileOrDir (self ::convertPath ("{$ temp_dir }/ {$ dir [0 ]}/ {$ sub_item }" ), self ::convertPath ("{$ extract_path }/ {$ sub_item }" ));
705735 }
706736 } else {
707737 foreach ($ dir as $ item ) {
708- rename (self ::convertPath ("{$ temp_dir }/ {$ item }" ), self ::convertPath ("{$ extract_path }/ {$ item }" ));
738+ self :: moveFileOrDir (self ::convertPath ("{$ temp_dir }/ {$ item }" ), self ::convertPath ("{$ extract_path }/ {$ item }" ));
709739 }
710740 }
711741 // move top-level files to extract_path
712742 foreach ($ top_files as $ top_file ) {
713- rename (self ::convertPath ("{$ temp_dir }/ {$ top_file }" ), self ::convertPath ("{$ extract_path }/ {$ top_file }" ));
743+ self :: moveFileOrDir (self ::convertPath ("{$ temp_dir }/ {$ top_file }" ), self ::convertPath ("{$ extract_path }/ {$ top_file }" ));
714744 }
715745 }
746+
747+ // Clean up temp directory
748+ self ::removeDir ($ temp_dir );
716749 }
717750}
0 commit comments