fd/atomic: Better separate this orthogonal functionality

Nothing is using the old interface, so we can move it for better
separation of responisbilities.

Change-Id: I3b6e429b106aa22e58df25e0801b60af0fbd6d70
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/src/fd/atomic.cpp b/src/fd/atomic.cpp
new file mode 100644
index 0000000..df200e6
--- /dev/null
+++ b/src/fd/atomic.cpp
@@ -0,0 +1,125 @@
+#include <cstdlib>
+#include <filesystem>
+#include <fmt/format.h>
+#include <stdplus/fd/atomic.hpp>
+#include <stdplus/fd/managed.hpp>
+#include <stdplus/util/cexec.hpp>
+#include <sys/stat.h>
+#include <system_error>
+#include <utility>
+
+namespace stdplus
+{
+namespace fd
+{
+
+static std::string makeTmpName(const std::filesystem::path& filename)
+{
+    auto name = filename.filename();
+    auto path =
+        filename.parent_path() / fmt::format(".{}.XXXXXX", name.native());
+    return path.native();
+}
+
+static int mktemp(std::string& tmpl)
+{
+    mode_t old = umask(0077);
+    int fd = mkstemp(tmpl.data());
+    umask(old);
+    return CHECK_ERRNO(fd, [&](int error) {
+        throw std::system_error(error, std::generic_category(),
+                                fmt::format("mkstemp({})", tmpl));
+    });
+}
+
+AtomicWriter::AtomicWriter(const std::filesystem::path& filename, int mode,
+                           std::string_view tmpl) :
+    filename(filename),
+    mode(mode),
+    tmpname(!tmpl.empty() ? std::string(tmpl) : makeTmpName(filename)),
+    fd(mktemp(tmpname))
+{
+}
+
+AtomicWriter::AtomicWriter(AtomicWriter&& other) :
+    filename(std::move(other.filename)), mode(other.mode),
+    tmpname(std::move(other.tmpname)), fd(std::move(other.fd))
+{
+    // We don't want to cleanup twice
+    other.tmpname.clear();
+}
+
+AtomicWriter& AtomicWriter::operator=(AtomicWriter&& other)
+{
+    if (this != &other)
+    {
+        filename = std::move(other.filename);
+        mode = other.mode;
+        tmpname = std::move(other.tmpname);
+        fd = std::move(other.fd);
+
+        // We don't want to cleanup twice
+        other.tmpname.clear();
+    }
+    return *this;
+}
+
+AtomicWriter::~AtomicWriter()
+{
+    cleanup();
+}
+
+void AtomicWriter::commit(bool allow_copy)
+{
+    try
+    {
+        CHECK_ERRNO(fsync(get()), "fsync");
+        CHECK_ERRNO(fchmod(get(), mode), "fchmod");
+        // We want the file to be closed before renaming it
+        {
+            auto ifd = std::move(fd);
+        }
+        try
+        {
+            std::filesystem::rename(tmpname, filename);
+            tmpname.clear();
+        }
+        catch (const std::filesystem::filesystem_error& e)
+        {
+            if (!allow_copy || e.code() != std::errc::cross_device_link)
+            {
+                throw;
+            }
+            std::filesystem::copy(tmpname, filename);
+        }
+    }
+    catch (...)
+    {
+        // We do this to ensure that any crashes that might happen before the
+        // destructor don't cause us to leave stale files.
+        cleanup();
+        throw;
+    }
+}
+
+int AtomicWriter::get() const
+{
+    return fd.get();
+}
+
+void AtomicWriter::cleanup() noexcept
+{
+    if (!tmpname.empty())
+    {
+        // Ensure the FD is closed prior to removing the file
+        {
+            auto ifd = std::move(fd);
+        }
+        std::error_code ec;
+        std::filesystem::remove(tmpname, ec);
+        tmpname.clear();
+    }
+}
+
+} // namespace fd
+} // namespace stdplus