@@ -493,6 +493,44 @@ public static function fullpath(string $path, string $relative_path_base): strin
493493 return FileSystem::convertPath ($ path );
494494 }
495495
496+ /**
497+ * Move file or directory, handling cross-device scenarios
498+ * Uses rename() if possible, falls back to copy+delete for cross-device moves
499+ *
500+ * @param string $source Source path
501+ * @param string $dest Destination path
502+ */
503+ public static function moveFileOrDir (string $ source , string $ dest ): void
504+ {
505+ $ source = FileSystem::convertPath ($ source );
506+ $ dest = FileSystem::convertPath ($ dest );
507+
508+ // Check if source and dest are on the same device to avoid cross-device rename errors
509+ $ source_stat = @stat ($ source );
510+ $ dest_parent = dirname ($ dest );
511+ $ dest_stat = @stat ($ dest_parent );
512+
513+ // Only use rename if on same device
514+ if ($ source_stat !== false && $ dest_stat !== false && $ source_stat ['dev ' ] === $ dest_stat ['dev ' ]) {
515+ if (@rename ($ source , $ dest )) {
516+ return ;
517+ }
518+ }
519+
520+ // Fall back to copy + delete for cross-device moves or if rename failed
521+ if (is_dir ($ source )) {
522+ FileSystem::copyDir ($ source , $ dest );
523+ FileSystem::removeDir ($ source );
524+ } else {
525+ if (!copy ($ source , $ dest )) {
526+ throw new FileSystemException ("Failed to copy file from {$ source } to {$ dest }" );
527+ }
528+ if (!unlink ($ source )) {
529+ throw new FileSystemException ("Failed to remove source file: {$ source }" );
530+ }
531+ }
532+ }
533+
496534 private static function replaceFile (string $ filename , int $ replace_type = REPLACE_FILE_STR , mixed $ callback_or_search = null , mixed $ to_replace = null ): false |int
497535 {
498536 logger ()->debug ('Replacing file with type[ ' . $ replace_type . ']: ' . $ filename );
0 commit comments