From 8b217d531305433df01c789dab4dc81d58f05eba Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@duempel.org>
Date: Mon, 5 Jan 2015 20:24:59 +0100
Subject: fs/io/FileOutputStream: use O_TMPFILE if available

The Linux feature allows writing new files to an invisible file, and
then replace the old file.  This preserves the old file if we get
interrupted by some event.
---
 NEWS                           |  1 +
 configure.ac                   |  2 +-
 src/fs/io/FileOutputStream.cxx | 66 +++++++++++++++++++++++++++++++++++++-----
 src/fs/io/FileOutputStream.hxx |  8 +++++
 4 files changed, 69 insertions(+), 8 deletions(-)

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 <unistd.h>
 #include <errno.h>
 
+#ifdef HAVE_LINKAT
+#ifndef O_TMPFILE
+/* supported since Linux 3.11 */
+#define __O_TMPFILE 020000000
+#define O_TMPFILE (__O_TMPFILE | O_DIRECTORY)
+#include <stdio.h>
+#endif
+
+/**
+ * Open a file using Linux's O_TMPFILE for writing the given file.
+ */
+static int
+OpenTempFile(Path path)
+{
+	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),
-	 fd(OpenFile(path,
-		     O_WRONLY|O_CREAT|O_TRUNC,
-		     0666))
+	:path(_path)
 {
-	if (fd < 0)
-		error.FormatErrno("Failed to create %s", path.c_str());
+#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);
 
-- 
cgit v1.2.3