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
* modification, are permitted provided that the following conditions
@ -31,6 +31,10 @@
#include "system/Error.hxx"
#include "util/StringFormat.hxx"
#ifdef _WIN32
#include <tchar.h>
#endif
#ifdef __linux__
#include <fcntl.h>
#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
{

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
* 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;
};

View File

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