#include "asset_store.h"
#include <charconv>
#include <filesystem>
#include <optional>
#include <string>
#include <system_error>
#include <utility>
#include <spdlog/spdlog.h>
#include "data.h"
#include "public_id.h"
namespace
{
struct RecoveryName
{
std::string public_id;
std::int64_t revision;
};
std::optional<RecoveryName> parseRecoveryName(
const std::string& name,
std::string_view prefix)
{
if(!name.starts_with(prefix))
{
return std::nullopt;
}
const std::size_t begin = prefix.size();
std::size_t marker = name.find("-r", begin);
while(marker != std::string::npos)
{
const std::size_t revision_begin = marker + 2;
const std::size_t suffix = name.find('-', revision_begin);
if(suffix != std::string::npos)
{
const std::string public_id = name.substr(begin, marker - begin);
const std::string revision_text = name.substr(
revision_begin, suffix - revision_begin);
auto identity = parsePublicId(public_id);
std::int64_t revision = 0;
const auto parsed = std::from_chars(
revision_text.data(),
revision_text.data() + revision_text.size(),
revision);
if(identity && !revision_text.empty() &&
parsed.ec == std::errc{} &&
parsed.ptr == revision_text.data() + revision_text.size() &&
revision > 0)
{
return RecoveryName{public_id, revision};
}
}
marker = name.find("-r", marker + 2);
}
return std::nullopt;
}
mw::E<void> removeDirectory(const std::filesystem::path& path)
{
std::error_code filesystem_error;
std::filesystem::remove_all(path, filesystem_error);
if(filesystem_error)
{
return std::unexpected(mw::runtimeError(
"Failed to remove abandoned card assets: " +
filesystem_error.message()));
}
return {};
}
} // namespace
AssetStore::AssetStore(std::filesystem::path card_storage_root)
: published_root_(
std::move(card_storage_root) / "published")
{}
std::filesystem::path AssetStore::directory(
const std::string& public_id) const
{
return published_root_ / public_id;
}
mw::E<void> AssetStore::publish(
const std::filesystem::path& staging_directory,
const std::string& public_id) const
{
std::error_code filesystem_error;
std::filesystem::create_directories(
published_root_, filesystem_error);
if(filesystem_error)
{
return std::unexpected(mw::runtimeError(
"Failed to create the published asset root: " +
filesystem_error.message()));
}
const std::filesystem::path destination = published_root_ / public_id;
if(std::filesystem::exists(destination, filesystem_error))
{
return std::unexpected(mw::runtimeError(
"The destination card asset directory already exists"));
}
if(filesystem_error)
{
return std::unexpected(mw::runtimeError(
"Failed to inspect the published asset directory: " +
filesystem_error.message()));
}
std::filesystem::rename(
staging_directory, destination, filesystem_error);
if(filesystem_error)
{
return std::unexpected(mw::runtimeError(
"Failed to publish the card assets: " +
filesystem_error.message()));
}
return {};
}
mw::E<AssetReplacement> AssetStore::replace(
const std::filesystem::path& staging_directory,
const std::string& public_id,
std::int64_t previous_revision) const
{
const std::filesystem::path destination = published_root_ / public_id;
const std::filesystem::path previous = staging_directory.parent_path() /
("backup-" + public_id + "-r" +
std::to_string(previous_revision) + "-" +
staging_directory.filename().string());
std::error_code filesystem_error;
if(!std::filesystem::is_directory(destination, filesystem_error))
{
return std::unexpected(mw::runtimeError(
"The existing card asset directory is missing"));
}
if(filesystem_error)
{
return std::unexpected(mw::runtimeError(
"Failed to inspect the existing card assets: " +
filesystem_error.message()));
}
std::filesystem::rename(destination, previous, filesystem_error);
if(filesystem_error)
{
return std::unexpected(mw::runtimeError(
"Failed to retain the existing card assets: " +
filesystem_error.message()));
}
std::filesystem::rename(
staging_directory, destination, filesystem_error);
if(filesystem_error)
{
std::error_code restore_error;
std::filesystem::rename(previous, destination, restore_error);
return std::unexpected(mw::runtimeError(
"Failed to publish the edited card assets: " +
filesystem_error.message()));
}
return AssetReplacement{public_id, previous};
}
void AssetStore::restore(const AssetReplacement& replacement) const
{
const std::filesystem::path destination =
published_root_ / replacement.public_id;
std::error_code filesystem_error;
std::filesystem::remove_all(destination, filesystem_error);
if(!filesystem_error)
{
std::filesystem::rename(
replacement.previous_directory,
destination,
filesystem_error);
}
if(filesystem_error)
{
spdlog::critical(
"Failed to restore rolled-back assets for card {}: {}",
replacement.public_id,
filesystem_error.message());
}
}
void AssetStore::finish(const AssetReplacement& replacement) const
{
std::error_code filesystem_error;
std::filesystem::remove_all(
replacement.previous_directory, filesystem_error);
if(filesystem_error)
{
spdlog::warn(
"Failed to remove previous assets for card {}: {}",
replacement.public_id,
filesystem_error.message());
}
}
mw::E<TrashedAssets> AssetStore::trash(
const std::string& public_id,
std::int64_t revision,
const std::string& suffix) const
{
const std::filesystem::path source = published_root_ / public_id;
std::error_code filesystem_error;
if(!std::filesystem::exists(source, filesystem_error))
{
if(filesystem_error)
{
return std::unexpected(mw::runtimeError(
"Failed to inspect card assets for deletion: " +
filesystem_error.message()));
}
return TrashedAssets{public_id, {}};
}
const std::filesystem::path trash_root =
published_root_.parent_path() / ".trash";
std::filesystem::create_directories(trash_root, filesystem_error);
if(filesystem_error)
{
return std::unexpected(mw::runtimeError(
"Failed to create the private trash directory: " +
filesystem_error.message()));
}
const std::filesystem::path destination = trash_root /
(public_id + "-r" + std::to_string(revision) + "-" + suffix);
std::filesystem::rename(source, destination, filesystem_error);
if(filesystem_error)
{
return std::unexpected(mw::runtimeError(
"Failed to move card assets into private trash: " +
filesystem_error.message()));
}
return TrashedAssets{public_id, destination};
}
void AssetStore::restore(const TrashedAssets& trashed) const
{
if(trashed.trash_directory.empty())
{
return;
}
std::error_code filesystem_error;
std::filesystem::rename(
trashed.trash_directory,
published_root_ / trashed.public_id,
filesystem_error);
if(filesystem_error)
{
spdlog::critical(
"Failed to restore deleted assets for card {}: {}",
trashed.public_id,
filesystem_error.message());
}
}
void AssetStore::finish(const TrashedAssets& trashed) const
{
if(trashed.trash_directory.empty())
{
return;
}
std::error_code filesystem_error;
std::filesystem::remove_all(trashed.trash_directory, filesystem_error);
if(filesystem_error)
{
spdlog::warn(
"Failed to remove committed trash for card {}: {}",
trashed.public_id,
filesystem_error.message());
}
}
void AssetStore::removePublished(const std::string& public_id) const
{
std::error_code filesystem_error;
std::filesystem::remove_all(
published_root_ / public_id, filesystem_error);
if(filesystem_error)
{
spdlog::error(
"Failed to remove rolled-back assets for card {}: {}",
public_id,
filesystem_error.message());
}
}
mw::E<void> AssetStore::reconcile(DataSourceInterface& data_source) const
{
const std::filesystem::path storage_root = published_root_.parent_path();
const std::filesystem::path staging_root = storage_root / ".staging";
const std::filesystem::path trash_root = storage_root / ".trash";
std::error_code filesystem_error;
std::filesystem::create_directories(staging_root, filesystem_error);
if(filesystem_error)
{
return std::unexpected(mw::runtimeError(
"Failed to inspect staging during recovery: " +
filesystem_error.message()));
}
for(const std::filesystem::directory_entry& entry :
std::filesystem::directory_iterator(staging_root))
{
const std::string name = entry.path().filename().string();
auto recovery = parseRecoveryName(name, "backup-");
if(!recovery)
{
if(!name.starts_with("backup-"))
{
auto removed = removeDirectory(entry.path());
if(!removed)
{
return removed;
}
}
else
{
spdlog::warn("Leaving malformed recovery entry {}", name);
}
continue;
}
auto identity = parsePublicId(recovery->public_id);
auto card = data_source.getCard(
*identity, GameContentScope::INCLUDE_INTERNAL);
if(!card)
{
return std::unexpected(std::move(card.error()));
}
const std::filesystem::path canonical =
published_root_ / recovery->public_id;
if(*card && (**card).revision == recovery->revision)
{
auto removed = removeDirectory(canonical);
if(!removed)
{
return removed;
}
std::filesystem::rename(
entry.path(), canonical, filesystem_error);
if(filesystem_error)
{
return std::unexpected(mw::runtimeError(
"Failed to restore interrupted card edit: " +
filesystem_error.message()));
}
}
else if(*card && (**card).revision > recovery->revision)
{
auto removed = removeDirectory(entry.path());
if(!removed)
{
return removed;
}
}
else if(!*card)
{
auto removed = removeDirectory(entry.path());
if(!removed)
{
return removed;
}
}
else
{
return std::unexpected(mw::runtimeError(
"A card revision predates its recovery backup"));
}
}
std::filesystem::create_directories(trash_root, filesystem_error);
if(filesystem_error)
{
return std::unexpected(mw::runtimeError(
"Failed to inspect trash during recovery: " +
filesystem_error.message()));
}
for(const std::filesystem::directory_entry& entry :
std::filesystem::directory_iterator(trash_root))
{
auto recovery = parseRecoveryName(
entry.path().filename().string(), "");
if(!recovery)
{
spdlog::warn(
"Leaving malformed trash entry {}",
entry.path().filename().string());
continue;
}
auto identity = parsePublicId(recovery->public_id);
auto card = data_source.getCard(
*identity, GameContentScope::INCLUDE_INTERNAL);
if(!card)
{
return std::unexpected(std::move(card.error()));
}
const std::filesystem::path canonical =
published_root_ / recovery->public_id;
if(*card && !std::filesystem::exists(canonical))
{
std::filesystem::rename(
entry.path(), canonical, filesystem_error);
if(filesystem_error)
{
return std::unexpected(mw::runtimeError(
"Failed to restore interrupted card deletion: " +
filesystem_error.message()));
}
}
else if(!*card || std::filesystem::exists(canonical))
{
auto removed = removeDirectory(entry.path());
if(!removed)
{
return removed;
}
}
}
for(const std::filesystem::directory_entry& entry :
std::filesystem::directory_iterator(published_root_))
{
const std::string public_id = entry.path().filename().string();
auto identity = parsePublicId(public_id);
if(!identity)
{
spdlog::warn("Leaving non-card published entry {}", public_id);
continue;
}
auto card = data_source.getCard(
*identity, GameContentScope::INCLUDE_INTERNAL);
if(!card)
{
return std::unexpected(std::move(card.error()));
}
if(!*card)
{
auto removed = removeDirectory(entry.path());
if(!removed)
{
return removed;
}
}
}
return {};
}