Browse code

deprecate pkg/atomicwriter, migrate to github.com/moby/sys/atomicwriter

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Sebastiaan van Stijn authored on 2025/04/05 03:58:00
Showing 21 changed files
... ...
@@ -30,13 +30,13 @@ import (
30 30
 	"github.com/docker/docker/image"
31 31
 	libcontainerdtypes "github.com/docker/docker/libcontainerd/types"
32 32
 	"github.com/docker/docker/oci"
33
-	"github.com/docker/docker/pkg/atomicwriter"
34 33
 	"github.com/docker/docker/pkg/idtools"
35 34
 	"github.com/docker/docker/restartmanager"
36 35
 	"github.com/docker/docker/volume"
37 36
 	volumemounts "github.com/docker/docker/volume/mounts"
38 37
 	"github.com/docker/go-units"
39 38
 	agentexec "github.com/moby/swarmkit/v2/agent/exec"
39
+	"github.com/moby/sys/atomicwriter"
40 40
 	"github.com/moby/sys/signal"
41 41
 	"github.com/moby/sys/symlink"
42 42
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
... ...
@@ -6,7 +6,7 @@ import (
6 6
 	"path/filepath"
7 7
 	"strings"
8 8
 
9
-	"github.com/docker/docker/pkg/atomicwriter"
9
+	"github.com/moby/sys/atomicwriter"
10 10
 )
11 11
 
12 12
 // convertKVStringsToMap converts ["key=value"] to {"key":"value"}
... ...
@@ -23,12 +23,12 @@ import (
23 23
 	"github.com/docker/docker/internal/containerfs"
24 24
 	"github.com/docker/docker/internal/directory"
25 25
 	"github.com/docker/docker/pkg/archive"
26
-	"github.com/docker/docker/pkg/atomicwriter"
27 26
 	"github.com/docker/docker/pkg/chrootarchive"
28 27
 	"github.com/docker/docker/pkg/idtools"
29 28
 	"github.com/docker/docker/quota"
30 29
 	"github.com/docker/go-units"
31 30
 	"github.com/moby/locker"
31
+	"github.com/moby/sys/atomicwriter"
32 32
 	"github.com/moby/sys/mount"
33 33
 	"github.com/moby/sys/userns"
34 34
 	"github.com/opencontainers/selinux/go-selinux/label"
... ...
@@ -4,8 +4,8 @@ import (
4 4
 	"os"
5 5
 	"path/filepath"
6 6
 
7
-	"github.com/docker/docker/pkg/atomicwriter"
8 7
 	"github.com/google/uuid"
8
+	"github.com/moby/sys/atomicwriter"
9 9
 	"github.com/pkg/errors"
10 10
 )
11 11
 
... ...
@@ -21,7 +21,7 @@ import (
21 21
 	"github.com/docker/docker/daemon/config"
22 22
 	"github.com/docker/docker/errdefs"
23 23
 	"github.com/docker/docker/libcontainerd/shimopts"
24
-	"github.com/docker/docker/pkg/atomicwriter"
24
+	"github.com/moby/sys/atomicwriter"
25 25
 	"github.com/opencontainers/runtime-spec/specs-go/features"
26 26
 	"github.com/pkg/errors"
27 27
 )
... ...
@@ -5,7 +5,7 @@ import (
5 5
 	"path/filepath"
6 6
 	"sync"
7 7
 
8
-	"github.com/docker/docker/pkg/atomicwriter"
8
+	"github.com/moby/sys/atomicwriter"
9 9
 )
10 10
 
11 11
 // Store implements a K/V store for mapping distribution-related IDs
... ...
@@ -8,7 +8,7 @@ import (
8 8
 	"sync"
9 9
 
10 10
 	"github.com/containerd/log"
11
-	"github.com/docker/docker/pkg/atomicwriter"
11
+	"github.com/moby/sys/atomicwriter"
12 12
 	"github.com/opencontainers/go-digest"
13 13
 	"github.com/pkg/errors"
14 14
 )
... ...
@@ -12,8 +12,8 @@ import (
12 12
 
13 13
 	"github.com/containerd/log"
14 14
 	"github.com/docker/distribution"
15
-	"github.com/docker/docker/pkg/atomicwriter"
16 15
 	"github.com/docker/docker/pkg/ioutils"
16
+	"github.com/moby/sys/atomicwriter"
17 17
 	"github.com/opencontainers/go-digest"
18 18
 	"github.com/pkg/errors"
19 19
 )
... ...
@@ -31,7 +31,7 @@ import (
31 31
 	"text/template"
32 32
 
33 33
 	"github.com/containerd/log"
34
-	"github.com/docker/docker/pkg/atomicwriter"
34
+	"github.com/moby/sys/atomicwriter"
35 35
 	"github.com/opencontainers/go-digest"
36 36
 	"github.com/pkg/errors"
37 37
 )
38 38
deleted file mode 100644
... ...
@@ -1,245 +0,0 @@
1
-// Package atomicwriter provides utilities to perform atomic writes to a
2
-// file or set of files.
3
-package atomicwriter
4
-
5
-import (
6
-	"errors"
7
-	"fmt"
8
-	"io"
9
-	"os"
10
-	"path/filepath"
11
-	"syscall"
12
-
13
-	"github.com/moby/sys/sequential"
14
-)
15
-
16
-func validateDestination(fileName string) error {
17
-	if fileName == "" {
18
-		return errors.New("file name is empty")
19
-	}
20
-	if dir := filepath.Dir(fileName); dir != "" && dir != "." && dir != ".." {
21
-		di, err := os.Stat(dir)
22
-		if err != nil {
23
-			return fmt.Errorf("invalid output path: %w", err)
24
-		}
25
-		if !di.IsDir() {
26
-			return fmt.Errorf("invalid output path: %w", &os.PathError{Op: "stat", Path: dir, Err: syscall.ENOTDIR})
27
-		}
28
-	}
29
-
30
-	// Deliberately using Lstat here to match the behavior of [os.Rename],
31
-	// which is used when completing the write and does not resolve symlinks.
32
-	fi, err := os.Lstat(fileName)
33
-	if err != nil {
34
-		if os.IsNotExist(err) {
35
-			return nil
36
-		}
37
-		return fmt.Errorf("failed to stat output path: %w", err)
38
-	}
39
-
40
-	switch mode := fi.Mode(); {
41
-	case mode.IsRegular():
42
-		return nil // Regular file
43
-	case mode&os.ModeDir != 0:
44
-		return errors.New("cannot write to a directory")
45
-	case mode&os.ModeSymlink != 0:
46
-		return errors.New("cannot write to a symbolic link directly")
47
-	case mode&os.ModeNamedPipe != 0:
48
-		return errors.New("cannot write to a named pipe (FIFO)")
49
-	case mode&os.ModeSocket != 0:
50
-		return errors.New("cannot write to a socket")
51
-	case mode&os.ModeDevice != 0:
52
-		if mode&os.ModeCharDevice != 0 {
53
-			return errors.New("cannot write to a character device file")
54
-		}
55
-		return errors.New("cannot write to a block device file")
56
-	case mode&os.ModeSetuid != 0:
57
-		return errors.New("cannot write to a setuid file")
58
-	case mode&os.ModeSetgid != 0:
59
-		return errors.New("cannot write to a setgid file")
60
-	case mode&os.ModeSticky != 0:
61
-		return errors.New("cannot write to a sticky bit file")
62
-	default:
63
-		return fmt.Errorf("unknown file mode: %[1]s (%#[1]o)", mode)
64
-	}
65
-}
66
-
67
-// New returns a WriteCloser so that writing to it writes to a
68
-// temporary file and closing it atomically changes the temporary file to
69
-// destination path. Writing and closing concurrently is not allowed.
70
-// NOTE: umask is not considered for the file's permissions.
71
-//
72
-// New uses [sequential.CreateTemp] to use sequential file access on Windows,
73
-// avoiding depleting the standby list un-necessarily. On Linux, this equates to
74
-// a regular [os.CreateTemp]. Refer to the [Win32 API documentation] for details
75
-// on sequential file access.
76
-//
77
-// [Win32 API documentation]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN
78
-func New(filename string, perm os.FileMode) (io.WriteCloser, error) {
79
-	if err := validateDestination(filename); err != nil {
80
-		return nil, err
81
-	}
82
-	abspath, err := filepath.Abs(filename)
83
-	if err != nil {
84
-		return nil, err
85
-	}
86
-
87
-	f, err := sequential.CreateTemp(filepath.Dir(abspath), ".tmp-"+filepath.Base(filename))
88
-	if err != nil {
89
-		return nil, err
90
-	}
91
-	return &atomicFileWriter{
92
-		f:    f,
93
-		fn:   abspath,
94
-		perm: perm,
95
-	}, nil
96
-}
97
-
98
-// WriteFile atomically writes data to a file named by filename and with the
99
-// specified permission bits. The given filename is created if it does not exist,
100
-// but the destination directory must exist. It can be used as a drop-in replacement
101
-// for [os.WriteFile], but currently does not allow the destination path to be
102
-// a symlink. WriteFile is implemented using [New] for its implementation.
103
-//
104
-// NOTE: umask is not considered for the file's permissions.
105
-func WriteFile(filename string, data []byte, perm os.FileMode) error {
106
-	f, err := New(filename, perm)
107
-	if err != nil {
108
-		return err
109
-	}
110
-	n, err := f.Write(data)
111
-	if err == nil && n < len(data) {
112
-		err = io.ErrShortWrite
113
-		f.(*atomicFileWriter).writeErr = err
114
-	}
115
-	if err1 := f.Close(); err == nil {
116
-		err = err1
117
-	}
118
-	return err
119
-}
120
-
121
-type atomicFileWriter struct {
122
-	f        *os.File
123
-	fn       string
124
-	writeErr error
125
-	written  bool
126
-	perm     os.FileMode
127
-}
128
-
129
-func (w *atomicFileWriter) Write(dt []byte) (int, error) {
130
-	w.written = true
131
-	n, err := w.f.Write(dt)
132
-	if err != nil {
133
-		w.writeErr = err
134
-	}
135
-	return n, err
136
-}
137
-
138
-func (w *atomicFileWriter) Close() (retErr error) {
139
-	defer func() {
140
-		if err := os.Remove(w.f.Name()); !errors.Is(err, os.ErrNotExist) && retErr == nil {
141
-			retErr = err
142
-		}
143
-	}()
144
-	if err := w.f.Sync(); err != nil {
145
-		_ = w.f.Close()
146
-		return err
147
-	}
148
-	if err := w.f.Close(); err != nil {
149
-		return err
150
-	}
151
-	if err := os.Chmod(w.f.Name(), w.perm); err != nil {
152
-		return err
153
-	}
154
-	if w.writeErr == nil && w.written {
155
-		return os.Rename(w.f.Name(), w.fn)
156
-	}
157
-	return nil
158
-}
159
-
160
-// WriteSet is used to atomically write a set
161
-// of files and ensure they are visible at the same time.
162
-// Must be committed to a new directory.
163
-type WriteSet struct {
164
-	root string
165
-}
166
-
167
-// NewWriteSet creates a new atomic write set to
168
-// atomically create a set of files. The given directory
169
-// is used as the base directory for storing files before
170
-// commit. If no temporary directory is given the system
171
-// default is used.
172
-func NewWriteSet(tmpDir string) (*WriteSet, error) {
173
-	td, err := os.MkdirTemp(tmpDir, "write-set-")
174
-	if err != nil {
175
-		return nil, err
176
-	}
177
-
178
-	return &WriteSet{
179
-		root: td,
180
-	}, nil
181
-}
182
-
183
-// WriteFile writes a file to the set, guaranteeing the file
184
-// has been synced.
185
-func (ws *WriteSet) WriteFile(filename string, data []byte, perm os.FileMode) error {
186
-	f, err := ws.FileWriter(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
187
-	if err != nil {
188
-		return err
189
-	}
190
-	n, err := f.Write(data)
191
-	if err == nil && n < len(data) {
192
-		err = io.ErrShortWrite
193
-	}
194
-	if err1 := f.Close(); err == nil {
195
-		err = err1
196
-	}
197
-	return err
198
-}
199
-
200
-type syncFileCloser struct {
201
-	*os.File
202
-}
203
-
204
-func (w syncFileCloser) Close() error {
205
-	err := w.File.Sync()
206
-	if err1 := w.File.Close(); err == nil {
207
-		err = err1
208
-	}
209
-	return err
210
-}
211
-
212
-// FileWriter opens a file writer inside the set. The file
213
-// should be synced and closed before calling commit.
214
-//
215
-// FileWriter uses [sequential.OpenFile] to use sequential file access on Windows,
216
-// avoiding depleting the standby list un-necessarily. On Linux, this equates to
217
-// a regular [os.OpenFile]. Refer to the [Win32 API documentation] for details
218
-// on sequential file access.
219
-//
220
-// [Win32 API documentation]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN
221
-func (ws *WriteSet) FileWriter(name string, flag int, perm os.FileMode) (io.WriteCloser, error) {
222
-	f, err := sequential.OpenFile(filepath.Join(ws.root, name), flag, perm)
223
-	if err != nil {
224
-		return nil, err
225
-	}
226
-	return syncFileCloser{f}, nil
227
-}
228
-
229
-// Cancel cancels the set and removes all temporary data
230
-// created in the set.
231
-func (ws *WriteSet) Cancel() error {
232
-	return os.RemoveAll(ws.root)
233
-}
234
-
235
-// Commit moves all created files to the target directory. The
236
-// target directory must not exist and the parent of the target
237
-// directory must exist.
238
-func (ws *WriteSet) Commit(target string) error {
239
-	return os.Rename(ws.root, target)
240
-}
241
-
242
-// String returns the location the set is writing to.
243
-func (ws *WriteSet) String() string {
244
-	return ws.root
245
-}
246 1
new file mode 100644
... ...
@@ -0,0 +1,56 @@
0
+package atomicwriter
1
+
2
+import (
3
+	"io"
4
+	"os"
5
+
6
+	"github.com/moby/sys/atomicwriter"
7
+)
8
+
9
+// New returns a WriteCloser so that writing to it writes to a
10
+// temporary file and closing it atomically changes the temporary file to
11
+// destination path. Writing and closing concurrently is not allowed.
12
+// NOTE: umask is not considered for the file's permissions.
13
+//
14
+// New uses [sequential.CreateTemp] to use sequential file access on Windows,
15
+// avoiding depleting the standby list un-necessarily. On Linux, this equates to
16
+// a regular [os.CreateTemp]. Refer to the [Win32 API documentation] for details
17
+// on sequential file access.
18
+//
19
+// Deprecated: use [atomicwriter.New] instead.
20
+//
21
+// [Win32 API documentation]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN
22
+func New(filename string, perm os.FileMode) (io.WriteCloser, error) {
23
+	return atomicwriter.New(filename, perm)
24
+}
25
+
26
+// WriteFile atomically writes data to a file named by filename and with the
27
+// specified permission bits. The given filename is created if it does not exist,
28
+// but the destination directory must exist. It can be used as a drop-in replacement
29
+// for [os.WriteFile], but currently does not allow the destination path to be
30
+// a symlink. WriteFile is implemented using [New] for its implementation.
31
+//
32
+// NOTE: umask is not considered for the file's permissions.
33
+//
34
+// Deprecated: use [atomicwriter.WriteFile] instead.
35
+func WriteFile(filename string, data []byte, perm os.FileMode) error {
36
+	return atomicwriter.WriteFile(filename, data, perm)
37
+}
38
+
39
+// WriteSet is used to atomically write a set
40
+// of files and ensure they are visible at the same time.
41
+// Must be committed to a new directory.
42
+//
43
+// Deprecated: use [atomicwriter.WriteSet] instead.
44
+type WriteSet = atomicwriter.WriteSet
45
+
46
+// NewWriteSet creates a new atomic write set to
47
+// atomically create a set of files. The given directory
48
+// is used as the base directory for storing files before
49
+// commit. If no temporary directory is given the system
50
+// default is used.
51
+//
52
+// Deprecated: use [atomicwriter.NewWriteSet] instead.
53
+func NewWriteSet(tmpDir string) (*atomicwriter.WriteSet, error) {
54
+	return atomicwriter.NewWriteSet(tmpDir)
55
+}
0 56
deleted file mode 100644
... ...
@@ -1,325 +0,0 @@
1
-package atomicwriter
2
-
3
-import (
4
-	"bytes"
5
-	"errors"
6
-	"os"
7
-	"path/filepath"
8
-	"runtime"
9
-	"strings"
10
-	"syscall"
11
-	"testing"
12
-)
13
-
14
-// testMode returns the file-mode to use in tests, accounting for Windows
15
-// not supporting full Linux file mode.
16
-func testMode() os.FileMode {
17
-	if runtime.GOOS == "windows" {
18
-		return 0o666
19
-	}
20
-	return 0o640
21
-}
22
-
23
-// assertFile asserts the given fileName to exist, and to have the expected
24
-// content and mode.
25
-func assertFile(t *testing.T, fileName string, fileContent []byte, expectedMode os.FileMode) {
26
-	t.Helper()
27
-	actual, err := os.ReadFile(fileName)
28
-	if err != nil {
29
-		t.Fatalf("Error reading from file: %v", err)
30
-	}
31
-
32
-	if !bytes.Equal(actual, fileContent) {
33
-		t.Errorf("Data mismatch, expected %q, got %q", fileContent, actual)
34
-	}
35
-
36
-	st, err := os.Stat(fileName)
37
-	if err != nil {
38
-		t.Fatalf("Error statting file: %v", err)
39
-	}
40
-	if st.Mode() != expectedMode {
41
-		t.Errorf("Mode mismatched, expected %o, got %o", expectedMode, st.Mode())
42
-	}
43
-}
44
-
45
-// assertFileCount asserts the given directory has the expected number
46
-// of files, and returns the list of files found.
47
-func assertFileCount(t *testing.T, directory string, expected int) []os.DirEntry {
48
-	t.Helper()
49
-	files, err := os.ReadDir(directory)
50
-	if err != nil {
51
-		t.Fatalf("Error reading dir: %v", err)
52
-	}
53
-	if len(files) != expected {
54
-		t.Errorf("Expected %d files, got %d: %v", expected, len(files), files)
55
-	}
56
-	return files
57
-}
58
-
59
-func TestNew(t *testing.T) {
60
-	for _, tc := range []string{"normal", "symlinked"} {
61
-		tmpDir := t.TempDir()
62
-		parentDir := tmpDir
63
-		actualParentDir := parentDir
64
-		if tc == "symlinked" {
65
-			actualParentDir = filepath.Join(tmpDir, "parent-dir")
66
-			if err := os.Mkdir(actualParentDir, 0o700); err != nil {
67
-				t.Fatal(err)
68
-			}
69
-			parentDir = filepath.Join(tmpDir, "parent-dir-symlink")
70
-			if err := os.Symlink(actualParentDir, parentDir); err != nil {
71
-				t.Fatal(err)
72
-			}
73
-		}
74
-		t.Run(tc, func(t *testing.T) {
75
-			for _, tc := range []string{"new-file", "existing-file"} {
76
-				t.Run(tc, func(t *testing.T) {
77
-					fileName := filepath.Join(parentDir, "test.txt")
78
-					var origFileCount int
79
-					if tc == "existing-file" {
80
-						if err := os.WriteFile(fileName, []byte("original content"), testMode()); err != nil {
81
-							t.Fatalf("Error writing file: %v", err)
82
-						}
83
-						origFileCount = 1
84
-					}
85
-					writer, err := New(fileName, testMode())
86
-					if writer == nil {
87
-						t.Errorf("Writer is nil")
88
-					}
89
-					if err != nil {
90
-						t.Fatalf("Error creating new atomicwriter: %v", err)
91
-					}
92
-					files := assertFileCount(t, actualParentDir, origFileCount+1)
93
-					if tmpFileName := files[0].Name(); !strings.HasPrefix(tmpFileName, ".tmp-test.txt") {
94
-						t.Errorf("Unexpected file name for temp-file: %s", tmpFileName)
95
-					}
96
-
97
-					// Closing the writer without writing should clean up the temp-file,
98
-					// and should not replace the destination file.
99
-					if err = writer.Close(); err != nil {
100
-						t.Errorf("Error closing writer: %v", err)
101
-					}
102
-					assertFileCount(t, actualParentDir, origFileCount)
103
-					if tc == "existing-file" {
104
-						assertFile(t, fileName, []byte("original content"), testMode())
105
-					}
106
-				})
107
-			}
108
-		})
109
-	}
110
-}
111
-
112
-func TestNewInvalid(t *testing.T) {
113
-	t.Run("missing target dir", func(t *testing.T) {
114
-		tmpDir := t.TempDir()
115
-		fileName := filepath.Join(tmpDir, "missing-dir", "test.txt")
116
-		writer, err := New(fileName, testMode())
117
-		if writer != nil {
118
-			t.Errorf("Should not have created writer")
119
-		}
120
-		if !errors.Is(err, os.ErrNotExist) {
121
-			t.Errorf("Should produce a 'not found' error, but got %[1]T (%[1]v)", err)
122
-		}
123
-	})
124
-	t.Run("target dir is not a directory", func(t *testing.T) {
125
-		tmpDir := t.TempDir()
126
-		parentPath := filepath.Join(tmpDir, "not-a-dir")
127
-		err := os.WriteFile(parentPath, nil, testMode())
128
-		if err != nil {
129
-			t.Fatalf("Error writing file: %v", err)
130
-		}
131
-		fileName := filepath.Join(parentPath, "new-file.txt")
132
-		writer, err := New(fileName, testMode())
133
-		if writer != nil {
134
-			t.Errorf("Should not have created writer")
135
-		}
136
-		// This should match the behavior of os.WriteFile, which returns a [os.PathError] with [syscall.ENOTDIR].
137
-		if !errors.Is(err, syscall.ENOTDIR) {
138
-			t.Errorf("Should produce a 'not a directory' error, but got %[1]T (%[1]v)", err)
139
-		}
140
-	})
141
-	t.Run("empty filename", func(t *testing.T) {
142
-		writer, err := New("", testMode())
143
-		if writer != nil {
144
-			t.Errorf("Should not have created writer")
145
-		}
146
-		if err == nil || err.Error() != "file name is empty" {
147
-			t.Errorf("Should produce a 'file name is empty' error, but got %[1]T (%[1]v)", err)
148
-		}
149
-	})
150
-	t.Run("directory", func(t *testing.T) {
151
-		tmpDir := t.TempDir()
152
-		writer, err := New(tmpDir, testMode())
153
-		if writer != nil {
154
-			t.Errorf("Should not have created writer")
155
-		}
156
-		if err == nil || err.Error() != "cannot write to a directory" {
157
-			t.Errorf("Should produce a 'cannot write to a directory' error, but got %[1]T (%[1]v)", err)
158
-		}
159
-	})
160
-	t.Run("symlinked file", func(t *testing.T) {
161
-		tmpDir := t.TempDir()
162
-		linkTarget := filepath.Join(tmpDir, "symlink-target")
163
-		if err := os.WriteFile(linkTarget, []byte("orig content"), testMode()); err != nil {
164
-			t.Fatal(err)
165
-		}
166
-		fileName := filepath.Join(tmpDir, "symlinked-file")
167
-		if err := os.Symlink(linkTarget, fileName); err != nil {
168
-			t.Fatal(err)
169
-		}
170
-		writer, err := New(fileName, testMode())
171
-		if writer != nil {
172
-			t.Errorf("Should not have created writer")
173
-		}
174
-		if err == nil || err.Error() != "cannot write to a symbolic link directly" {
175
-			t.Errorf("Should produce a 'cannot write to a symbolic link directly' error, but got %[1]T (%[1]v)", err)
176
-		}
177
-	})
178
-}
179
-
180
-func TestWriteFile(t *testing.T) {
181
-	t.Run("empty filename", func(t *testing.T) {
182
-		err := WriteFile("", nil, testMode())
183
-		if err == nil || err.Error() != "file name is empty" {
184
-			t.Errorf("Should produce a 'file name is empty' error, but got %[1]T (%[1]v)", err)
185
-		}
186
-	})
187
-	t.Run("write to directory", func(t *testing.T) {
188
-		err := WriteFile(t.TempDir(), nil, testMode())
189
-		if err == nil || err.Error() != "cannot write to a directory" {
190
-			t.Errorf("Should produce a 'cannot write to a directory' error, but got %[1]T (%[1]v)", err)
191
-		}
192
-	})
193
-	t.Run("write to file", func(t *testing.T) {
194
-		tmpDir := t.TempDir()
195
-		fileName := filepath.Join(tmpDir, "test.txt")
196
-		fileContent := []byte("file content")
197
-		fileMode := testMode()
198
-		if err := WriteFile(fileName, fileContent, fileMode); err != nil {
199
-			t.Fatalf("Error writing to file: %v", err)
200
-		}
201
-		assertFile(t, fileName, fileContent, fileMode)
202
-		assertFileCount(t, tmpDir, 1)
203
-	})
204
-	t.Run("missing parent directory", func(t *testing.T) {
205
-		tmpDir := t.TempDir()
206
-		fileName := filepath.Join(tmpDir, "missing-dir", "test.txt")
207
-		fileContent := []byte("file content")
208
-		fileMode := testMode()
209
-		if err := WriteFile(fileName, fileContent, fileMode); !errors.Is(err, os.ErrNotExist) {
210
-			t.Errorf("Should produce a 'not found' error, but got %[1]T (%[1]v)", err)
211
-		}
212
-		assertFileCount(t, tmpDir, 0)
213
-	})
214
-	t.Run("symlinked file", func(t *testing.T) {
215
-		tmpDir := t.TempDir()
216
-		linkTarget := filepath.Join(tmpDir, "symlink-target")
217
-		originalContent := []byte("original content")
218
-		fileMode := testMode()
219
-		if err := os.WriteFile(linkTarget, originalContent, fileMode); err != nil {
220
-			t.Fatal(err)
221
-		}
222
-		if err := os.Symlink(linkTarget, filepath.Join(tmpDir, "symlinked-file")); err != nil {
223
-			t.Fatal(err)
224
-		}
225
-		origFileCount := 2
226
-		assertFileCount(t, tmpDir, origFileCount)
227
-
228
-		fileName := filepath.Join(tmpDir, "symlinked-file")
229
-		err := WriteFile(fileName, []byte("new content"), testMode())
230
-		if err == nil || err.Error() != "cannot write to a symbolic link directly" {
231
-			t.Errorf("Should produce a 'cannot write to a symbolic link directly' error, but got %[1]T (%[1]v)", err)
232
-		}
233
-		assertFile(t, linkTarget, originalContent, fileMode)
234
-		assertFileCount(t, tmpDir, origFileCount)
235
-	})
236
-	t.Run("symlinked directory", func(t *testing.T) {
237
-		tmpDir := t.TempDir()
238
-		actualParentDir := filepath.Join(tmpDir, "parent-dir")
239
-		if err := os.Mkdir(actualParentDir, 0o700); err != nil {
240
-			t.Fatal(err)
241
-		}
242
-		actualTargetFile := filepath.Join(actualParentDir, "target-file")
243
-		if err := os.WriteFile(actualTargetFile, []byte("orig content"), testMode()); err != nil {
244
-			t.Fatal(err)
245
-		}
246
-		parentDir := filepath.Join(tmpDir, "parent-dir-symlink")
247
-		if err := os.Symlink(actualParentDir, parentDir); err != nil {
248
-			t.Fatal(err)
249
-		}
250
-		origFileCount := 1
251
-		assertFileCount(t, actualParentDir, origFileCount)
252
-
253
-		fileName := filepath.Join(parentDir, "target-file")
254
-		fileContent := []byte("new content")
255
-		fileMode := testMode()
256
-		if err := WriteFile(fileName, fileContent, fileMode); err != nil {
257
-			t.Fatalf("Error writing to file: %v", err)
258
-		}
259
-		assertFile(t, fileName, fileContent, fileMode)
260
-		assertFile(t, actualTargetFile, fileContent, fileMode)
261
-		assertFileCount(t, actualParentDir, origFileCount)
262
-	})
263
-}
264
-
265
-func TestWriteSetCommit(t *testing.T) {
266
-	tmpDir := t.TempDir()
267
-
268
-	if err := os.Mkdir(filepath.Join(tmpDir, "tmp"), 0o700); err != nil {
269
-		t.Fatalf("Error creating tmp directory: %s", err)
270
-	}
271
-
272
-	targetDir := filepath.Join(tmpDir, "target")
273
-	ws, err := NewWriteSet(filepath.Join(tmpDir, "tmp"))
274
-	if err != nil {
275
-		t.Fatalf("Error creating atomic write set: %s", err)
276
-	}
277
-
278
-	fileContent := []byte("file content")
279
-	fileMode := testMode()
280
-
281
-	if err := ws.WriteFile("foo", fileContent, fileMode); err != nil {
282
-		t.Fatalf("Error writing to file: %v", err)
283
-	}
284
-
285
-	if _, err := os.ReadFile(filepath.Join(targetDir, "foo")); err == nil {
286
-		t.Fatalf("Expected error reading file where should not exist")
287
-	}
288
-
289
-	if err := ws.Commit(targetDir); err != nil {
290
-		t.Fatalf("Error committing file: %s", err)
291
-	}
292
-
293
-	assertFile(t, filepath.Join(targetDir, "foo"), fileContent, fileMode)
294
-	assertFileCount(t, targetDir, 1)
295
-}
296
-
297
-func TestWriteSetCancel(t *testing.T) {
298
-	tmpDir := t.TempDir()
299
-
300
-	if err := os.Mkdir(filepath.Join(tmpDir, "tmp"), 0o700); err != nil {
301
-		t.Fatalf("Error creating tmp directory: %s", err)
302
-	}
303
-
304
-	ws, err := NewWriteSet(filepath.Join(tmpDir, "tmp"))
305
-	if err != nil {
306
-		t.Fatalf("Error creating atomic write set: %s", err)
307
-	}
308
-
309
-	fileContent := []byte("file content")
310
-	fileMode := testMode()
311
-	if err := ws.WriteFile("foo", fileContent, fileMode); err != nil {
312
-		t.Fatalf("Error writing to file: %v", err)
313
-	}
314
-
315
-	if err := ws.Cancel(); err != nil {
316
-		t.Fatalf("Error committing file: %s", err)
317
-	}
318
-
319
-	if _, err := os.ReadFile(filepath.Join(tmpDir, "target", "foo")); err == nil {
320
-		t.Fatalf("Expected error reading file where should not exist")
321
-	} else if !errors.Is(err, os.ErrNotExist) {
322
-		t.Fatalf("Unexpected error reading file: %s", err)
323
-	}
324
-	assertFileCount(t, filepath.Join(tmpDir, "tmp"), 0)
325
-}
... ...
@@ -4,7 +4,7 @@ import (
4 4
 	"io"
5 5
 	"os"
6 6
 
7
-	"github.com/docker/docker/pkg/atomicwriter"
7
+	"github.com/moby/sys/atomicwriter"
8 8
 )
9 9
 
10 10
 // NewAtomicFileWriter returns WriteCloser so that writing to it writes to a
... ...
@@ -19,11 +19,11 @@ import (
19 19
 	"github.com/docker/docker/api/types/events"
20 20
 	"github.com/docker/docker/internal/containerfs"
21 21
 	"github.com/docker/docker/internal/lazyregexp"
22
-	"github.com/docker/docker/pkg/atomicwriter"
23 22
 	"github.com/docker/docker/pkg/authorization"
24 23
 	v2 "github.com/docker/docker/plugin/v2"
25 24
 	"github.com/docker/docker/registry"
26 25
 	"github.com/moby/pubsub"
26
+	"github.com/moby/sys/atomicwriter"
27 27
 	"github.com/opencontainers/go-digest"
28 28
 	"github.com/opencontainers/runtime-spec/specs-go"
29 29
 	"github.com/pkg/errors"
... ...
@@ -9,7 +9,7 @@ import (
9 9
 	"sync"
10 10
 
11 11
 	"github.com/distribution/reference"
12
-	"github.com/docker/docker/pkg/atomicwriter"
12
+	"github.com/moby/sys/atomicwriter"
13 13
 	"github.com/opencontainers/go-digest"
14 14
 	"github.com/pkg/errors"
15 15
 )
... ...
@@ -70,6 +70,7 @@ require (
70 70
 	github.com/moby/patternmatcher v0.6.0
71 71
 	github.com/moby/pubsub v1.0.0
72 72
 	github.com/moby/swarmkit/v2 v2.0.0-20250103191802-8c1959736554
73
+	github.com/moby/sys/atomicwriter v0.0.0-20250404210502-6e2523cbf3a1
73 74
 	github.com/moby/sys/mount v0.3.4
74 75
 	github.com/moby/sys/mountinfo v0.7.2
75 76
 	github.com/moby/sys/reexec v0.1.0
... ...
@@ -397,6 +397,8 @@ github.com/moby/pubsub v1.0.0 h1:jkp/imWsmJz2f6LyFsk7EkVeN2HxR/HTTOY8kHrsxfA=
397 397
 github.com/moby/pubsub v1.0.0/go.mod h1:bXSO+3h5MNXXCaEG+6/NlAIk7MMZbySZlnB+cUQhKKc=
398 398
 github.com/moby/swarmkit/v2 v2.0.0-20250103191802-8c1959736554 h1:DMHJbgyNZWyrPKYjCYt2IxEO7KA0eSd4fo6KQsv2W84=
399 399
 github.com/moby/swarmkit/v2 v2.0.0-20250103191802-8c1959736554/go.mod h1:mTTGIAz/59OGZR5Qe+QByIe3Nxc+sSuJkrsStFhr6Lg=
400
+github.com/moby/sys/atomicwriter v0.0.0-20250404210502-6e2523cbf3a1 h1:RfsCoQh4+GdgY8QQ7y05leVCa8niO1Phmy5CF3YiSgo=
401
+github.com/moby/sys/atomicwriter v0.0.0-20250404210502-6e2523cbf3a1/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
400 402
 github.com/moby/sys/mount v0.3.4 h1:yn5jq4STPztkkzSKpZkLcmjue+bZJ0u2AuQY1iNI1Ww=
401 403
 github.com/moby/sys/mount v0.3.4/go.mod h1:KcQJMbQdJHPlq5lcYT+/CjatWM4PuxKe+XLSVS4J6Os=
402 404
 github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
403 405
new file mode 100644
... ...
@@ -0,0 +1,202 @@
0
+
1
+                                 Apache License
2
+                           Version 2.0, January 2004
3
+                        http://www.apache.org/licenses/
4
+
5
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+   1. Definitions.
8
+
9
+      "License" shall mean the terms and conditions for use, reproduction,
10
+      and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+      "Licensor" shall mean the copyright owner or entity authorized by
13
+      the copyright owner that is granting the License.
14
+
15
+      "Legal Entity" shall mean the union of the acting entity and all
16
+      other entities that control, are controlled by, or are under common
17
+      control with that entity. For the purposes of this definition,
18
+      "control" means (i) the power, direct or indirect, to cause the
19
+      direction or management of such entity, whether by contract or
20
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+      outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+      "You" (or "Your") shall mean an individual or Legal Entity
24
+      exercising permissions granted by this License.
25
+
26
+      "Source" form shall mean the preferred form for making modifications,
27
+      including but not limited to software source code, documentation
28
+      source, and configuration files.
29
+
30
+      "Object" form shall mean any form resulting from mechanical
31
+      transformation or translation of a Source form, including but
32
+      not limited to compiled object code, generated documentation,
33
+      and conversions to other media types.
34
+
35
+      "Work" shall mean the work of authorship, whether in Source or
36
+      Object form, made available under the License, as indicated by a
37
+      copyright notice that is included in or attached to the work
38
+      (an example is provided in the Appendix below).
39
+
40
+      "Derivative Works" shall mean any work, whether in Source or Object
41
+      form, that is based on (or derived from) the Work and for which the
42
+      editorial revisions, annotations, elaborations, or other modifications
43
+      represent, as a whole, an original work of authorship. For the purposes
44
+      of this License, Derivative Works shall not include works that remain
45
+      separable from, or merely link (or bind by name) to the interfaces of,
46
+      the Work and Derivative Works thereof.
47
+
48
+      "Contribution" shall mean any work of authorship, including
49
+      the original version of the Work and any modifications or additions
50
+      to that Work or Derivative Works thereof, that is intentionally
51
+      submitted to Licensor for inclusion in the Work by the copyright owner
52
+      or by an individual or Legal Entity authorized to submit on behalf of
53
+      the copyright owner. For the purposes of this definition, "submitted"
54
+      means any form of electronic, verbal, or written communication sent
55
+      to the Licensor or its representatives, including but not limited to
56
+      communication on electronic mailing lists, source code control systems,
57
+      and issue tracking systems that are managed by, or on behalf of, the
58
+      Licensor for the purpose of discussing and improving the Work, but
59
+      excluding communication that is conspicuously marked or otherwise
60
+      designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+      "Contributor" shall mean Licensor and any individual or Legal Entity
63
+      on behalf of whom a Contribution has been received by Licensor and
64
+      subsequently incorporated within the Work.
65
+
66
+   2. Grant of Copyright License. Subject to the terms and conditions of
67
+      this License, each Contributor hereby grants to You a perpetual,
68
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+      copyright license to reproduce, prepare Derivative Works of,
70
+      publicly display, publicly perform, sublicense, and distribute the
71
+      Work and such Derivative Works in Source or Object form.
72
+
73
+   3. Grant of Patent License. Subject to the terms and conditions of
74
+      this License, each Contributor hereby grants to You a perpetual,
75
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+      (except as stated in this section) patent license to make, have made,
77
+      use, offer to sell, sell, import, and otherwise transfer the Work,
78
+      where such license applies only to those patent claims licensable
79
+      by such Contributor that are necessarily infringed by their
80
+      Contribution(s) alone or by combination of their Contribution(s)
81
+      with the Work to which such Contribution(s) was submitted. If You
82
+      institute patent litigation against any entity (including a
83
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+      or a Contribution incorporated within the Work constitutes direct
85
+      or contributory patent infringement, then any patent licenses
86
+      granted to You under this License for that Work shall terminate
87
+      as of the date such litigation is filed.
88
+
89
+   4. Redistribution. You may reproduce and distribute copies of the
90
+      Work or Derivative Works thereof in any medium, with or without
91
+      modifications, and in Source or Object form, provided that You
92
+      meet the following conditions:
93
+
94
+      (a) You must give any other recipients of the Work or
95
+          Derivative Works a copy of this License; and
96
+
97
+      (b) You must cause any modified files to carry prominent notices
98
+          stating that You changed the files; and
99
+
100
+      (c) You must retain, in the Source form of any Derivative Works
101
+          that You distribute, all copyright, patent, trademark, and
102
+          attribution notices from the Source form of the Work,
103
+          excluding those notices that do not pertain to any part of
104
+          the Derivative Works; and
105
+
106
+      (d) If the Work includes a "NOTICE" text file as part of its
107
+          distribution, then any Derivative Works that You distribute must
108
+          include a readable copy of the attribution notices contained
109
+          within such NOTICE file, excluding those notices that do not
110
+          pertain to any part of the Derivative Works, in at least one
111
+          of the following places: within a NOTICE text file distributed
112
+          as part of the Derivative Works; within the Source form or
113
+          documentation, if provided along with the Derivative Works; or,
114
+          within a display generated by the Derivative Works, if and
115
+          wherever such third-party notices normally appear. The contents
116
+          of the NOTICE file are for informational purposes only and
117
+          do not modify the License. You may add Your own attribution
118
+          notices within Derivative Works that You distribute, alongside
119
+          or as an addendum to the NOTICE text from the Work, provided
120
+          that such additional attribution notices cannot be construed
121
+          as modifying the License.
122
+
123
+      You may add Your own copyright statement to Your modifications and
124
+      may provide additional or different license terms and conditions
125
+      for use, reproduction, or distribution of Your modifications, or
126
+      for any such Derivative Works as a whole, provided Your use,
127
+      reproduction, and distribution of the Work otherwise complies with
128
+      the conditions stated in this License.
129
+
130
+   5. Submission of Contributions. Unless You explicitly state otherwise,
131
+      any Contribution intentionally submitted for inclusion in the Work
132
+      by You to the Licensor shall be under the terms and conditions of
133
+      this License, without any additional terms or conditions.
134
+      Notwithstanding the above, nothing herein shall supersede or modify
135
+      the terms of any separate license agreement you may have executed
136
+      with Licensor regarding such Contributions.
137
+
138
+   6. Trademarks. This License does not grant permission to use the trade
139
+      names, trademarks, service marks, or product names of the Licensor,
140
+      except as required for reasonable and customary use in describing the
141
+      origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+   7. Disclaimer of Warranty. Unless required by applicable law or
144
+      agreed to in writing, Licensor provides the Work (and each
145
+      Contributor provides its Contributions) on an "AS IS" BASIS,
146
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+      implied, including, without limitation, any warranties or conditions
148
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+      PARTICULAR PURPOSE. You are solely responsible for determining the
150
+      appropriateness of using or redistributing the Work and assume any
151
+      risks associated with Your exercise of permissions under this License.
152
+
153
+   8. Limitation of Liability. In no event and under no legal theory,
154
+      whether in tort (including negligence), contract, or otherwise,
155
+      unless required by applicable law (such as deliberate and grossly
156
+      negligent acts) or agreed to in writing, shall any Contributor be
157
+      liable to You for damages, including any direct, indirect, special,
158
+      incidental, or consequential damages of any character arising as a
159
+      result of this License or out of the use or inability to use the
160
+      Work (including but not limited to damages for loss of goodwill,
161
+      work stoppage, computer failure or malfunction, or any and all
162
+      other commercial damages or losses), even if such Contributor
163
+      has been advised of the possibility of such damages.
164
+
165
+   9. Accepting Warranty or Additional Liability. While redistributing
166
+      the Work or Derivative Works thereof, You may choose to offer,
167
+      and charge a fee for, acceptance of support, warranty, indemnity,
168
+      or other liability obligations and/or rights consistent with this
169
+      License. However, in accepting such obligations, You may act only
170
+      on Your own behalf and on Your sole responsibility, not on behalf
171
+      of any other Contributor, and only if You agree to indemnify,
172
+      defend, and hold each Contributor harmless for any liability
173
+      incurred by, or claims asserted against, such Contributor by reason
174
+      of your accepting any such warranty or additional liability.
175
+
176
+   END OF TERMS AND CONDITIONS
177
+
178
+   APPENDIX: How to apply the Apache License to your work.
179
+
180
+      To apply the Apache License to your work, attach the following
181
+      boilerplate notice, with the fields enclosed by brackets "[]"
182
+      replaced with your own identifying information. (Don't include
183
+      the brackets!)  The text should be enclosed in the appropriate
184
+      comment syntax for the file format. We also recommend that a
185
+      file or class name and description of purpose be included on the
186
+      same "printed page" as the copyright notice for easier
187
+      identification within third-party archives.
188
+
189
+   Copyright [yyyy] [name of copyright owner]
190
+
191
+   Licensed under the Apache License, Version 2.0 (the "License");
192
+   you may not use this file except in compliance with the License.
193
+   You may obtain a copy of the License at
194
+
195
+       http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+   Unless required by applicable law or agreed to in writing, software
198
+   distributed under the License is distributed on an "AS IS" BASIS,
199
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+   See the License for the specific language governing permissions and
201
+   limitations under the License.
0 202
new file mode 100644
... ...
@@ -0,0 +1,245 @@
0
+// Package atomicwriter provides utilities to perform atomic writes to a
1
+// file or set of files.
2
+package atomicwriter
3
+
4
+import (
5
+	"errors"
6
+	"fmt"
7
+	"io"
8
+	"os"
9
+	"path/filepath"
10
+	"syscall"
11
+
12
+	"github.com/moby/sys/sequential"
13
+)
14
+
15
+func validateDestination(fileName string) error {
16
+	if fileName == "" {
17
+		return errors.New("file name is empty")
18
+	}
19
+	if dir := filepath.Dir(fileName); dir != "" && dir != "." && dir != ".." {
20
+		di, err := os.Stat(dir)
21
+		if err != nil {
22
+			return fmt.Errorf("invalid output path: %w", err)
23
+		}
24
+		if !di.IsDir() {
25
+			return fmt.Errorf("invalid output path: %w", &os.PathError{Op: "stat", Path: dir, Err: syscall.ENOTDIR})
26
+		}
27
+	}
28
+
29
+	// Deliberately using Lstat here to match the behavior of [os.Rename],
30
+	// which is used when completing the write and does not resolve symlinks.
31
+	fi, err := os.Lstat(fileName)
32
+	if err != nil {
33
+		if os.IsNotExist(err) {
34
+			return nil
35
+		}
36
+		return fmt.Errorf("failed to stat output path: %w", err)
37
+	}
38
+
39
+	switch mode := fi.Mode(); {
40
+	case mode.IsRegular():
41
+		return nil // Regular file
42
+	case mode&os.ModeDir != 0:
43
+		return errors.New("cannot write to a directory")
44
+	case mode&os.ModeSymlink != 0:
45
+		return errors.New("cannot write to a symbolic link directly")
46
+	case mode&os.ModeNamedPipe != 0:
47
+		return errors.New("cannot write to a named pipe (FIFO)")
48
+	case mode&os.ModeSocket != 0:
49
+		return errors.New("cannot write to a socket")
50
+	case mode&os.ModeDevice != 0:
51
+		if mode&os.ModeCharDevice != 0 {
52
+			return errors.New("cannot write to a character device file")
53
+		}
54
+		return errors.New("cannot write to a block device file")
55
+	case mode&os.ModeSetuid != 0:
56
+		return errors.New("cannot write to a setuid file")
57
+	case mode&os.ModeSetgid != 0:
58
+		return errors.New("cannot write to a setgid file")
59
+	case mode&os.ModeSticky != 0:
60
+		return errors.New("cannot write to a sticky bit file")
61
+	default:
62
+		return fmt.Errorf("unknown file mode: %[1]s (%#[1]o)", mode)
63
+	}
64
+}
65
+
66
+// New returns a WriteCloser so that writing to it writes to a
67
+// temporary file and closing it atomically changes the temporary file to
68
+// destination path. Writing and closing concurrently is not allowed.
69
+// NOTE: umask is not considered for the file's permissions.
70
+//
71
+// New uses [sequential.CreateTemp] to use sequential file access on Windows,
72
+// avoiding depleting the standby list un-necessarily. On Linux, this equates to
73
+// a regular [os.CreateTemp]. Refer to the [Win32 API documentation] for details
74
+// on sequential file access.
75
+//
76
+// [Win32 API documentation]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN
77
+func New(filename string, perm os.FileMode) (io.WriteCloser, error) {
78
+	if err := validateDestination(filename); err != nil {
79
+		return nil, err
80
+	}
81
+	abspath, err := filepath.Abs(filename)
82
+	if err != nil {
83
+		return nil, err
84
+	}
85
+
86
+	f, err := sequential.CreateTemp(filepath.Dir(abspath), ".tmp-"+filepath.Base(filename))
87
+	if err != nil {
88
+		return nil, err
89
+	}
90
+	return &atomicFileWriter{
91
+		f:    f,
92
+		fn:   abspath,
93
+		perm: perm,
94
+	}, nil
95
+}
96
+
97
+// WriteFile atomically writes data to a file named by filename and with the
98
+// specified permission bits. The given filename is created if it does not exist,
99
+// but the destination directory must exist. It can be used as a drop-in replacement
100
+// for [os.WriteFile], but currently does not allow the destination path to be
101
+// a symlink. WriteFile is implemented using [New] for its implementation.
102
+//
103
+// NOTE: umask is not considered for the file's permissions.
104
+func WriteFile(filename string, data []byte, perm os.FileMode) error {
105
+	f, err := New(filename, perm)
106
+	if err != nil {
107
+		return err
108
+	}
109
+	n, err := f.Write(data)
110
+	if err == nil && n < len(data) {
111
+		err = io.ErrShortWrite
112
+		f.(*atomicFileWriter).writeErr = err
113
+	}
114
+	if err1 := f.Close(); err == nil {
115
+		err = err1
116
+	}
117
+	return err
118
+}
119
+
120
+type atomicFileWriter struct {
121
+	f        *os.File
122
+	fn       string
123
+	writeErr error
124
+	written  bool
125
+	perm     os.FileMode
126
+}
127
+
128
+func (w *atomicFileWriter) Write(dt []byte) (int, error) {
129
+	w.written = true
130
+	n, err := w.f.Write(dt)
131
+	if err != nil {
132
+		w.writeErr = err
133
+	}
134
+	return n, err
135
+}
136
+
137
+func (w *atomicFileWriter) Close() (retErr error) {
138
+	defer func() {
139
+		if err := os.Remove(w.f.Name()); !errors.Is(err, os.ErrNotExist) && retErr == nil {
140
+			retErr = err
141
+		}
142
+	}()
143
+	if err := w.f.Sync(); err != nil {
144
+		_ = w.f.Close()
145
+		return err
146
+	}
147
+	if err := w.f.Close(); err != nil {
148
+		return err
149
+	}
150
+	if err := os.Chmod(w.f.Name(), w.perm); err != nil {
151
+		return err
152
+	}
153
+	if w.writeErr == nil && w.written {
154
+		return os.Rename(w.f.Name(), w.fn)
155
+	}
156
+	return nil
157
+}
158
+
159
+// WriteSet is used to atomically write a set
160
+// of files and ensure they are visible at the same time.
161
+// Must be committed to a new directory.
162
+type WriteSet struct {
163
+	root string
164
+}
165
+
166
+// NewWriteSet creates a new atomic write set to
167
+// atomically create a set of files. The given directory
168
+// is used as the base directory for storing files before
169
+// commit. If no temporary directory is given the system
170
+// default is used.
171
+func NewWriteSet(tmpDir string) (*WriteSet, error) {
172
+	td, err := os.MkdirTemp(tmpDir, "write-set-")
173
+	if err != nil {
174
+		return nil, err
175
+	}
176
+
177
+	return &WriteSet{
178
+		root: td,
179
+	}, nil
180
+}
181
+
182
+// WriteFile writes a file to the set, guaranteeing the file
183
+// has been synced.
184
+func (ws *WriteSet) WriteFile(filename string, data []byte, perm os.FileMode) error {
185
+	f, err := ws.FileWriter(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
186
+	if err != nil {
187
+		return err
188
+	}
189
+	n, err := f.Write(data)
190
+	if err == nil && n < len(data) {
191
+		err = io.ErrShortWrite
192
+	}
193
+	if err1 := f.Close(); err == nil {
194
+		err = err1
195
+	}
196
+	return err
197
+}
198
+
199
+type syncFileCloser struct {
200
+	*os.File
201
+}
202
+
203
+func (w syncFileCloser) Close() error {
204
+	err := w.File.Sync()
205
+	if err1 := w.File.Close(); err == nil {
206
+		err = err1
207
+	}
208
+	return err
209
+}
210
+
211
+// FileWriter opens a file writer inside the set. The file
212
+// should be synced and closed before calling commit.
213
+//
214
+// FileWriter uses [sequential.OpenFile] to use sequential file access on Windows,
215
+// avoiding depleting the standby list un-necessarily. On Linux, this equates to
216
+// a regular [os.OpenFile]. Refer to the [Win32 API documentation] for details
217
+// on sequential file access.
218
+//
219
+// [Win32 API documentation]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN
220
+func (ws *WriteSet) FileWriter(name string, flag int, perm os.FileMode) (io.WriteCloser, error) {
221
+	f, err := sequential.OpenFile(filepath.Join(ws.root, name), flag, perm)
222
+	if err != nil {
223
+		return nil, err
224
+	}
225
+	return syncFileCloser{f}, nil
226
+}
227
+
228
+// Cancel cancels the set and removes all temporary data
229
+// created in the set.
230
+func (ws *WriteSet) Cancel() error {
231
+	return os.RemoveAll(ws.root)
232
+}
233
+
234
+// Commit moves all created files to the target directory. The
235
+// target directory must not exist and the parent of the target
236
+// directory must exist.
237
+func (ws *WriteSet) Commit(target string) error {
238
+	return os.Rename(ws.root, target)
239
+}
240
+
241
+// String returns the location the set is writing to.
242
+func (ws *WriteSet) String() string {
243
+	return ws.root
244
+}
... ...
@@ -1007,6 +1007,9 @@ github.com/moby/swarmkit/v2/volumequeue
1007 1007
 github.com/moby/swarmkit/v2/watch
1008 1008
 github.com/moby/swarmkit/v2/watch/queue
1009 1009
 github.com/moby/swarmkit/v2/xnet
1010
+# github.com/moby/sys/atomicwriter v0.0.0-20250404210502-6e2523cbf3a1
1011
+## explicit; go 1.18
1012
+github.com/moby/sys/atomicwriter
1010 1013
 # github.com/moby/sys/mount v0.3.4
1011 1014
 ## explicit; go 1.17
1012 1015
 github.com/moby/sys/mount
... ...
@@ -16,10 +16,10 @@ import (
16 16
 	"github.com/containerd/log"
17 17
 	"github.com/docker/docker/daemon/names"
18 18
 	"github.com/docker/docker/errdefs"
19
-	"github.com/docker/docker/pkg/atomicwriter"
20 19
 	"github.com/docker/docker/pkg/idtools"
21 20
 	"github.com/docker/docker/quota"
22 21
 	"github.com/docker/docker/volume"
22
+	"github.com/moby/sys/atomicwriter"
23 23
 	"github.com/pkg/errors"
24 24
 )
25 25