mirror of
https://github.com/mirkobrombin/raven.git
synced 2026-11-03 21:46:32 +00:00
127 lines
4.7 KiB
C++
127 lines
4.7 KiB
C++
#include "snapshot.h"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <ctime>
|
|
#include <stdexcept>
|
|
|
|
namespace raven {
|
|
|
|
namespace {
|
|
|
|
// Copies a tree: directories, symbolic links, and files with their modification times, which the next snapshot compares.
|
|
void copy_tree(const std::filesystem::path &from, const std::filesystem::path &to, const std::filesystem::path *previous)
|
|
{
|
|
std::filesystem::create_directories(to);
|
|
for (auto walk = std::filesystem::recursive_directory_iterator(from, std::filesystem::directory_options::skip_permission_denied);
|
|
walk != std::filesystem::recursive_directory_iterator(); ++walk) {
|
|
const auto relative = walk->path().lexically_relative(from);
|
|
const auto target = to / relative;
|
|
std::error_code error;
|
|
if (walk->is_symlink(error)) {
|
|
std::filesystem::copy_symlink(walk->path(), target);
|
|
} else if (walk->is_directory(error)) {
|
|
std::filesystem::create_directories(target);
|
|
} else if (walk->is_regular_file(error)) {
|
|
const auto size = walk->file_size(error);
|
|
const auto modified = walk->last_write_time(error);
|
|
if (previous) {
|
|
// Unchanged since the previous snapshot: both snapshots share the one copy, which neither ever changes.
|
|
const auto prior = *previous / relative;
|
|
std::error_code missing;
|
|
if (std::filesystem::is_regular_file(prior, missing) && std::filesystem::file_size(prior, missing) == size &&
|
|
std::filesystem::last_write_time(prior, missing) == modified && !missing) {
|
|
std::filesystem::create_hard_link(prior, target, error);
|
|
if (!error)
|
|
continue;
|
|
}
|
|
}
|
|
std::filesystem::copy_file(walk->path(), target, std::filesystem::copy_options::overwrite_existing);
|
|
std::filesystem::last_write_time(target, modified);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string timestamp()
|
|
{
|
|
const std::time_t now = std::time(nullptr);
|
|
std::tm parts{};
|
|
localtime_r(&now, &parts);
|
|
char text[32];
|
|
std::strftime(text, sizeof(text), "%Y%m%d-%H%M%S", &parts);
|
|
return text;
|
|
}
|
|
|
|
}
|
|
|
|
std::filesystem::path snapshot_directory(const std::filesystem::path &prefix)
|
|
{
|
|
const auto plain = prefix.lexically_normal();
|
|
const auto name = plain.has_filename() ? plain.filename() : plain.parent_path().filename();
|
|
const auto parent = plain.has_filename() ? plain.parent_path() : plain.parent_path().parent_path();
|
|
return parent / (name.string() + ".snapshots");
|
|
}
|
|
|
|
std::vector<std::filesystem::path> list_snapshots(const std::filesystem::path &prefix)
|
|
{
|
|
std::vector<std::filesystem::path> found;
|
|
std::error_code error;
|
|
for (const auto &entry : std::filesystem::directory_iterator(snapshot_directory(prefix), error))
|
|
if (entry.is_directory(error) && entry.path().extension() != ".partial")
|
|
found.push_back(entry.path());
|
|
std::sort(found.begin(), found.end());
|
|
return found;
|
|
}
|
|
|
|
std::filesystem::path take_snapshot(const std::filesystem::path &prefix, std::string_view label)
|
|
{
|
|
const auto store = snapshot_directory(prefix);
|
|
std::filesystem::create_directories(store);
|
|
const auto existing = list_snapshots(prefix);
|
|
std::string name = timestamp() + "-";
|
|
for (const char character : label)
|
|
name.push_back(std::isalnum(static_cast<unsigned char>(character)) || character == '-' || character == '.' ? character : '_');
|
|
// Two snapshots in the same second keep apart.
|
|
const std::string first = name;
|
|
for (int count = 2; std::filesystem::exists(store / name); ++count)
|
|
name = first + "~" + std::to_string(count);
|
|
const auto partial = store / (name + ".partial");
|
|
std::filesystem::remove_all(partial);
|
|
copy_tree(prefix, partial, existing.empty() ? nullptr : &existing.back());
|
|
std::filesystem::rename(partial, store / name);
|
|
return store / name;
|
|
}
|
|
|
|
std::filesystem::path restore_snapshot(const std::filesystem::path &prefix, std::optional<std::string> name)
|
|
{
|
|
const auto snapshots = list_snapshots(prefix);
|
|
std::filesystem::path chosen;
|
|
if (name) {
|
|
chosen = snapshot_directory(prefix) / *name;
|
|
if (!std::filesystem::is_directory(chosen))
|
|
throw std::runtime_error("no snapshot named " + *name);
|
|
} else {
|
|
if (snapshots.empty())
|
|
throw std::runtime_error("the prefix has no snapshot");
|
|
chosen = snapshots.back();
|
|
}
|
|
const auto kept = take_snapshot(prefix, "before-restore");
|
|
std::error_code error;
|
|
for (const auto &entry : std::filesystem::directory_iterator(prefix, error))
|
|
std::filesystem::remove_all(entry.path());
|
|
copy_tree(chosen, prefix, nullptr);
|
|
return kept;
|
|
}
|
|
|
|
void prune_snapshots(const std::filesystem::path &prefix, std::string_view marker, size_t keep)
|
|
{
|
|
std::vector<std::filesystem::path> marked;
|
|
for (const auto &snapshot : list_snapshots(prefix))
|
|
if (snapshot.filename().string().find(marker) != std::string::npos)
|
|
marked.push_back(snapshot);
|
|
for (size_t index = 0; index + keep < marked.size(); ++index)
|
|
std::filesystem::remove_all(marked[index]);
|
|
}
|
|
|
|
}
|