Files
spacebox/src/filesystem.hpp
cocktailfrank c6acedf204 Add support for playing music files remotely in Emscripten builds
Use the HTML5 Audio element to load and play files specified by URL in
Emscripten builds.

Temporary fix in Steam stats script to handle corrupt float values in
total distance walked
2026-05-12 21:49:20 -04:00

181 lines
7.1 KiB
C++

/* +=======================================================+
____/ \____ /: Open source game framework licensed to freely use, :
\ / / : copy, and modify - created for dank.game :
+==\ ^__^ /==+ : :
: ~/ \~ : : Download at https://open.shampoo.ooo/shampoo/spacebox :
: ~~~~~~~~~~~~ : +=======================================================+
: SPACE ~~~~~ : /
: ~~~~~~~ BOX :/
+=============*/
#pragma once
/* C++17's filesystem library is still experimental in Ubuntu 18.04's clang-8 */
#if defined(__UBUNTU18__)
#include <experimental/filesystem>
namespace fs = std::experimental::filesystem;
#else
#include <filesystem>
namespace fs = std::filesystem;
#endif
#include <sstream>
#include <fstream>
#include <optional>
#if defined(__EMSCRIPTEN__)
#include <emscripten.h>
#endif
#include "math.hpp"
#include "Log.hpp"
namespace sb::filesystem {
/*!
* Get a shared pointer to a path which will be deleted automatically, if necessary, when the pointer goes out of
* scope.
*
* By default, the given path will be created inside fs::temp_directory_path(). To override that location, specific
* an alternate parent directory as the second argument.
*
* The path is not actually created, so it is just a name at first. If the path gets created, either as a file or
* directory, it will automatically be deleted when the pointer goes out of scope. If the path is a directory, the
* directory contents will also be deleted, recursively.
*
* @param location Path within specified parent directory to create the temporary path
* @param directory Parent directory for the given path
*
* @return Shared pointer to a path within specified parent directory
*/
std::shared_ptr<fs::path> temp_path(const fs::path& name, std::optional<fs::path> directory = std::nullopt);
/*!
* Write stream-able data to a given file path. In order to make the operation atomic, the data will be written to a
* temporary file first, then moved to the destination path.
*
* If the backup flag is set and a file exists at the given destination path, the file will be copied to the same
* location, with ".backup" appended to the file name. If a backup file already exists at that location, it will be
* overwritten with the new backup. If an error occurs during backup, an error will be logged but no exception will
* be thrown.
*
* If width is specified, `std::setw` will be used to format the data for output. This can be used, for example, to
* set the indentation width of JSON data.
*
* The given data must be compatible with `std::basic_ofstream::operator<<`, like std::string or nlohmann::json for
* example.
*
* The temporary file is obtained using C++17's temp_directory_path function in the filesystem library.
*
* The function will attempt to set the file permissions to 0644. To set alternate permissions, run the filesystem
* library's `permissions` function afterward.
*
* On Emscripten builds, a filesystem sync will run after the file is in place.
*
* @param content Data that can be passed to std::basic_ofstream::operator<< to be written
* @param destination Path to a file where the given content will be written
* @param backup Move existing file to a backup by appending ".backup" to the file name
* @param line_break Add a line break to the end of the output
* @param width Optional formatting width for the output data
*
* @throw std::runtime_error If writing the data fails
*/
template<typename Streamable>
void write_atomic(
const Streamable& content,
fs::path destination,
bool backup = true,
bool line_break = true,
int width = 0)
{
try
{
/* Only backup if the current file is not empty */
if (backup && fs::exists(destination) && fs::file_size(destination) > 0)
{
/* Copy existing data to a backup file */
fs::copy_file(destination, destination.string() + ".backup", fs::copy_options::overwrite_existing);
}
}
catch (const std::exception& error)
{
sb::Log::Multi(sb::Log::ERR) << "Error writing backup at " << destination << ". " << error.what() <<
sb::Log::end;
}
/* Use random string generator to generate a name that doesn't exist, to avoid any name collision. */
std::shared_ptr<fs::path> temp_destination;
do
{
temp_destination = temp_path(sb::math::random::id(7), destination.parent_path());
} while (fs::exists(*temp_destination));
/* Write data to file in a temporary location. Use line break flag to determine whether to append std::endl. */
std::ofstream temp_file;
try
{
temp_file = std::ofstream(*temp_destination);
}
catch (const std::exception& error)
{
throw std::runtime_error(std::string("Error creating file at ") + temp_destination->string());
}
if (line_break)
{
temp_file << std::setw(width) << content << std::endl;
}
else
{
temp_file << std::setw(width) << content;
}
temp_file.close();
/* Use ofstream operator override to check if an error occurred (see /en/cpp/io/basic_ios/operator!.html) */
if (!temp_file)
{
throw std::runtime_error(std::string("Error while writing data to ") + temp_destination->string());
}
/* Move data to permanent location */
try
{
fs::rename(*temp_destination, destination);
}
catch (const std::exception& error)
{
std::stringstream message;
message << "Error moving file " << *temp_destination << " to permanent location " << destination << ". " <<
error.what();
throw std::runtime_error(message.str());
}
/* Set file permissions to rw-/r--/r-- */
try
{
fs::permissions(
destination,
fs::perms::owner_read | fs::perms::owner_write | fs::perms::group_read | fs::perms::group_write);
}
catch (const fs::filesystem_error& error)
{
sb::Log::Multi(sb::Log::WARN) << "Could not set permissions for progress file at " << destination <<
". " << error.what() << sb::Log::end;
}
#if defined(__EMSCRIPTEN__)
EM_ASM(
/* Sync filesystem from Emscripten to persistent storage. Only run a new sync if a sync isn't already
* running. */
FS.syncfs(false, function(error) {
if (error !== null)
{
console.log("Error syncing storage using Filesystem API", error);
}
});
);
#endif
/* To-do: Android */
}
}