// MsgSyncReq: copy files to the remote host
-// Copyright (C) 2021 Simon Ruderich
+// Copyright (C) 2021-2023 Simon Ruderich
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//go:build !windows
// +build !windows
package sync
// openReadonlyFlags are flags for open* syscalls to safely read a file or
// directory.
//
-// O_NOFOLLOW prevents symlink attacks
+// O_NOFOLLOW prevents symlink attacks in the last path component
// O_NONBLOCK is necessary to prevent blocking on FIFOs
const openReadonlyFlags = unix.O_RDONLY | unix.O_NOFOLLOW | unix.O_NONBLOCK
func (s *Sync) syncFile(file *safcm.File, changed *bool) error {
// The general strategy is "update by rename": If any property of a
- // file changes it will be written to a temporary file and then
- // renamed "over" the original file. This is simple and prevents race
- // conditions where the file is partially readable while changes to
- // permissions or owner/group are applied. However, this strategy does
- // not work for directories which must be removed first (was
- // directory), must remove the existing file (will be directory) or
- // must be directly modified (changed permissions or owner). In the
+ // file changes the new version will be written to a temporary file
+ // and then renamed "over" the original file. This is simple and
+ // prevents race conditions where the file is partially readable while
+ // changes to permissions or owner/group are applied. However, this
+ // strategy does not work for directories which must be removed first
+ // (was directory), must remove the existing file (will be directory)
+ // or must be directly modified (changed permissions or owner). In the
// first two cases the old path is removed. In the last the directory
// is modified (carefully) in place.
//
parentFd, baseName, err := OpenParentDirectoryNoSymlinks(file.Path)
if err != nil {
+ if os.IsNotExist(err) && s.req.DryRun {
+ change.Created = true
+ debugf("will create (parent missing)")
+ *changed = true
+ debugf("dry-run, skipping changes")
+ s.resp.FileChanges = append(s.resp.FileChanges, change)
+ return nil
+ }
return err
}
defer unix.Close(parentFd)
// Some BSD systems permit changing permissions of
// symlinks but ignore them on traversal. To keep it
// simple we don't support that and always use 0777
- // for symlink permissions (the value on GNU/Linux).
+ // for symlink permissions (the value on GNU/Linux)
+ // when comparing. The actual permissions on the file
+ // system might be different on BSD systems.
//
// TODO: Add proper support for symlinks on BSD
change.Old.Mode |= 0777
}
// Compare file content (if possible)
- switch change.Old.Mode.Type() {
+ switch change.Old.Mode.Type() { //nolint:exhaustive
case 0: // regular file
x, err := io.ReadAll(oldFh)
if err != nil {
// (accidentally) replacing a directory tree with a file.
const msg = "will not replace non-empty directory, " +
"please remove manually"
- err := unix.Unlinkat(parentFd, baseName, 0)
+ err := unix.Unlinkat(parentFd, baseName, 0 /* flags */)
if err != nil && !os.IsNotExist(err) {
err2 := unix.Unlinkat(parentFd, baseName,
- AT_REMOVEDIR)
+ unix.AT_REMOVEDIR)
if err2 != nil && !os.IsNotExist(err2) {
// See src/os/file_unix.go in Go's sources
if err2 == unix.ENOTDIR {
// the error when the user tries to access this
// directory (access for the group will fail though).
mode := change.Old.Mode & fs.ModePerm & 0700
+ // Retain setgid/sticky so that the behavior does not
+ // change when creating and removing files.
+ mode |= change.Old.Mode & fs.ModeSetgid
+ mode |= change.Old.Mode & fs.ModeSticky
debugf("chmodding %#o (temporary)", mode)
err := oldFh.Chmod(mode)
if err != nil {
err = unix.Fchownat(parentFd, tmpBase, file.Uid, file.Gid,
unix.AT_SYMLINK_NOFOLLOW)
if err != nil {
- unix.Unlinkat(parentFd, tmpBase, 0)
+ unix.Unlinkat(parentFd, tmpBase, 0 /* flags */) //nolint:errcheck
return err
}
// Permissions are irrelevant for symlinks (on most systems)
debugf("renaming %q", slashpath.Join(dir, tmpBase))
err = unix.Renameat(parentFd, tmpBase, parentFd, baseName)
if err != nil {
- unix.Unlinkat(parentFd, tmpBase, 0)
+ unix.Unlinkat(parentFd, tmpBase, 0 /* flags */) //nolint:errcheck
return err
}
+ // To guarantee durability fsync must be called on a parent directory
+ // after adding, renaming or removing files therein.
+ //
+ // Calling sync on the files itself is not enough according to POSIX;
+ // man 2 fsync: "Calling fsync() does not necessarily ensure that the
+ // entry in the directory containing the file has also reached disk.
+ // For that an explicit fsync() on a file descriptor for the directory
+ // is also needed."
err = unix.Fsync(parentFd)
if err != nil {
return err
}
dir = ".."
parts = []string{filepath.Base(wd)}
- } else if parts[0] != "." {
+ } else {
// Relative path: start at the current directory
dir = "."
+ if parts[0] == "." {
+ parts = parts[1:]
+ }
}
- dirFd, err := unix.Openat(unix.AT_FDCWD, dir, openReadonlyFlags, 0)
+ dirFd, err := unix.Openat(unix.AT_FDCWD, dir,
+ openReadonlyFlags, 0 /* mode */)
if err != nil {
return -1, "", err
}
}
func OpenAtNoFollow(dirFd int, base string) (*os.File, error) {
- fd, err := unix.Openat(dirFd, base, openReadonlyFlags, 0)
+ fd, err := unix.Openat(dirFd, base, openReadonlyFlags, 0 /* mode */)
if err != nil {
return nil, err
}
_, err = fh.Write(data)
if err != nil {
fh.Close()
- unix.Unlinkat(dirFd, tmpBase, 0)
+ unix.Unlinkat(dirFd, tmpBase, 0 /* flags */) //nolint:errcheck
return "", err
}
// createTempAt() creates the file with 0600
err = fh.Chown(uid, gid)
if err != nil {
fh.Close()
- unix.Unlinkat(dirFd, tmpBase, 0)
+ unix.Unlinkat(dirFd, tmpBase, 0) //nolint:errcheck
return "", err
}
err = fh.Chmod(mode)
if err != nil {
fh.Close()
- unix.Unlinkat(dirFd, tmpBase, 0)
+ unix.Unlinkat(dirFd, tmpBase, 0) //nolint:errcheck
return "", err
}
err = fh.Sync()
if err != nil {
fh.Close()
- unix.Unlinkat(dirFd, tmpBase, 0)
+ unix.Unlinkat(dirFd, tmpBase, 0) //nolint:errcheck
return "", err
}
err = fh.Close()
if err != nil {
- unix.Unlinkat(dirFd, tmpBase, 0)
+ unix.Unlinkat(dirFd, tmpBase, 0) //nolint:errcheck
return "", err
}
return os.NewFile(uintptr(fd), ""), tmpBase, nil
}
-
-// SyncPath syncs path, which should be a directory. To guarantee durability
-// it must be called on a parent directory after adding, renaming or removing
-// files therein.
-//
-// Calling sync on the files itself is not enough according to POSIX; man 2
-// fsync: "Calling fsync() does not necessarily ensure that the entry in the
-// directory containing the file has also reached disk. For that an explicit
-// fsync() on a file descriptor for the directory is also needed."
-func SyncPath(path string) error {
- x, err := os.Open(path)
- if err != nil {
- return err
- }
- err = x.Sync()
- closeErr := x.Close()
- if err != nil {
- return err
- }
- return closeErr
-}