Skip to content

Commit f7c6d5c

Browse files
nhz2lazarusA
authored andcommitted
Make mv more atomic by trying rename before deleting dst (JuliaLang#55384)
As noted in JuliaLang#41584 and https://discourse.julialang.org/t/safe-overwriting-of-files/117758/3 `mv` is usually expected to be "best effort atomic". Currently calling `mv` with `force=true` calls `checkfor_mv_cp_cptree(src, dst, "moving"; force=true)` before renaming. `checkfor_mv_cp_cptree` will delete `dst` if exists and isn't the same as `src`. If `dst` is an existing file and julia stops after deleting `dst` but before doing the rename, `dst` will be removed but will not be replaced with `src`. This PR changes `mv` with `force=true` to first try rename, and only delete `dst` if that fails. Assuming file system support and the first rename works, julia stopping will not lead to `dst` being removed without being replaced. This also replaces a stopgap solution from JuliaLang#36638 (comment)
1 parent 4836b5c commit f7c6d5c

File tree

4 files changed

+74
-13
lines changed

4 files changed

+74
-13
lines changed

base/file.jl

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -440,11 +440,61 @@ julia> rm("goodbye.txt");
440440
```
441441
"""
442442
function mv(src::AbstractString, dst::AbstractString; force::Bool=false)
443-
checkfor_mv_cp_cptree(src, dst, "moving"; force=force)
444-
rename(src, dst)
443+
if force
444+
_mv_replace(src, dst)
445+
else
446+
_mv_noreplace(src, dst)
447+
end
448+
end
449+
450+
function _mv_replace(src::AbstractString, dst::AbstractString)
451+
# This check is copied from checkfor_mv_cp_cptree
452+
if ispath(dst) && Base.samefile(src, dst)
453+
abs_src = islink(src) ? abspath(readlink(src)) : abspath(src)
454+
abs_dst = islink(dst) ? abspath(readlink(dst)) : abspath(dst)
455+
throw(ArgumentError(string("'src' and 'dst' refer to the same file/dir. ",
456+
"This is not supported.\n ",
457+
"`src` refers to: $(abs_src)\n ",
458+
"`dst` refers to: $(abs_dst)\n")))
459+
end
460+
# First try to do a regular rename, because this might avoid a situation
461+
# where dst is deleted or truncated.
462+
try
463+
rename(src, dst)
464+
catch err
465+
err isa IOError || rethrow()
466+
err.code==Base.UV_ENOENT && rethrow()
467+
# on rename error try to delete dst if it exists and isn't the same as src
468+
checkfor_mv_cp_cptree(src, dst, "moving"; force=true)
469+
try
470+
rename(src, dst)
471+
catch err
472+
err isa IOError || rethrow()
473+
# on second error, default to force cp && rm
474+
cp(src, dst; force=true, follow_symlinks=false)
475+
rm(src; recursive=true)
476+
end
477+
end
478+
dst
479+
end
480+
481+
function _mv_noreplace(src::AbstractString, dst::AbstractString)
482+
# Error if dst exists.
483+
# This check currently has TOCTTOU issues.
484+
checkfor_mv_cp_cptree(src, dst, "moving"; force=false)
485+
try
486+
rename(src, dst)
487+
catch err
488+
err isa IOError || rethrow()
489+
err.code==Base.UV_ENOENT && rethrow()
490+
# on error, default to cp && rm
491+
cp(src, dst; force=false, follow_symlinks=false)
492+
rm(src; recursive=true)
493+
end
445494
dst
446495
end
447496

497+
448498
"""
449499
touch(path::AbstractString)
450500
touch(fd::File)
@@ -1126,15 +1176,24 @@ function unlink(p::AbstractString)
11261176
nothing
11271177
end
11281178

1129-
# For move command
1130-
function rename(src::AbstractString, dst::AbstractString; force::Bool=false)
1131-
err = ccall(:jl_fs_rename, Int32, (Cstring, Cstring), src, dst)
1132-
# on error, default to cp && rm
1179+
"""
1180+
rename(oldpath::AbstractString, newpath::AbstractString)
1181+
1182+
Change the name of a file from `oldpath` to `newpath`. If `newpath` is an existing file it may be replaced.
1183+
Equivalent to [rename(2)](https://man7.org/linux/man-pages/man2/rename.2.html).
1184+
Throws an `IOError` on failure.
1185+
Return `newpath`.
1186+
1187+
OS-specific restrictions may apply when `oldpath` and `newpath` are in different directories.
1188+
1189+
See also: [`mv`](@ref).
1190+
"""
1191+
function rename(oldpath::AbstractString, newpath::AbstractString)
1192+
err = ccall(:jl_fs_rename, Int32, (Cstring, Cstring), oldpath, newpath)
11331193
if err < 0
1134-
cp(src, dst; force=force, follow_symlinks=false)
1135-
rm(src; recursive=true)
1194+
uv_error("rename($(repr(oldpath)), $(repr(newpath)))", err)
11361195
end
1137-
nothing
1196+
newpath
11381197
end
11391198

11401199
function sendfile(src::AbstractString, dst::AbstractString)

base/loading.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3047,7 +3047,9 @@ function compilecache(pkg::PkgId, path::String, internal_stderr::IO = stderr, in
30473047
end
30483048
end
30493049
# this is atomic according to POSIX (not Win32):
3050-
rename(tmppath, cachefile; force=true)
3050+
# but force=true means it will fall back to non atomic
3051+
# move if the initial rename fails.
3052+
mv(tmppath, cachefile; force=true)
30513053
return cachefile, ocachefile
30523054
end
30533055
finally
@@ -3066,7 +3068,7 @@ end
30663068

30673069
function rename_unique_ocachefile(tmppath_so::String, ocachefile_orig::String, ocachefile::String = ocachefile_orig, num = 0)
30683070
try
3069-
rename(tmppath_so, ocachefile; force=true)
3071+
mv(tmppath_so, ocachefile; force=true)
30703072
catch e
30713073
e isa IOError || rethrow()
30723074
# If `rm` was called on a dir containing a loaded DLL, we moved it to temp for cleanup

test/file.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1031,7 +1031,7 @@ if !Sys.iswindows() || Sys.windows_version() >= Sys.WINDOWS_VISTA_VER
10311031
@test_throws Base._UVError("open($(repr(nonexisting_src)), $(Base.JL_O_RDONLY), 0)", Base.UV_ENOENT) cp(nonexisting_src, dst; force=true, follow_symlinks=false)
10321032
@test_throws Base._UVError("open($(repr(nonexisting_src)), $(Base.JL_O_RDONLY), 0)", Base.UV_ENOENT) cp(nonexisting_src, dst; force=true, follow_symlinks=true)
10331033
# mv
1034-
@test_throws Base._UVError("open($(repr(nonexisting_src)), $(Base.JL_O_RDONLY), 0)", Base.UV_ENOENT) mv(nonexisting_src, dst; force=true)
1034+
@test_throws Base._UVError("rename($(repr(nonexisting_src)), $(repr(dst)))", Base.UV_ENOENT) mv(nonexisting_src, dst; force=true)
10351035
end
10361036
end
10371037

test/filesystem.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ end
4444
@testset "Base.Filesystem docstrings" begin
4545
undoc = Docs.undocumented_names(Base.Filesystem)
4646
@test_broken isempty(undoc)
47-
@test undoc == [:File, :Filesystem, :cptree, :futime, :rename, :sendfile, :unlink]
47+
@test undoc == [:File, :Filesystem, :cptree, :futime, :sendfile, :unlink]
4848
end
4949

5050
@testset "write return type" begin

0 commit comments

Comments
 (0)