From 0c98d93e9acddc525703153e35f02ff117bfd520 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Thu, 12 May 2022 19:26:49 +0200 Subject: [PATCH] io/FileOutputStream: write to temporary file if O_TMPFILE is not available --- src/io/FileOutputStream.cxx | 82 +++++++++++++++++++++++++++++++++++-- src/io/FileOutputStream.hxx | 10 ++++- test/net/meson.build | 1 + 3 files changed, 88 insertions(+), 5 deletions(-) diff --git a/src/io/FileOutputStream.cxx b/src/io/FileOutputStream.cxx index a7cd50fe3..f25adf29a 100644 --- a/src/io/FileOutputStream.cxx +++ b/src/io/FileOutputStream.cxx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014-2018 Max Kellermann + * Copyright 2014-2022 Max Kellermann * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -31,6 +31,10 @@ #include "system/Error.hxx" #include "util/StringFormat.hxx" +#ifdef _WIN32 +#include +#endif + #ifdef __linux__ #include #endif @@ -81,8 +85,22 @@ FileOutputStream::Open() #ifdef _WIN32 inline void -FileOutputStream::OpenCreate([[maybe_unused]] bool visible) +FileOutputStream::OpenCreate(bool visible) { + if (!visible) { + /* attempt to create a temporary file */ + tmp_path = path.WithSuffix(_T(".tmp")); + Delete(tmp_path); + + handle = CreateFile(tmp_path.c_str(), GENERIC_WRITE, 0, nullptr, + CREATE_NEW, + FILE_ATTRIBUTE_NORMAL|FILE_FLAG_WRITE_THROUGH, + nullptr); + if (handle != INVALID_HANDLE_VALUE) + return; + + } + handle = CreateFile(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL|FILE_FLAG_WRITE_THROUGH, @@ -149,10 +167,17 @@ FileOutputStream::Sync() void FileOutputStream::Commit() -{ +try { assert(IsDefined()); Close(); + + if (tmp_path != nullptr) + RenameOrThrow(tmp_path, path); +} catch (...) { + if (tmp_path != nullptr) + Delete(tmp_path); + throw; } #else @@ -200,6 +225,22 @@ FileOutputStream::OpenCreate([[maybe_unused]] bool visible) } #endif + if (!visible) { + /* attempt to create a temporary file */ + tmp_path = path + ".tmp"; + Delete(tmp_path); + + if (fd.Open( +#ifdef __linux__ + directory_fd, +#endif + tmp_path.c_str(), + O_WRONLY|O_CREAT|O_EXCL, + 0666)) + return; + + } + /* fall back to plain POSIX */ if (!fd.Open( #ifdef __linux__ @@ -263,7 +304,7 @@ FileOutputStream::Sync() void FileOutputStream::Commit() -{ +try { assert(IsDefined()); #ifdef HAVE_O_TMPFILE @@ -283,6 +324,13 @@ FileOutputStream::Commit() if (!Close()) { throw FormatErrno("Failed to commit %s", path.c_str()); } + + if (tmp_path != nullptr) + RenameOrThrow(tmp_path, path); +} catch (...) { + if (tmp_path != nullptr) + Delete(tmp_path); + throw; } #endif @@ -294,6 +342,11 @@ FileOutputStream::Cancel() noexcept Close(); + if (tmp_path != nullptr) { + Delete(tmp_path); + return; + } + switch (mode) { case Mode::CREATE: #ifdef HAVE_O_TMPFILE @@ -310,6 +363,27 @@ FileOutputStream::Cancel() noexcept } } +inline void +FileOutputStream::RenameOrThrow([[maybe_unused]] Path old_path, + [[maybe_unused]] Path new_path) const +{ + assert(old_path != nullptr); + assert(new_path != nullptr); + +#ifdef _WIN32 + if (!MoveFileEx(old_path.c_str(), new_path.c_str(), + MOVEFILE_REPLACE_EXISTING)) + throw MakeLastError("Failed to rename file"); +#elif defined(__linux__) + if (renameat(directory_fd.Get(), tmp_path.c_str(), + directory_fd.Get(), path.c_str()) < 0) + throw MakeErrno("Failed to rename file"); +#else + if (rename(tmp_path.c_str(), path.c_str())) + throw MakeErrno("Failed to rename file"); +#endif +} + inline void FileOutputStream::Delete(Path delete_path) const noexcept { diff --git a/src/io/FileOutputStream.hxx b/src/io/FileOutputStream.hxx index e89d03480..c1b04322f 100644 --- a/src/io/FileOutputStream.hxx +++ b/src/io/FileOutputStream.hxx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014-2018 Max Kellermann + * Copyright 2014-2022 Max Kellermann * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -68,6 +68,13 @@ class Path; class FileOutputStream final : public OutputStream { const AllocatedPath path; + /** + * If a temporary file is being written to, then this is its + * path. Commit() will rename it to the path specified in the + * constructor. + */ + AllocatedPath tmp_path{nullptr}; + #ifdef __linux__ const FileDescriptor directory_fd; #endif @@ -206,6 +213,7 @@ private: #endif } + void RenameOrThrow(Path old_path, Path new_path) const; void Delete(Path delete_path) const noexcept; }; diff --git a/test/net/meson.build b/test/net/meson.build index b21c9a2fb..00f35a194 100644 --- a/test/net/meson.build +++ b/test/net/meson.build @@ -19,6 +19,7 @@ test( include_directories: inc, dependencies: [ net_dep, + fs_dep, gtest_dep, ], ),