protected function doMoveInternal( array $params ) {
$status = $this->newStatus();
- $source = $this->resolveToFSPath( $params['src'] );
- if ( $source === null ) {
+ $fsSrcPath = $this->resolveToFSPath( $params['src'] );
+ if ( $fsSrcPath === null ) {
$status->fatal( 'backend-fail-invalidpath', $params['src'] );
return $status;
}
- $dest = $this->resolveToFSPath( $params['dst'] );
- if ( $dest === null ) {
+ $fsDstPath = $this->resolveToFSPath( $params['dst'] );
+ if ( $fsDstPath === null ) {
$status->fatal( 'backend-fail-invalidpath', $params['dst'] );
return $status;
}
- if ( !is_file( $source ) ) {
- if ( empty( $params['ignoreMissingSource'] ) ) {
- $status->fatal( 'backend-fail-move', $params['src'] );
- }
-
- return $status; // do nothing; either OK or bad status
+ if ( $fsSrcPath === $fsDstPath ) {
+ return $status; // no-op
}
+ $ignoreMissing = !empty( $params['ignoreMissingSource'] );
+
if ( !empty( $params['async'] ) ) { // deferred
- $cmd = implode( ' ', [
- $this->isWindows ? 'MOVE /Y' : 'mv', // (overwrite)
- escapeshellarg( $this->cleanPathSlashes( $source ) ),
- escapeshellarg( $this->cleanPathSlashes( $dest ) )
- ] );
+ // https://manpages.debian.org/buster/coreutils/mv.1.en.html
+ // https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/move
+ $encSrc = escapeshellarg( $this->cleanPathSlashes( $fsSrcPath ) );
+ $encDst = escapeshellarg( $this->cleanPathSlashes( $fsDstPath ) );
+ if ( $this->isWindows ) {
+ $writeCmd = "MOVE /Y $encSrc $encDst";
+ $cmd = $ignoreMissing ? "IF EXIST $encSrc $writeCmd" : $writeCmd;
+ } else {
+ $writeCmd = "mv -f $encSrc $encDst";
+ $cmd = $ignoreMissing ? "test -f $encSrc && $writeCmd" : $writeCmd;
+ }
$handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
if ( $errors !== '' && !( $this->isWindows && $errors[0] === " " ) ) {
$status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
};
$status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
} else { // immediate write
- $this->trapWarnings();
- $ok = ( $source === $dest ) ? true : rename( $source, $dest );
- $this->untrapWarnings();
- clearstatcache(); // file no longer at source
- if ( !$ok ) {
+ // Use rename() here since (a) this clears xattrs, (b) any threads still reading the
+ // old inode are unaffected since it writes to a new inode, and (c) this is fast and
+ // atomic within a file system volume (as is normally the case)
+ $this->trapWarnings( '/: No such file or directory$/' );
+ $moved = rename( $fsSrcPath, $fsDstPath );
+ $hadError = $this->untrapWarnings();
+ if ( $hadError || ( !$moved && !$ignoreMissing ) ) {
$status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
return $status;
"$base/unittest-cont1/e/fileB.a",
"$base/unittest-cont1/e/fileC.a"
];
- $createOps = [];
- $delOps = [];
+ $createOps = $copyOps = $moveOps = $deleteOps = [];
foreach ( $files as $path ) {
$status = $this->prepare( [ 'dir' => dirname( $path ) ] );
$this->assertGoodStatus( $status,
$createOps[] = [ 'op' => 'create', 'dst' => $path, 'content' => mt_rand( 0, 50000 ) ];
$copyOps[] = [ 'op' => 'copy', 'src' => $path, 'dst' => "$path-2" ];
$moveOps[] = [ 'op' => 'move', 'src' => "$path-2", 'dst' => "$path-3" ];
- $delOps[] = [ 'op' => 'delete', 'src' => $path ];
- $delOps[] = [ 'op' => 'delete', 'src' => "$path-3" ];
- $delOps[] = [ 'op' => 'delete', 'src' => "$path-gone", 'ignoreMissingSource' => true ];
+ $moveOps[] = [
+ 'op' => 'move',
+ 'src' => "$path-nothing",
+ 'dst' => "$path-nowhere",
+ 'ignoreMissingSource' => true
+ ];
+ $deleteOps[] = [ 'op' => 'delete', 'src' => $path ];
+ $deleteOps[] = [ 'op' => 'delete', 'src' => "$path-3" ];
+ $deleteOps[] = [
+ 'op' => 'delete',
+ 'src' => "$path-gone",
+ 'ignoreMissingSource' => true
+ ];
}
- $delOps[] = [ 'op' => 'null' ];
+ $deleteOps[] = [ 'op' => 'null' ];
$this->assertGoodStatus(
$this->backend->doQuickOperations( $createOps ),
"File {$files[0]} still exists." );
$this->assertGoodStatus(
- $this->backend->doQuickOperations( $delOps ),
+ $this->backend->doQuickOperations( $deleteOps ),
"Quick deletion of source files succeeded ($backendName)." );
foreach ( $files as $file ) {
$this->assertFalse( $this->backend->fileExists( [ 'src' => $file ] ),