diff --git a/NEWS b/NEWS index 28d22722d..d70b04690 100644 --- a/NEWS +++ b/NEWS @@ -18,6 +18,7 @@ ver 0.20 (not yet released) * mixer - null: new plugin * reset song priority on playback +* write database and state file atomically * remove dependency on GLib ver 0.19.8 (not yet released) diff --git a/configure.ac b/configure.ac index 79a4952b0..94ce62e73 100644 --- a/configure.ac +++ b/configure.ac @@ -219,7 +219,7 @@ AC_SEARCH_LIBS([socket], [socket]) AC_SEARCH_LIBS([gethostbyname], [nsl]) if test x$host_is_linux = xyes; then - AC_CHECK_FUNCS(pipe2 accept4) + AC_CHECK_FUNCS(pipe2 accept4 linkat) fi AC_CHECK_FUNCS(getpwnam_r getpwuid_r) diff --git a/src/fs/io/FileOutputStream.cxx b/src/fs/io/FileOutputStream.cxx index 2951149b9..7a6416557 100644 --- a/src/fs/io/FileOutputStream.cxx +++ b/src/fs/io/FileOutputStream.cxx @@ -79,14 +79,47 @@ FileOutputStream::Cancel() #include #include -FileOutputStream::FileOutputStream(Path _path, Error &error) - :path(_path), - fd(OpenFile(path, - O_WRONLY|O_CREAT|O_TRUNC, - 0666)) +#ifdef HAVE_LINKAT +#ifndef O_TMPFILE +/* supported since Linux 3.11 */ +#define __O_TMPFILE 020000000 +#define O_TMPFILE (__O_TMPFILE | O_DIRECTORY) +#include +#endif + +/** + * Open a file using Linux's O_TMPFILE for writing the given file. + */ +static int +OpenTempFile(Path path) { - if (fd < 0) - error.FormatErrno("Failed to create %s", path.c_str()); + const auto directory = path.GetDirectoryName(); + if (directory.IsNull()) + return -1; + + return OpenFile(directory, O_TMPFILE|O_WRONLY, 0666); +} + +#endif /* HAVE_LINKAT */ + +FileOutputStream::FileOutputStream(Path _path, Error &error) + :path(_path) +{ +#ifdef HAVE_LINKAT + /* try Linux's O_TMPFILE first */ + fd = OpenTempFile(path); + is_tmpfile = fd >= 0; + if (!is_tmpfile) { +#endif + /* fall back to plain POSIX */ + fd = OpenFile(path, + O_WRONLY|O_CREAT|O_TRUNC, + 0666); + if (fd < 0) + error.FormatErrno("Failed to create %s", path.c_str()); +#ifdef HAVE_LINKAT + } +#endif } bool @@ -112,6 +145,22 @@ FileOutputStream::Commit(Error &error) { assert(IsDefined()); +#if HAVE_LINKAT + if (is_tmpfile) { + RemoveFile(path); + + /* hard-link the temporary file to the final path */ + char fd_path[64]; + snprintf(fd_path, sizeof(fd_path), "/proc/self/fd/%d", fd); + if (linkat(AT_FDCWD, fd_path, AT_FDCWD, path.c_str(), + AT_SYMLINK_FOLLOW) < 0) { + error.FormatErrno("Failed to commit %s", path.c_str()); + close(fd); + return false; + } + } +#endif + bool success = close(fd) == 0; fd = -1; if (!success) @@ -128,7 +177,10 @@ FileOutputStream::Cancel() close(fd); fd = -1; - RemoveFile(path); +#ifdef HAVE_LINKAT + if (!is_tmpfile) +#endif + RemoveFile(path); } #endif diff --git a/src/fs/io/FileOutputStream.hxx b/src/fs/io/FileOutputStream.hxx index 5ac2f9e15..03d062134 100644 --- a/src/fs/io/FileOutputStream.hxx +++ b/src/fs/io/FileOutputStream.hxx @@ -42,6 +42,14 @@ class FileOutputStream final : public OutputStream { int fd; #endif +#ifdef HAVE_LINKAT + /** + * Was O_TMPFILE used? If yes, then linkat() must be used to + * create a link to this file. + */ + bool is_tmpfile; +#endif + public: FileOutputStream(Path _path, Error &error);