io/FileOutputStream: write to temporary file if O_TMPFILE is not available

This commit is contained in:
Max Kellermann 2022-05-12 19:26:49 +02:00
parent c344403bed
commit 0c98d93e9a
3 changed files with 88 additions and 5 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2014-2018 Max Kellermann <max.kellermann@gmail.com> * Copyright 2014-2022 Max Kellermann <max.kellermann@gmail.com>
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions * modification, are permitted provided that the following conditions
@ -31,6 +31,10 @@
#include "system/Error.hxx" #include "system/Error.hxx"
#include "util/StringFormat.hxx" #include "util/StringFormat.hxx"
#ifdef _WIN32
#include <tchar.h>
#endif
#ifdef __linux__ #ifdef __linux__
#include <fcntl.h> #include <fcntl.h>
#endif #endif
@ -81,8 +85,22 @@ FileOutputStream::Open()
#ifdef _WIN32 #ifdef _WIN32
inline void 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, handle = CreateFile(path.c_str(), GENERIC_WRITE, 0, nullptr,
CREATE_ALWAYS, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL|FILE_FLAG_WRITE_THROUGH, FILE_ATTRIBUTE_NORMAL|FILE_FLAG_WRITE_THROUGH,
@ -149,10 +167,17 @@ FileOutputStream::Sync()
void void
FileOutputStream::Commit() FileOutputStream::Commit()
{ try {
assert(IsDefined()); assert(IsDefined());
Close(); Close();
if (tmp_path != nullptr)
RenameOrThrow(tmp_path, path);
} catch (...) {
if (tmp_path != nullptr)
Delete(tmp_path);
throw;
} }
#else #else
@ -200,6 +225,22 @@ FileOutputStream::OpenCreate([[maybe_unused]] bool visible)
} }
#endif #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 */ /* fall back to plain POSIX */
if (!fd.Open( if (!fd.Open(
#ifdef __linux__ #ifdef __linux__
@ -263,7 +304,7 @@ FileOutputStream::Sync()
void void
FileOutputStream::Commit() FileOutputStream::Commit()
{ try {
assert(IsDefined()); assert(IsDefined());
#ifdef HAVE_O_TMPFILE #ifdef HAVE_O_TMPFILE
@ -283,6 +324,13 @@ FileOutputStream::Commit()
if (!Close()) { if (!Close()) {
throw FormatErrno("Failed to commit %s", path.c_str()); 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 #endif
@ -294,6 +342,11 @@ FileOutputStream::Cancel() noexcept
Close(); Close();
if (tmp_path != nullptr) {
Delete(tmp_path);
return;
}
switch (mode) { switch (mode) {
case Mode::CREATE: case Mode::CREATE:
#ifdef HAVE_O_TMPFILE #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 inline void
FileOutputStream::Delete(Path delete_path) const noexcept FileOutputStream::Delete(Path delete_path) const noexcept
{ {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2014-2018 Max Kellermann <max.kellermann@gmail.com> * Copyright 2014-2022 Max Kellermann <max.kellermann@gmail.com>
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions * modification, are permitted provided that the following conditions
@ -68,6 +68,13 @@ class Path;
class FileOutputStream final : public OutputStream { class FileOutputStream final : public OutputStream {
const AllocatedPath path; 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__ #ifdef __linux__
const FileDescriptor directory_fd; const FileDescriptor directory_fd;
#endif #endif
@ -206,6 +213,7 @@ private:
#endif #endif
} }
void RenameOrThrow(Path old_path, Path new_path) const;
void Delete(Path delete_path) const noexcept; void Delete(Path delete_path) const noexcept;
}; };

View File

@ -19,6 +19,7 @@ test(
include_directories: inc, include_directories: inc,
dependencies: [ dependencies: [
net_dep, net_dep,
fs_dep,
gtest_dep, gtest_dep,
], ],
), ),