mirror of
https://github.com/sal063/AC6_recomp
synced 2026-08-21 23:00:53 -04:00
Make save and content writes atomic so an interrupted write cannot corrupt data
Overwriting a file through the content device used to delete the old file before the first new byte was written, so a crash or kill mid-save destroyed the existing data. A failed first-time save then left a half-written file the game reported as corrupted until it was deleted by hand. Writes now go to a temp file and commit by rename on close, keeping one .bak generation of the previous version. An interrupted write leaves the old data untouched, and a startup sweep resolves any write that died between the commit steps.
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include <native/filesystem/device.h>
|
||||
@@ -35,13 +37,30 @@ class HostPathDevice : public Device {
|
||||
uint32_t sectors_per_allocation_unit() const override { return 1; }
|
||||
uint32_t bytes_per_sector() const override { return 0x200; }
|
||||
|
||||
// Write-in-progress marker. When a marker path is set (content
|
||||
// packages do this), the device keeps a marker file on disk while any
|
||||
// atomic write session is open and removes it when the last one commits.
|
||||
// A marker still present at the next mount means a write died mid-flight
|
||||
// and the container may be torn across files - the content manager then
|
||||
// quarantines it. Empty path (the default) disables the marker.
|
||||
void set_write_marker_path(const std::filesystem::path& marker_path) {
|
||||
write_marker_path_ = marker_path;
|
||||
}
|
||||
void OnAtomicWriteBegin();
|
||||
void OnAtomicWriteEnd();
|
||||
|
||||
private:
|
||||
void PopulateEntry(HostPathEntry* parent_entry);
|
||||
void SweepStaleAtomicArtifacts();
|
||||
|
||||
std::string name_;
|
||||
std::filesystem::path host_path_;
|
||||
std::unique_ptr<Entry> root_entry_;
|
||||
bool read_only_;
|
||||
|
||||
std::filesystem::path write_marker_path_;
|
||||
std::mutex write_marker_mutex_;
|
||||
int active_atomic_writes_ = 0;
|
||||
};
|
||||
|
||||
} // namespace rex::filesystem
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
#include "host_path_entry.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <native/filesystem.h>
|
||||
#include <native/filesystem/devices/host_path_device.h>
|
||||
@@ -15,6 +19,22 @@
|
||||
|
||||
namespace rex::filesystem {
|
||||
|
||||
namespace {
|
||||
|
||||
bool HasAsciiSuffix(const std::string& text, const char* suffix) {
|
||||
const size_t suffix_len = std::strlen(suffix);
|
||||
return text.size() >= suffix_len &&
|
||||
text.compare(text.size() - suffix_len, suffix_len, suffix) == 0;
|
||||
}
|
||||
|
||||
// Host-side artifact of the atomic write path? (never guest-visible)
|
||||
bool IsAtomicWriteArtifactName(const std::string& utf8_name) {
|
||||
return HasAsciiSuffix(utf8_name, kAtomicWriteTempSuffix) ||
|
||||
HasAsciiSuffix(utf8_name, kAtomicWriteBackupSuffix);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
HostPathDevice::HostPathDevice(const std::string_view mount_path,
|
||||
const std::filesystem::path& host_path, bool read_only)
|
||||
: Device(mount_path), name_("STFS"), host_path_(host_path), read_only_(read_only) {}
|
||||
@@ -32,6 +52,10 @@ bool HostPathDevice::Initialize() {
|
||||
}
|
||||
}
|
||||
|
||||
if (!read_only_) {
|
||||
SweepStaleAtomicArtifacts();
|
||||
}
|
||||
|
||||
auto root_entry = new HostPathEntry(this, nullptr, "", host_path_);
|
||||
root_entry->attributes_ = kFileAttributeDirectory;
|
||||
root_entry_ = std::unique_ptr<Entry>(root_entry);
|
||||
@@ -90,6 +114,12 @@ Entry* HostPathDevice::ResolvePath(const std::string_view path) {
|
||||
void HostPathDevice::PopulateEntry(HostPathEntry* parent_entry) {
|
||||
auto child_infos = rex::filesystem::ListFiles(parent_entry->host_path());
|
||||
for (auto& child_info : child_infos) {
|
||||
if (child_info.type == rex::filesystem::FileInfo::Type::kFile &&
|
||||
IsAtomicWriteArtifactName(rex::path_to_utf8(child_info.name))) {
|
||||
// .rex-tmp / .rex-bak are host-side artifacts of the atomic write
|
||||
// path; the guest must never see them.
|
||||
continue;
|
||||
}
|
||||
auto child = HostPathEntry::Create(this, parent_entry,
|
||||
parent_entry->host_path() / child_info.name, child_info);
|
||||
parent_entry->children_.push_back(std::unique_ptr<Entry>(child));
|
||||
@@ -100,4 +130,80 @@ void HostPathDevice::PopulateEntry(HostPathEntry* parent_entry) {
|
||||
}
|
||||
}
|
||||
|
||||
void HostPathDevice::SweepStaleAtomicArtifacts() {
|
||||
// A previous session that died mid-write can leave "<name>.rex-tmp" behind
|
||||
// (and, in the narrow window between the two commit renames, the real file
|
||||
// moved aside to "<name>.rex-bak" with the temp never renamed in). Recover
|
||||
// the backup when the real file is missing, then drop stale temps. .rex-bak
|
||||
// files themselves are kept: they are the one previous generation the
|
||||
// atomic write path maintains.
|
||||
std::vector<std::filesystem::path> stale_temps;
|
||||
std::error_code ec;
|
||||
for (auto it = std::filesystem::recursive_directory_iterator(
|
||||
host_path_, std::filesystem::directory_options::skip_permission_denied, ec);
|
||||
!ec && it != std::filesystem::recursive_directory_iterator(); it.increment(ec)) {
|
||||
std::error_code file_ec;
|
||||
if (!it->is_regular_file(file_ec)) {
|
||||
continue;
|
||||
}
|
||||
if (HasAsciiSuffix(rex::path_to_utf8(it->path().filename()), kAtomicWriteTempSuffix)) {
|
||||
stale_temps.push_back(it->path());
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& temp_path : stale_temps) {
|
||||
const std::string temp_utf8 = rex::path_to_utf8(temp_path);
|
||||
const auto real_path =
|
||||
rex::to_path(temp_utf8.substr(0, temp_utf8.size() - std::strlen(kAtomicWriteTempSuffix)));
|
||||
std::filesystem::path bak_path = real_path;
|
||||
bak_path += kAtomicWriteBackupSuffix;
|
||||
|
||||
std::error_code sweep_ec;
|
||||
if (!std::filesystem::exists(real_path, sweep_ec) &&
|
||||
std::filesystem::exists(bak_path, sweep_ec)) {
|
||||
std::error_code restore_ec;
|
||||
std::filesystem::rename(bak_path, real_path, restore_ec);
|
||||
if (!restore_ec) {
|
||||
REXFS_WARN("Recovered '{}' from its backup after an interrupted write",
|
||||
rex::path_to_utf8(real_path));
|
||||
}
|
||||
}
|
||||
std::error_code rm_ec;
|
||||
if (std::filesystem::remove(temp_path, rm_ec) && !rm_ec) {
|
||||
REXFS_INFO("Removed stale write temp '{}' (interrupted write; original kept)", temp_utf8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HostPathDevice::OnAtomicWriteBegin() {
|
||||
if (write_marker_path_.empty()) {
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(write_marker_mutex_);
|
||||
if (active_atomic_writes_++ == 0) {
|
||||
auto marker = rex::filesystem::OpenFile(write_marker_path_, "wb");
|
||||
if (marker) {
|
||||
static const char kMarkerText[] =
|
||||
"Write in progress. If this file is still here on the next launch, the last "
|
||||
"write died mid-flight and the container next to it will be quarantined.\n";
|
||||
fwrite(kMarkerText, 1, sizeof(kMarkerText) - 1, marker);
|
||||
fclose(marker);
|
||||
} else {
|
||||
REXFS_WARN("Could not create write-in-progress marker '{}'",
|
||||
rex::path_to_utf8(write_marker_path_));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HostPathDevice::OnAtomicWriteEnd() {
|
||||
if (write_marker_path_.empty()) {
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(write_marker_mutex_);
|
||||
if (active_atomic_writes_ > 0 && --active_atomic_writes_ == 0) {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(write_marker_path_, ec);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rex::filesystem
|
||||
|
||||
@@ -50,6 +50,72 @@ X_STATUS HostPathEntry::Open(uint32_t desired_access, File** out_file) {
|
||||
REXFS_ERROR("Attempting to open file for write access on read-only device");
|
||||
return X_STATUS_ACCESS_DENIED;
|
||||
}
|
||||
|
||||
const bool wants_write =
|
||||
(desired_access & (FileAccess::kGenericWrite | FileAccess::kFileWriteData |
|
||||
FileAccess::kFileAppendData)) != 0;
|
||||
const bool truncate_pending = pending_truncate_;
|
||||
pending_truncate_ = false;
|
||||
|
||||
// Atomic write session: guest writes land in "<name>.rex-tmp" in
|
||||
// the same directory (same volume, so the commit rename is atomic) and are
|
||||
// committed over the real file when the handle closes, keeping one
|
||||
// ".rex-bak" generation. A crash, kill or failed write mid-save leaves the
|
||||
// OLD file intact instead of a torn one - the mechanism that left saves
|
||||
// permanently stuck behind the game's "Game Data is corrupted / please
|
||||
// delete it" dialog.
|
||||
if (wants_write && !is_read_only()) {
|
||||
if (!atomic_write_active_) {
|
||||
std::filesystem::path temp_path = host_path_;
|
||||
temp_path += kAtomicWriteTempSuffix;
|
||||
|
||||
std::error_code ec;
|
||||
bool temp_ready = false;
|
||||
if (!truncate_pending && std::filesystem::is_regular_file(host_path_, ec)) {
|
||||
// Preserve read-modify-write semantics: the handle must see the
|
||||
// current contents until the guest overwrites them.
|
||||
std::filesystem::copy_file(host_path_, temp_path,
|
||||
std::filesystem::copy_options::overwrite_existing, ec);
|
||||
temp_ready = !ec;
|
||||
} else {
|
||||
// Fresh create, or a deferred truncation: start from empty.
|
||||
auto temp_file = rex::filesystem::OpenFile(temp_path, "wb");
|
||||
if (temp_file) {
|
||||
fclose(temp_file);
|
||||
temp_ready = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (temp_ready) {
|
||||
auto temp_handle = rex::filesystem::FileHandle::OpenExisting(temp_path, desired_access);
|
||||
if (temp_handle) {
|
||||
atomic_write_active_ = true;
|
||||
static_cast<HostPathDevice*>(device_)->OnAtomicWriteBegin();
|
||||
*out_file = new HostPathFile(desired_access, this, std::move(temp_handle),
|
||||
std::move(temp_path), truncate_pending);
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
}
|
||||
std::error_code cleanup_ec;
|
||||
std::filesystem::remove(temp_path, cleanup_ec);
|
||||
REXFS_WARN("Atomic write unavailable for '{}' - writing in place",
|
||||
rex::path_to_utf8(host_path_));
|
||||
} else {
|
||||
REXFS_WARN("Second concurrent write handle for '{}' - writing in place (not atomic)",
|
||||
rex::path_to_utf8(host_path_));
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback / read-only path: direct handle on the real file. A truncation
|
||||
// that could not ride an atomic session must land on disk here after all.
|
||||
if (truncate_pending) {
|
||||
auto file = rex::filesystem::OpenFile(host_path_, "wb");
|
||||
if (!file) {
|
||||
return X_STATUS_ACCESS_DENIED;
|
||||
}
|
||||
fclose(file);
|
||||
}
|
||||
|
||||
auto file_handle = rex::filesystem::FileHandle::OpenExisting(host_path_, desired_access);
|
||||
if (!file_handle) {
|
||||
return X_STATUS_NO_SUCH_FILE;
|
||||
@@ -58,6 +124,51 @@ X_STATUS HostPathEntry::Open(uint32_t desired_access, File** out_file) {
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
void HostPathEntry::CommitAtomicWrite(const std::filesystem::path& temp_path, bool commit,
|
||||
bool dirty) {
|
||||
atomic_write_active_ = false;
|
||||
auto* host_device = static_cast<HostPathDevice*>(device_);
|
||||
|
||||
std::error_code ec;
|
||||
if (!commit) {
|
||||
// A write failed during the session: abandon the temp, the previous file
|
||||
// stays exactly as it was. The guest already saw the write error.
|
||||
std::filesystem::remove(temp_path, ec);
|
||||
REXFS_ERROR("Write to '{}' failed - previous contents kept intact",
|
||||
rex::path_to_utf8(host_path_));
|
||||
} else if (!dirty) {
|
||||
// Write handle closed without writing anything: nothing to commit.
|
||||
std::filesystem::remove(temp_path, ec);
|
||||
} else {
|
||||
// Keep exactly one backup generation, then swap the finished temp in.
|
||||
std::filesystem::path bak_path = host_path_;
|
||||
bak_path += kAtomicWriteBackupSuffix;
|
||||
bool have_bak = false;
|
||||
if (std::filesystem::exists(host_path_, ec) && !ec) {
|
||||
std::error_code bak_ec;
|
||||
std::filesystem::remove(bak_path, bak_ec);
|
||||
bak_ec.clear();
|
||||
std::filesystem::rename(host_path_, bak_path, bak_ec);
|
||||
have_bak = !bak_ec;
|
||||
}
|
||||
ec.clear();
|
||||
std::filesystem::rename(temp_path, host_path_, ec);
|
||||
if (ec) {
|
||||
REXFS_ERROR("Failed to commit write to '{}': {} - restoring previous contents",
|
||||
rex::path_to_utf8(host_path_), ec.message());
|
||||
if (have_bak) {
|
||||
std::error_code restore_ec;
|
||||
std::filesystem::rename(bak_path, host_path_, restore_ec);
|
||||
}
|
||||
std::error_code rm_ec;
|
||||
std::filesystem::remove(temp_path, rm_ec);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
host_device->OnAtomicWriteEnd();
|
||||
}
|
||||
|
||||
std::unique_ptr<memory::MappedMemory> HostPathEntry::OpenMapped(memory::MappedMemory::Mode mode,
|
||||
size_t offset, size_t length) {
|
||||
return memory::MappedMemory::Open(host_path_, mode, offset, length);
|
||||
@@ -67,11 +178,20 @@ bool HostPathEntry::Truncate() {
|
||||
if (is_read_only() || (attributes_ & kFileAttributeDirectory)) {
|
||||
return false;
|
||||
}
|
||||
auto file = rex::filesystem::OpenFile(host_path_, "wb");
|
||||
// Probe writability without destroying anything: the old behaviour opened
|
||||
// "wb" (truncating the real file on the spot), so an interrupted overwrite
|
||||
// had already lost the previous contents before the first new byte landed.
|
||||
// A locked file (AV/cloud sync) must still fail LOUDLY here - the guest
|
||||
// gets a save error and retries - rather than silently later.
|
||||
auto file = rex::filesystem::OpenFile(host_path_, "r+b");
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
fclose(file);
|
||||
// Defer the on-disk truncation into the atomic write session the VFS opens
|
||||
// right after: the session starts from an empty temp and the
|
||||
// real file is only replaced at commit.
|
||||
pending_truncate_ = true;
|
||||
size_ = 0;
|
||||
allocation_size_ = 0;
|
||||
return true;
|
||||
@@ -129,8 +249,15 @@ void HostPathEntry::RenameEntryInternal(const std::vector<std::string_view>& pat
|
||||
}
|
||||
|
||||
void HostPathEntry::update() {
|
||||
// During an atomic write session the in-flight contents live in the temp
|
||||
// file; size queries must reflect what the guest just wrote, not the
|
||||
// yet-to-be-replaced previous file.
|
||||
std::filesystem::path query_path = host_path_;
|
||||
if (atomic_write_active_) {
|
||||
query_path += kAtomicWriteTempSuffix;
|
||||
}
|
||||
rex::filesystem::FileInfo file_info;
|
||||
if (!rex::filesystem::GetInfo(host_path_, &file_info)) {
|
||||
if (!rex::filesystem::GetInfo(query_path, &file_info)) {
|
||||
return;
|
||||
}
|
||||
if (file_info.type == rex::filesystem::FileInfo::Type::kFile) {
|
||||
|
||||
@@ -14,6 +14,11 @@ namespace rex::filesystem {
|
||||
|
||||
class HostPathDevice;
|
||||
|
||||
// Host-side artifacts of the atomic write path. Never guest-visible:
|
||||
// the device skips them when populating the entry tree.
|
||||
inline constexpr char kAtomicWriteTempSuffix[] = ".rex-tmp";
|
||||
inline constexpr char kAtomicWriteBackupSuffix[] = ".rex-bak";
|
||||
|
||||
class HostPathEntry : public Entry {
|
||||
public:
|
||||
HostPathEntry(Device* device, Entry* parent, const std::string_view path,
|
||||
@@ -29,6 +34,15 @@ class HostPathEntry : public Entry {
|
||||
X_STATUS Open(uint32_t desired_access, File** out_file) override;
|
||||
bool Truncate() override;
|
||||
|
||||
// Atomic write session: a write handle opened by Open() writes
|
||||
// into "<name>.rex-tmp" in the same directory; HostPathFile calls this on
|
||||
// close. commit=false abandons the temp (failed write - old data stays);
|
||||
// dirty=false means nothing was written (temp discarded, file untouched).
|
||||
// A successful commit keeps ONE previous generation at "<name>.rex-bak"
|
||||
// and renames the temp over the real file, so an interrupted or failed
|
||||
// save can never leave a torn file in place.
|
||||
void CommitAtomicWrite(const std::filesystem::path& temp_path, bool commit, bool dirty);
|
||||
|
||||
bool can_map() const override { return true; }
|
||||
std::unique_ptr<memory::MappedMemory> OpenMapped(memory::MappedMemory::Mode mode, size_t offset,
|
||||
size_t length) override;
|
||||
@@ -47,6 +61,13 @@ class HostPathEntry : public Entry {
|
||||
void RenameEntryInternal(const std::vector<std::string_view>& path_parts) override;
|
||||
|
||||
std::filesystem::path host_path_;
|
||||
// Truncate() defers the on-disk truncation into the atomic write session
|
||||
// the VFS opens immediately afterwards, so an interrupted overwrite leaves
|
||||
// the previous file intact. Consumed (and cleared) by the next Open().
|
||||
bool pending_truncate_ = false;
|
||||
// One atomic session per entry at a time; a second concurrent write handle
|
||||
// falls back to in-place access (logged).
|
||||
bool atomic_write_active_ = false;
|
||||
};
|
||||
|
||||
} // namespace rex::filesystem
|
||||
|
||||
@@ -12,9 +12,27 @@ HostPathFile::HostPathFile(uint32_t file_access, HostPathEntry* entry,
|
||||
std::unique_ptr<rex::filesystem::FileHandle> file_handle)
|
||||
: File(file_access, entry), file_handle_(std::move(file_handle)) {}
|
||||
|
||||
HostPathFile::HostPathFile(uint32_t file_access, HostPathEntry* entry,
|
||||
std::unique_ptr<rex::filesystem::FileHandle> file_handle,
|
||||
std::filesystem::path temp_path, bool started_dirty)
|
||||
: File(file_access, entry),
|
||||
file_handle_(std::move(file_handle)),
|
||||
atomic_(true),
|
||||
temp_path_(std::move(temp_path)),
|
||||
dirty_(started_dirty) {}
|
||||
|
||||
HostPathFile::~HostPathFile() = default;
|
||||
|
||||
void HostPathFile::Destroy() {
|
||||
if (atomic_) {
|
||||
// Flush before the commit renames: the data must be on its way to disk
|
||||
// before the temp becomes the real file.
|
||||
if (file_handle_) {
|
||||
file_handle_->Flush();
|
||||
file_handle_.reset();
|
||||
}
|
||||
static_cast<HostPathEntry*>(entry_)->CommitAtomicWrite(temp_path_, !write_failed_, dirty_);
|
||||
}
|
||||
delete this;
|
||||
}
|
||||
|
||||
@@ -38,9 +56,13 @@ X_STATUS HostPathFile::WriteSync(std::span<const uint8_t> buffer, size_t byte_of
|
||||
return X_STATUS_ACCESS_DENIED;
|
||||
}
|
||||
|
||||
dirty_ = true;
|
||||
if (file_handle_->Write(byte_offset, buffer.data(), buffer.size(), out_bytes_written)) {
|
||||
return X_STATUS_SUCCESS;
|
||||
} else {
|
||||
// A failed write poisons the session: committing a partial temp over the
|
||||
// real file would be exactly the torn save this path exists to prevent.
|
||||
write_failed_ = true;
|
||||
return X_STATUS_END_OF_FILE;
|
||||
}
|
||||
}
|
||||
@@ -50,9 +72,11 @@ X_STATUS HostPathFile::SetLength(size_t length) {
|
||||
return X_STATUS_ACCESS_DENIED;
|
||||
}
|
||||
|
||||
dirty_ = true;
|
||||
if (file_handle_->SetLength(length)) {
|
||||
return X_STATUS_SUCCESS;
|
||||
} else {
|
||||
write_failed_ = true;
|
||||
return X_STATUS_END_OF_FILE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <span>
|
||||
#include <string>
|
||||
|
||||
@@ -17,8 +18,17 @@ class HostPathEntry;
|
||||
|
||||
class HostPathFile : public File {
|
||||
public:
|
||||
// Direct handle on the real file (read-only opens, and the fallback when an
|
||||
// atomic session cannot be established).
|
||||
HostPathFile(uint32_t file_access, HostPathEntry* entry,
|
||||
std::unique_ptr<rex::filesystem::FileHandle> file_handle);
|
||||
// Atomic write session: the handle points at "<name>.rex-tmp";
|
||||
// Destroy() flushes, closes and asks the entry to commit it over the real
|
||||
// file. started_dirty marks a deferred truncation as a modification even if
|
||||
// the guest then writes nothing.
|
||||
HostPathFile(uint32_t file_access, HostPathEntry* entry,
|
||||
std::unique_ptr<rex::filesystem::FileHandle> file_handle,
|
||||
std::filesystem::path temp_path, bool started_dirty);
|
||||
~HostPathFile() override;
|
||||
|
||||
void Destroy() override;
|
||||
@@ -30,6 +40,11 @@ class HostPathFile : public File {
|
||||
|
||||
private:
|
||||
std::unique_ptr<rex::filesystem::FileHandle> file_handle_;
|
||||
// Atomic write session state.
|
||||
bool atomic_ = false;
|
||||
std::filesystem::path temp_path_;
|
||||
bool dirty_ = false;
|
||||
bool write_failed_ = false;
|
||||
};
|
||||
|
||||
} // namespace rex::filesystem
|
||||
|
||||
@@ -312,11 +312,18 @@ X_STATUS VirtualFileSystem::OpenFile(Entry* root_entry, const std::string_view p
|
||||
break;
|
||||
case FileDisposition::kOverwrite:
|
||||
case FileDisposition::kOverwriteIf:
|
||||
// Overwrite by delete + recreate, or truncate if delete fails
|
||||
// (host file may be briefly locked by cloud sync, AV, etc.).
|
||||
if (entry->Delete()) {
|
||||
// Truncate first: a device that supports it (host paths) defers the
|
||||
// truncation into the atomic write session opened right below, so an
|
||||
// interrupted overwrite keeps the previous file contents.
|
||||
// The historical delete + recreate destroyed the old file before the
|
||||
// first new byte was written. Devices without Truncate, and files a
|
||||
// truncate probe finds locked (cloud sync, AV), keep the old
|
||||
// delete-or-fail behaviour.
|
||||
if (entry->Truncate()) {
|
||||
// Entry stays; the truncation materializes at open/commit.
|
||||
} else if (entry->Delete()) {
|
||||
entry = nullptr;
|
||||
} else if (!entry->Truncate()) {
|
||||
} else {
|
||||
return X_STATUS_ACCESS_DENIED;
|
||||
}
|
||||
*out_action = FileAction::kOverwritten;
|
||||
|
||||
Reference in New Issue
Block a user