Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
| ... | ... |
@@ -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" |
| ... | ... |
@@ -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" |
| ... | ... |
@@ -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 |
) |
| ... | ... |
@@ -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 |
) |
| 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 |
-} |
| ... | ... |
@@ -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" |
| ... | ... |
@@ -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 |
|