diff --git a/backend/local/local_internal_diskfull_windows_test.go b/backend/local/local_internal_diskfull_windows_test.go new file mode 100644 index 000000000..794c74db7 --- /dev/null +++ b/backend/local/local_internal_diskfull_windows_test.go @@ -0,0 +1,37 @@ +//go:build windows + +package local + +import ( + "os" + "testing" + + "github.com/rclone/rclone/fs/fserrors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/sys/windows" +) + +func TestUpdateFatalIfNoSpaceWindows(t *testing.T) { + tests := []struct { + name string + err error + }{ + {"ERROR_DISK_FULL", windows.ERROR_DISK_FULL}, + {"openat PathError", &os.PathError{Op: "openat", Path: "test.txt", Err: windows.ERROR_DISK_FULL}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Run("off", func(t *testing.T) { + err := updateWithReader(t, false, test.err) + require.Error(t, err) + assert.False(t, fserrors.IsFatalError(err)) + }) + t.Run("on", func(t *testing.T) { + err := updateWithReader(t, true, test.err) + require.Error(t, err) + assert.True(t, fserrors.IsFatalError(err)) + }) + }) + } +} diff --git a/fs/fserrors/enospc_error.go b/fs/fserrors/enospc_error.go index bbb424e46..1ec5e44cf 100644 --- a/fs/fserrors/enospc_error.go +++ b/fs/fserrors/enospc_error.go @@ -3,16 +3,24 @@ package fserrors import ( + "slices" "syscall" liberrors "github.com/rclone/rclone/lib/errors" ) +// noSpaceErrors are the errors which mean the disk is full. +// +// Platform specific files add to this list in their init functions. +var noSpaceErrors = []error{ + syscall.ENOSPC, +} + // IsErrNoSpace checks a possibly wrapped error to -// see if it contains a ENOSPC error +// see if it contains an out of space error. func IsErrNoSpace(cause error) (isNoSpc bool) { liberrors.Walk(cause, func(c error) bool { - if c == syscall.ENOSPC { + if slices.Contains(noSpaceErrors, c) { isNoSpc = true return true } diff --git a/fs/fserrors/enospc_error_windows.go b/fs/fserrors/enospc_error_windows.go new file mode 100644 index 000000000..8ad7dc024 --- /dev/null +++ b/fs/fserrors/enospc_error_windows.go @@ -0,0 +1,17 @@ +//go:build windows + +package fserrors + +import ( + "golang.org/x/sys/windows" +) + +func init() { + // Windows does not return syscall.ENOSPC, which Go defines here as a + // value in its application reserved range. A full disk reports these. + // https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499- + noSpaceErrors = append(noSpaceErrors, + windows.ERROR_DISK_FULL, + windows.ERROR_HANDLE_DISK_FULL, + ) +} diff --git a/fs/fserrors/enospc_error_windows_test.go b/fs/fserrors/enospc_error_windows_test.go new file mode 100644 index 000000000..3a55756b8 --- /dev/null +++ b/fs/fserrors/enospc_error_windows_test.go @@ -0,0 +1,59 @@ +//go:build windows + +package fserrors + +import ( + "os" + "path/filepath" + "syscall" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/sys/windows" +) + +func TestIsErrNoSpaceRealWindowsError(t *testing.T) { + dir := t.TempDir() + dirp, err := windows.UTF16PtrFromString(dir) + if err != nil { + t.Skipf("cannot convert the temporary directory path: %v", err) + } + var available, total, free uint64 + if err := windows.GetDiskFreeSpaceEx(dirp, &available, &total, &free); err != nil { + t.Skipf("cannot read the free space of the temporary directory: %v", err) + } + + f, err := os.Create(filepath.Join(dir, "truncate")) + require.NoError(t, err) + defer func() { require.NoError(t, f.Close()) }() + + err = f.Truncate(int64(free + 1<<30)) + if err == nil { + t.Skip("real Windows disk-full error coverage lost: volume did not enforce the free-space limit when truncating the file") + } + assert.True(t, IsErrNoSpace(err), "error = %v", err) +} + +func TestIsErrNoSpaceWindows(t *testing.T) { + tests := []struct { + name string + err error + want bool + }{ + {"syscall.ENOSPC", syscall.ENOSPC, true}, + {"ERROR_DISK_FULL", windows.ERROR_DISK_FULL, true}, + {"ERROR_HANDLE_DISK_FULL", windows.ERROR_HANDLE_DISK_FULL, true}, + {"openat PathError", &os.PathError{Op: "openat", Path: "file", Err: windows.ERROR_DISK_FULL}, true}, + {"mkdirat PathError", &os.PathError{Op: "mkdirat", Path: "dir", Err: windows.ERROR_DISK_FULL}, true}, + {"SyscallError", os.NewSyscallError("write", windows.ERROR_HANDLE_DISK_FULL), true}, + {"ERROR_ACCESS_DENIED", windows.ERROR_ACCESS_DENIED, false}, + {"access denied PathError", &os.PathError{Op: "openat", Path: "file", Err: windows.ERROR_ACCESS_DENIED}, false}, + {"nil", nil, false}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.want, IsErrNoSpace(test.err)) + }) + } +}