|
| 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