diff --git a/Source/Core/Common/FileUtil.cpp b/Source/Core/Common/FileUtil.cpp index bcfd74256d..d6c7599ced 100644 --- a/Source/Core/Common/FileUtil.cpp +++ b/Source/Core/Common/FileUtil.cpp @@ -25,6 +25,7 @@ #include "Common/CommonFuncs.h" #include "Common/CommonPaths.h" #include "Common/CommonTypes.h" +#include "Common/DirectIOFile.h" #ifdef __APPLE__ #include "Common/DynamicLibrary.h" #endif @@ -668,13 +669,42 @@ std::string CreateTempDir() #endif } -std::string GetTempFilenameForAtomicWrite(std::string path) +static auto TryToGetAbsolutePath(std::string path) { std::error_code error; auto absolute_path = fs::absolute(StringToPath(path), error); if (!error) path = PathToString(absolute_path); - return std::move(path) + ".xxx"; + return path; +} + +std::string GetTempFilenameForAtomicWrite(std::string path) +{ + return TryToGetAbsolutePath(std::move(path)) + ".xxx"; +} + +std::string CreateTempFileForAtomicWrite(std::string path) +{ + path = TryToGetAbsolutePath(std::move(path)); + while (true) + { + DirectIOFile file; + + // e.g. "/dir/file.txt" -> "/dir/file.txt.189234789.tmp" + const auto timestamp = Clock::now().time_since_epoch().count(); + std::string tmp_path = fmt::format("{}.{}.tmp", path, timestamp); + + const auto open_result = file.Open(tmp_path, AccessMode::Write, OpenMode::Create); + if (open_result.Succeeded()) + return tmp_path; + + // In the very unlikely case that the file already exists, we will try again. + if (open_result.Error() == File::OpenError::AlreadyExists) + continue; + + // Failure. + return {}; + } } #if defined(__APPLE__) diff --git a/Source/Core/Common/FileUtil.h b/Source/Core/Common/FileUtil.h index 37548acfed..585614b6f9 100644 --- a/Source/Core/Common/FileUtil.h +++ b/Source/Core/Common/FileUtil.h @@ -223,6 +223,10 @@ std::string CreateTempDir(); // Get a filename that can hopefully be atomically renamed to the given path. std::string GetTempFilenameForAtomicWrite(std::string path); +// Creates and returns the path to a newly created temporary file next to the given path. +// Returns an empty string on error, generally caused by lack of write permissions. +std::string CreateTempFileForAtomicWrite(std::string path); + // Gets a set user directory path // Don't call prior to setting the base user directory const std::string& GetUserPath(unsigned int dir_index); diff --git a/Source/UnitTests/Common/FileUtilTest.cpp b/Source/UnitTests/Common/FileUtilTest.cpp index ba5ed1b5b2..ce90b2729e 100644 --- a/Source/UnitTests/Common/FileUtilTest.cpp +++ b/Source/UnitTests/Common/FileUtilTest.cpp @@ -154,6 +154,17 @@ TEST_F(FileUtilTest, CreateFullPath) EXPECT_TRUE(File::IsFile(p3file)); } +TEST_F(FileUtilTest, CreateTempFileForAtomicWrite) +{ + EXPECT_TRUE(File::Exists(File::CreateTempFileForAtomicWrite(m_file_path))); + +#if defined(_WIN32) + EXPECT_FALSE(File::Exists(File::CreateTempFileForAtomicWrite("C:/con/cant_write_here.txt"))); +#else + EXPECT_FALSE(File::Exists(File::CreateTempFileForAtomicWrite("/dev/null/cant_write_here.txt"))); +#endif +} + TEST_F(FileUtilTest, DirectIOFile) { static constexpr std::array u8_test_data = {42, 7, 99};