internal/fileio: extract low-level file I/O from internal/fs

Move TempFile and PreallocateFile into internal/fileio. internal/fs
primarily focuses on converting between data.Node and the actual
filesystem state. Extract the two methods to not pull in unnecessary
dependencies.
This commit is contained in:
Michael Eischer
2026-06-14 10:50:28 +02:00
parent 284daaf0b4
commit 4d1b9cef63
12 changed files with 98 additions and 73 deletions
+2 -2
View File
@@ -16,7 +16,7 @@ import (
"github.com/restic/restic/internal/backend/util"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/fileio"
"github.com/cenkalti/backoff/v4"
)
@@ -151,7 +151,7 @@ func (b *Local) Save(_ context.Context, h backend.Handle, rd backend.RewindReade
// preallocate disk space
if size := rd.Length(); size > 0 {
if err := fs.PreallocateFile(f, size); err != nil {
if err := fileio.PreallocateFile(f, size); err != nil {
debug.Log("Failed to preallocate %v with size %v: %v", finalname, size, err)
}
}
+22
View File
@@ -0,0 +1,22 @@
//go:build !windows
package fileio
import (
"os"
)
// TempFile creates a temporary file which has already been deleted (on
// supported platforms)
func TempFile(dir, prefix string) (f *os.File, err error) {
f, err = os.CreateTemp(dir, prefix)
if err != nil {
return nil, err
}
if err = os.Remove(f.Name()); err != nil {
return nil, err
}
return f, nil
}
+60
View File
@@ -0,0 +1,60 @@
package fileio
import (
"math/rand"
"os"
"path/filepath"
"strconv"
"syscall"
"golang.org/x/sys/windows"
)
// TempFile creates a temporary file which is marked as delete-on-close
func TempFile(dir, prefix string) (f *os.File, err error) {
// slightly modified implementation of os.CreateTemp(dir, prefix) to allow us to add
// the FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE flags.
// These provide two large benefits:
// FILE_ATTRIBUTE_TEMPORARY tells Windows to keep the file in memory only if possible
// which reduces the amount of unnecessary disk writes.
// FILE_FLAG_DELETE_ON_CLOSE instructs Windows to automatically delete the file once
// all file descriptors are closed.
if dir == "" {
dir = os.TempDir()
}
access := uint32(windows.GENERIC_READ | windows.GENERIC_WRITE)
creation := uint32(windows.CREATE_NEW)
share := uint32(0) // prevent other processes from accessing the file
flags := uint32(windows.FILE_ATTRIBUTE_TEMPORARY | windows.FILE_FLAG_DELETE_ON_CLOSE)
for i := 0; i < 10000; i++ {
randSuffix := strconv.Itoa(int(1e9 + rand.Intn(1e9)%1e9))[1:]
path := filepath.Join(dir, prefix+randSuffix)
ptr, err := windows.UTF16PtrFromString(path)
if err != nil {
return nil, err
}
h, err := windows.CreateFile(ptr, access, share, nil, creation, flags, 0)
if os.IsExist(err) {
continue
}
// Access denied error can occur if the tmp files conflict with each other.
if isAccessDeniedError(err) {
continue
}
return os.NewFile(uintptr(h), path), err
}
// Proper error handling is still to do
return nil, os.ErrExist
}
func isAccessDeniedError(err error) bool {
if errno, ok := err.(syscall.Errno); ok {
return errno == windows.ERROR_ACCESS_DENIED
}
return false
}
@@ -1,21 +1,21 @@
package fs_test
package fileio_test
import (
"errors"
"os"
"testing"
"github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/fileio"
rtest "github.com/restic/restic/internal/test"
)
func TestTempFile(t *testing.T) {
// create two temp files at the same time to check that the
// collision avoidance works
f, err := fs.TempFile("", "test")
f, err := fileio.TempFile("", "test")
fn := f.Name()
rtest.OK(t, err)
f2, err := fs.TempFile("", "test")
f2, err := fileio.TempFile("", "test")
fn2 := f2.Name()
rtest.OK(t, err)
rtest.Assert(t, fn != fn2, "filenames don't differ %s", fn)
@@ -1,4 +1,4 @@
package fs
package fileio
import (
"os"
@@ -1,4 +1,4 @@
package fs
package fileio
import (
"os"
@@ -1,6 +1,6 @@
//go:build !linux && !darwin
package fs
package fileio
import "os"
@@ -1,4 +1,4 @@
package fs
package fileio
import (
"os"
@@ -7,6 +7,7 @@ import (
"syscall"
"testing"
"github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/test"
)
@@ -31,7 +32,7 @@ func TestPreallocate(t *testing.T) {
fi, err := wr.Stat()
test.OK(t, err)
efi := ExtendedStat(fi)
efi := fs.ExtendedStat(fi)
test.Assert(t, efi.Size == i || efi.Blocks > 0, "Preallocated size of %v, got size %v block %v", i, efi.Size, efi.Blocks)
})
}
-15
View File
@@ -13,21 +13,6 @@ func fixpath(name string) string {
return name
}
// TempFile creates a temporary file which has already been deleted (on
// supported platforms)
func TempFile(dir, prefix string) (f *os.File, err error) {
f, err = os.CreateTemp(dir, prefix)
if err != nil {
return nil, err
}
if err = os.Remove(f.Name()); err != nil {
return nil, err
}
return f, nil
}
// isNotSupported returns true if the error is caused by an unsupported file system feature.
func isNotSupported(err error) bool {
if perr, ok := err.(*os.PathError); ok && perr.Err == syscall.ENOTSUP {
-44
View File
@@ -1,10 +1,8 @@
package fs
import (
"math/rand"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/restic/restic/internal/data"
@@ -43,48 +41,6 @@ func fixpath(name string) string {
return name
}
// TempFile creates a temporary file which is marked as delete-on-close
func TempFile(dir, prefix string) (f *os.File, err error) {
// slightly modified implementation of os.CreateTemp(dir, prefix) to allow us to add
// the FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE flags.
// These provide two large benefits:
// FILE_ATTRIBUTE_TEMPORARY tells Windows to keep the file in memory only if possible
// which reduces the amount of unnecessary disk writes.
// FILE_FLAG_DELETE_ON_CLOSE instructs Windows to automatically delete the file once
// all file descriptors are closed.
if dir == "" {
dir = os.TempDir()
}
access := uint32(windows.GENERIC_READ | windows.GENERIC_WRITE)
creation := uint32(windows.CREATE_NEW)
share := uint32(0) // prevent other processes from accessing the file
flags := uint32(windows.FILE_ATTRIBUTE_TEMPORARY | windows.FILE_FLAG_DELETE_ON_CLOSE)
for i := 0; i < 10000; i++ {
randSuffix := strconv.Itoa(int(1e9 + rand.Intn(1e9)%1e9))[1:]
path := filepath.Join(dir, prefix+randSuffix)
ptr, err := windows.UTF16PtrFromString(path)
if err != nil {
return nil, err
}
h, err := windows.CreateFile(ptr, access, share, nil, creation, flags, 0)
if os.IsExist(err) {
continue
}
// Access denied error can occur if the tmp files conflict with each other.
if isAccessDeniedError(err) {
continue
}
return os.NewFile(uintptr(h), path), err
}
// Proper error handling is still to do
return nil, os.ErrExist
}
// Chmod changes the mode of the named file to mode.
func chmod(name string, mode os.FileMode) error {
return os.Chmod(fixpath(name), mode)
+2 -2
View File
@@ -16,7 +16,7 @@ import (
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/fileio"
"github.com/restic/restic/internal/repository/crypto"
"github.com/restic/restic/internal/repository/pack"
)
@@ -201,7 +201,7 @@ func (r *packerManager) forgetPacker(packer *packer) {
// created or one is returned that already has some blobs.
func (r *packerManager) newPacker() (pck *packer, err error) {
debug.Log("create new pack")
tmpfile, err := fs.TempFile("", "restic-temp-pack-")
tmpfile, err := fileio.TempFile("", "restic-temp-pack-")
if err != nil {
return nil, errors.WithStack(err)
}
+2 -1
View File
@@ -10,6 +10,7 @@ import (
"github.com/hashicorp/golang-lru/v2/simplelru"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/fileio"
"github.com/restic/restic/internal/fs"
)
@@ -166,7 +167,7 @@ func ensureSize(f *os.File, fi os.FileInfo, createSize int64, sparse bool) (*os.
return nil, err
}
} else if createSize > 0 {
err := fs.PreallocateFile(f, createSize)
err := fileio.PreallocateFile(f, createSize)
if err != nil {
// Just log the preallocate error but don't let it cause the restore process to fail.
// Preallocate might return an error if the filesystem (implementation) does not