Skip to content

Commit 477163b

Browse files
committed
use NtSetInformationFile if os.Rename fails
1 parent 9d08d8f commit 477163b

3 files changed

Lines changed: 117 additions & 1 deletion

File tree

atomicwriter/atomicwriter.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ func (w *atomicFileWriter) Close() (retErr error) {
152152
return err
153153
}
154154
if w.writeErr == nil && w.written {
155-
return os.Rename(w.f.Name(), w.fn)
155+
return atomicwriterRenameAt(w.f.Name(), w.fn)
156156
}
157157
return nil
158158
}

atomicwriter/atomicwriter_unix.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//go:build !windows
2+
3+
package atomicwriter
4+
5+
import "os"
6+
7+
func atomicwriterRenameAt(oldpath, newpath string) error {
8+
return os.Rename(oldpath, newpath)
9+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package atomicwriter
2+
3+
import (
4+
"os"
5+
"unsafe"
6+
7+
"golang.org/x/sys/windows"
8+
)
9+
10+
// fileRenameInformation is the FILE_RENAME_INFORMATION structure used by
11+
// NtSetInformationFile to rename a file. FileName is a variable-length
12+
// field; callers must allocate a buffer large enough to hold the full name.
13+
type fileRenameInformation struct {
14+
ReplaceIfExists uint32
15+
RootDirectory windows.Handle
16+
FileNameLength uint32
17+
FileName [1]uint16
18+
}
19+
20+
// fileRenameInformationEx is the FILE_RENAME_INFORMATION_EX structure used by
21+
// NtSetInformationFile to rename a file.
22+
type fileRenameInformationEx struct {
23+
Flags uint32
24+
RootDirectory windows.Handle
25+
FileNameLength uint32
26+
FileName [1]uint16
27+
}
28+
29+
// atomicwriterRenameAt renames oldpath to newpath using os.Rename. If it fails,
30+
// it attempts to use NtSetInformationFile with FILE_RENAME_POSIX_SEMANTICS,
31+
// which allows atomic replacement of a file even when the destination
32+
// is open by another process.
33+
func atomicwriterRenameAt(oldpath, newpath string) error {
34+
35+
err := os.Rename(oldpath, newpath)
36+
if err == nil {
37+
return nil
38+
}
39+
// Open the source file requesting DELETE access so we can rename it.
40+
srcPtr, err := windows.UTF16PtrFromString(oldpath)
41+
if err != nil {
42+
return &os.PathError{Op: "rename", Path: oldpath, Err: err}
43+
}
44+
handle, err := windows.CreateFile(
45+
srcPtr,
46+
windows.DELETE,
47+
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
48+
nil,
49+
windows.OPEN_EXISTING,
50+
windows.FILE_ATTRIBUTE_NORMAL,
51+
0,
52+
)
53+
if err != nil {
54+
return &os.PathError{Op: "rename", Path: oldpath, Err: err}
55+
}
56+
defer windows.CloseHandle(handle)
57+
58+
// NtSetInformationFile requires an absolute NT path (\??\C:\...) when
59+
// RootDirectory is NULL.
60+
ntNewPath := `\??\` + newpath
61+
newPathUTF16, err := windows.UTF16FromString(ntNewPath)
62+
if err != nil {
63+
return &os.PathError{Op: "rename", Path: newpath, Err: err}
64+
}
65+
66+
fileNameLen := len(newPathUTF16)*2 - 2 // byte length, excluding null terminator
67+
renameInfoEx := fileRenameInformationEx{
68+
Flags: windows.FILE_RENAME_REPLACE_IF_EXISTS |
69+
windows.FILE_RENAME_POSIX_SEMANTICS,
70+
}
71+
var dummyEx fileRenameInformationEx
72+
bufferSizeEx := int(unsafe.Offsetof(dummyEx.FileName)) + fileNameLen
73+
bufferEx := make([]byte, bufferSizeEx)
74+
infoEx := (*fileRenameInformationEx)(unsafe.Pointer(&bufferEx[0]))
75+
infoEx.Flags = renameInfoEx.Flags
76+
infoEx.FileNameLength = uint32(fileNameLen)
77+
copy((*[windows.MAX_LONG_PATH]uint16)(unsafe.Pointer(&infoEx.FileName[0]))[:fileNameLen/2:fileNameLen/2], newPathUTF16)
78+
79+
const (
80+
FileRenameInformation = 10
81+
FileRenameInformationEx = 65
82+
)
83+
var iosbEx windows.IO_STATUS_BLOCK
84+
85+
err = windows.NtSetInformationFile(handle, &iosbEx, &bufferEx[0], uint32(bufferSizeEx), FileRenameInformationEx)
86+
if err == nil {
87+
return nil
88+
}
89+
90+
// If the extended rename fails, fall back to the original FILE_RENAME_INFORMATION
91+
// which is supported on older versions of Windows. This may fail if the destination
92+
// file is open by another process, but there's no way to detect that beforehand.
93+
94+
var dummy fileRenameInformation
95+
bufferSize := int(unsafe.Offsetof(dummy.FileName)) + fileNameLen
96+
buffer := make([]byte, bufferSize)
97+
info := (*fileRenameInformation)(unsafe.Pointer(&buffer[0]))
98+
info.ReplaceIfExists = windows.FILE_RENAME_REPLACE_IF_EXISTS | windows.FILE_RENAME_POSIX_SEMANTICS
99+
info.FileNameLength = uint32(fileNameLen)
100+
copy((*[windows.MAX_LONG_PATH]uint16)(unsafe.Pointer(&info.FileName[0]))[:fileNameLen/2:fileNameLen/2], newPathUTF16)
101+
102+
var iosb windows.IO_STATUS_BLOCK
103+
if err := windows.NtSetInformationFile(handle, &iosb, &buffer[0], uint32(bufferSize), FileRenameInformation); err != nil {
104+
return &os.PathError{Op: "rename", Path: newpath, Err: err}
105+
}
106+
return nil
107+
}

0 commit comments

Comments
 (0)