// Copyright (C) 2015 The Syncthing Authors. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at https://mozilla.org/MPL/2.0/. package osutil import ( "errors" "path/filepath" "github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/fs" ) var ( ErrClosed = errors.New("write to closed writer") TempPrefix = ".syncthing.tmp." ) // An AtomicWriter is an *os.File that writes to a temporary file in the same // directory as the final path. On successful Close the file is renamed to // its final path. Any error on Write or during Close is accumulated and // returned on Close, so a lazy user can ignore errors until Close. type AtomicWriter struct { path string next fs.File fs fs.Filesystem err error } // CreateAtomic is like os.Create, except a temporary file name is used // instead of the given name. The file is created with secure (0600) // permissions. func CreateAtomic(path string) (*AtomicWriter, error) { fs := fs.NewFilesystem(fs.FilesystemTypeBasic, filepath.Dir(path)) return CreateAtomicFilesystem(fs, filepath.Base(path)) } // CreateAtomicFilesystem is like os.Create, except a temporary file name is used // instead of the given name. The file is created with secure (0600) // permissions. func CreateAtomicFilesystem(filesystem fs.Filesystem, path string) (*AtomicWriter, error) { // The security of this depends on the tempfile having secure // permissions, 0600, from the beginning. This is what os.CreateTemp // does. We have a test that verifies that that is the case, should this // ever change in the standard library in the future. fd, err := TempFile(filesystem, filepath.Dir(path), TempPrefix) if err != nil { return nil, err } w := &AtomicWriter{ path: path, next: fd, fs: filesystem, } return w, nil } // Write is like io.Writer, but is a no-op on an already failed AtomicWriter. func (w *AtomicWriter) Write(bs []byte) (int, error) { if w.err != nil { return 0, w.err } n, err := w.next.Write(bs) if err != nil { w.err = err w.next.Close() } return n, err } // Close closes the temporary file and renames it to the final path. It is // invalid to call Write() or Close() after Close(). func (w *AtomicWriter) Close() error { if w.err != nil { return w.err } // Try to not leave temp file around, but ignore error. defer w.fs.Remove(w.next.Name()) // sync() isn't supported everywhere, our best effort will suffice. _ = w.next.Sync() if err := w.next.Close(); err != nil { w.err = err return err } info, infoErr := w.fs.Lstat(w.path) if infoErr != nil && !fs.IsNotExist(infoErr) { w.err = infoErr return infoErr } err := w.fs.Rename(w.next.Name(), w.path) if build.IsWindows && fs.IsPermission(err) { // On Windows, we might not be allowed to rename over the file // because it's read-only. Get us some write permissions and try // again. _ = w.fs.Chmod(w.path, 0o644) err = w.fs.Rename(w.next.Name(), w.path) } if err != nil { w.err = err return err } if infoErr == nil { // Restore chmod setting for final file to what it was if err := w.fs.Chmod(w.path, info.Mode()); err != nil { // Only fail if permissions differ, since some filesystems are expected to not allow chmod (e.g. error // `operation not permitted`). infoAfterRename, infoAfterRenameErr := w.fs.Lstat(w.path) if infoAfterRenameErr != nil || infoAfterRename.Mode() != info.Mode() { w.err = err return err } } } // fsync the directory too if fd, err := w.fs.Open(filepath.Dir(w.next.Name())); err == nil { fd.Sync() fd.Close() } // Set w.err to return appropriately for any future operations. w.err = ErrClosed return nil }