Skip to content

Commit

Permalink
#5565: Add TemporaryOutputStream helper object taking care of all the…
Browse files Browse the repository at this point in the history
… temp file handling, renaming and overwriting.
  • Loading branch information
codereader committed Mar 28, 2021
1 parent 2481e99 commit 6daa204
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 50 deletions.
86 changes: 86 additions & 0 deletions libs/stream/TemporaryOutputStream.h
@@ -0,0 +1,86 @@
#pragma once

#include "i18n.h"
#include <string>
#include <fstream>
#include "os/fs.h"
#include "fmt/format.h"

namespace stream
{

// An output stream wrapper which opens a temporary file next to the actual target,
// moving the temporary file over the target file on demand.
// If something goes wrong, a std::runtime_error is thrown.
class TemporaryOutputStream
{
private:
fs::path _targetFile;
fs::path _temporaryPath;

std::ofstream _stream;

public:
TemporaryOutputStream(const fs::path& targetFile) :
_targetFile(targetFile),
_temporaryPath(getTemporaryPath(_targetFile)),
_stream(_temporaryPath)
{
if (!_stream.is_open())
{
throw std::runtime_error(fmt::format(_("Cannot open file for writing: {0}"), _temporaryPath.string()));
}
}

std::ostream& getStream()
{
return _stream;
}

void closeAndReplaceTargetFile()
{
_stream.close();

// Move the temporary stream over the actual file, removing the target first
if (fs::exists(_targetFile))
{
try
{
fs::remove(_targetFile);
}
catch (fs::filesystem_error& e)
{
rError() << "Could not remove the file " << _targetFile.string() << std::endl
<< e.what() << std::endl;

throw std::runtime_error(fmt::format(_("Could not remove the file {0}"), _targetFile.string()));
}
}

try
{
fs::rename(_temporaryPath, _targetFile);
}
catch (fs::filesystem_error& e)
{
rError() << "Could not rename the temporary file " << _temporaryPath.string() << std::endl
<< e.what() << std::endl;

throw std::runtime_error(
fmt::format(_("Could not rename the temporary file {0}"), _temporaryPath.string()));
}
}

private:
static fs::path getTemporaryPath(const fs::path& targetFile)
{
fs::path tempFile = targetFile;

tempFile.remove_filename();
tempFile /= "_" + targetFile.filename().string();

return tempFile;
}
};

}
5 changes: 5 additions & 0 deletions radiant/ui/materials/editor/MaterialEditor.cpp
Expand Up @@ -1093,6 +1093,11 @@ void MaterialEditor::updateStageColoredStatus()

bool MaterialEditor::saveCurrentMaterial()
{
if (!_material->isModified())
{
return true;
}

if (_material->getShaderFileInfo().fullPath().empty())
{
// Ask the user where to save it
Expand Down
61 changes: 11 additions & 50 deletions radiantcore/particles/ParticlesManager.cpp
Expand Up @@ -15,6 +15,7 @@
#include "i18n.h"

#include "parser/DefTokeniser.h"
#include "stream/TemporaryOutputStream.h"
#include "math/Vector4.h"
#include "os/fs.h"

Expand Down Expand Up @@ -346,20 +347,10 @@ void ParticlesManager::saveParticleDef(const std::string& particleName)
}

// Open a temporary file
fs::path tempFile = targetFile;

tempFile.remove_filename();
tempFile /= "_" + targetFile.filename().string();

std::ofstream tempStream(tempFile.string().c_str());

if (!tempStream.is_open())
{
throw std::runtime_error(
fmt::format(_("Cannot open file for writing: {0}"), tempFile.string()));
}
stream::TemporaryOutputStream tempStream(targetFile);

std::string tempString;
auto& stream = tempStream.getStream();

// If a previous file exists, open it for reading and filter out the particle def we'll be writing
if (fs::exists(targetFile))
Expand All @@ -373,65 +364,35 @@ void ParticlesManager::saveParticleDef(const std::string& particleName)
}

// Write the file to the output stream, up to the point the particle def should be written to
stripParticleDefFromStream(inheritStream, tempStream, particleName);
stripParticleDefFromStream(inheritStream, stream, particleName);

if (inheritStream.eof())
{
// Particle def was not found in the inherited stream, write our comment
tempStream << std::endl << std::endl;
stream << std::endl << std::endl;

writeParticleCommentHeader(tempStream);
writeParticleCommentHeader(stream);
}

// We're at the insertion point (which might as well be EOF of the inheritStream)

// Write the particle declaration
tempStream << *particle << std::endl;
stream << *particle << std::endl;

tempStream << inheritStream.rdbuf();
stream << inheritStream.rdbuf();

inheritStream.close();
}
else
{
// Just put the particle def into the file and that's it, leave a comment at the head of the decl
writeParticleCommentHeader(tempStream);
writeParticleCommentHeader(stream);

// Write the particle declaration
tempStream << *particle << std::endl;
stream << *particle << std::endl;
}

tempStream.close();

// Move the temporary stream over the actual file, removing the target first
if (fs::exists(targetFile))
{
try
{
fs::remove(targetFile);
}
catch (fs::filesystem_error& e)
{
rError() << "Could not remove the file " << targetFile.string() << std::endl
<< e.what() << std::endl;

throw std::runtime_error(
fmt::format(_("Could not remove the file {0}"), targetFile.string()));
}
}

try
{
fs::rename(tempFile, targetFile);
}
catch (fs::filesystem_error& e)
{
rError() << "Could not rename the temporary file " << tempFile.string() << std::endl
<< e.what() << std::endl;

throw std::runtime_error(
fmt::format(_("Could not rename the temporary file {0}"), tempFile.string()));
}
tempStream.closeAndReplaceTargetFile();
}

void ParticlesManager::stripParticleDefFromStream(std::istream& input,
Expand Down
33 changes: 33 additions & 0 deletions radiantcore/shaders/Doom3ShaderSystem.cpp
Expand Up @@ -22,6 +22,8 @@
#include "debugging/ScopedDebugTimer.h"
#include "module/StaticModule.h"

#include "os/file.h"
#include "os/path.h"
#include "string/predicate.h"
#include "string/replace.h"
#include "parser/DefBlockTokeniser.h"
Expand Down Expand Up @@ -438,6 +440,37 @@ void Doom3ShaderSystem::saveMaterial(const std::string& name)
{
ensureDefsLoaded();

auto material = _library->findShader(name);

if (!material->isModified())
{
rMessage() << "Material is not modified, nothing to save." << std::endl;
return;
}

if (!materialCanBeModified(name))
{
throw std::runtime_error("Cannot save this material, it's read-only.");
}

if (material->getShaderFileInfo().fullPath().empty())
{
throw std::runtime_error("No file path set on this material, cannot save.");
}

// Construct the output path for this material
fs::path outputPath = GlobalGameManager().getModPath();
outputPath /= material->getShaderFileInfo().fullPath();

auto outputDir = os::getContainingDir(outputPath.string());
if (!os::fileOrDirExists(outputDir))
{
rMessage() << "Creating mod materials path: " << outputDir << std::endl;
fs::create_directories(outputDir);
}

rMessage() << "Saving material " << name << " to " << outputPath << std::endl;


}

Expand Down
1 change: 1 addition & 0 deletions tools/msvc/libs.vcxproj
Expand Up @@ -230,6 +230,7 @@
<ClInclude Include="..\..\libs\stream\MapResourceStream.h" />
<ClInclude Include="..\..\libs\stream\PointerInputStream.h" />
<ClInclude Include="..\..\libs\stream\ScopedArchiveBuffer.h" />
<ClInclude Include="..\..\libs\stream\TemporaryOutputStream.h" />
<ClInclude Include="..\..\libs\stream\TextFileInputStream.h" />
<ClInclude Include="..\..\libs\stream\utils.h" />
<ClInclude Include="..\..\libs\string\case_conv.h" />
Expand Down
3 changes: 3 additions & 0 deletions tools/msvc/libs.vcxproj.filters
Expand Up @@ -283,6 +283,9 @@
<ClInclude Include="..\..\libs\materials\ParseLib.h">
<Filter>materials</Filter>
</ClInclude>
<ClInclude Include="..\..\libs\stream\TemporaryOutputStream.h">
<Filter>stream</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Filter Include="util">
Expand Down

0 comments on commit 6daa204

Please sign in to comment.