feat: extend WorldBackup
This commit is contained in:
1 parent
aa4bd109fe
commit
5cf0d6cc06
1 file changed
+164
@@ -154,3 +154,167 @@ std::vector<uint32_t> BackupIndex::chainTo(uint32_t id, bool& complete,
|
|||||||
uint32_t BackupStore::capture(BackupKind kind, const ByteBuffer& save, uint64_t tick) {
|
uint32_t BackupStore::capture(BackupKind kind, const ByteBuffer& save, uint64_t tick) {
|
||||||
failure.clear();
|
failure.clear();
|
||||||
|
|
||||||
|
IntegrityReport report = scanner.scan(save.data(), save.size());
|
||||||
|
if (!report.headerReadable) {
|
||||||
|
failure = "save is not a readable container, nothing to back up";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (report.countAtLeast(IssueSeverity::Recoverable) != 0) {
|
||||||
|
failure = "save reports " + std::to_string(report.countAtLeast(IssueSeverity::Recoverable)) +
|
||||||
|
" unresolved integrity findings; repair it before backing it up";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!isCanonicalSaveLayout(save.data(), save.size())) {
|
||||||
|
failure =
|
||||||
|
"save layout is fragmented; compact it first, a payload level backup cannot reproduce "
|
||||||
|
"hole placement byte for byte";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveHeader header;
|
||||||
|
std::vector<LiveChunk> current;
|
||||||
|
if (!sliceContainer(save, header, current)) {
|
||||||
|
failure = "save table does not address its own payloads";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
BackupEntry entry;
|
||||||
|
entry.kind = kind;
|
||||||
|
entry.tick = tick;
|
||||||
|
entry.version = static_cast<SaveVersion>(header.version);
|
||||||
|
entry.regionX = header.regionX;
|
||||||
|
entry.regionZ = header.regionZ;
|
||||||
|
entry.sectorBytes = header.sectorBytes;
|
||||||
|
|
||||||
|
const BackupEntry* parent = nullptr;
|
||||||
|
if (kind == BackupKind::Incremental) {
|
||||||
|
uint32_t parentId = chain.latestId();
|
||||||
|
parent = chain.find(parentId);
|
||||||
|
if (parent == nullptr) {
|
||||||
|
failure = "no full backup to build on; capture a full backup first";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
bool complete = false;
|
||||||
|
std::string walkFailure;
|
||||||
|
chain.chainTo(parentId, complete, walkFailure);
|
||||||
|
if (!complete) {
|
||||||
|
failure = "cannot extend a broken chain: " + walkFailure;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (parent->version != entry.version || parent->regionX != entry.regionX ||
|
||||||
|
parent->regionZ != entry.regionZ) {
|
||||||
|
failure = "chain covers region (" + std::to_string(parent->regionX) + ", " +
|
||||||
|
std::to_string(parent->regionZ) + ") at " + saveVersionName(parent->version) +
|
||||||
|
", the save is region (" + std::to_string(entry.regionX) + ", " +
|
||||||
|
std::to_string(entry.regionZ) + ") at " + saveVersionName(entry.version);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (tick < parent->tick) {
|
||||||
|
failure = "backup tick " + std::to_string(tick) + " runs behind parent " +
|
||||||
|
idText(parent->id) + " at tick " + std::to_string(parent->tick);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
entry.parentId = parentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const LiveChunk& chunk : current) {
|
||||||
|
BackupChunk stamp;
|
||||||
|
stamp.coord = chunk.coord;
|
||||||
|
stamp.checksum = chunk.checksum;
|
||||||
|
entry.state.push_back(stamp);
|
||||||
|
|
||||||
|
bool changed = true;
|
||||||
|
if (parent != nullptr) {
|
||||||
|
const BackupChunk* before = parent->findState(chunk.coord);
|
||||||
|
changed = before == nullptr || before->checksum != chunk.checksum;
|
||||||
|
}
|
||||||
|
if (!changed) continue;
|
||||||
|
BackupChunk written;
|
||||||
|
written.coord = chunk.coord;
|
||||||
|
written.checksum = chunk.checksum;
|
||||||
|
written.payload = chunk.payload;
|
||||||
|
entry.stored.push_back(std::move(written));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent != nullptr) {
|
||||||
|
for (const BackupChunk& before : parent->state) {
|
||||||
|
bool stillHere = false;
|
||||||
|
for (const LiveChunk& chunk : current)
|
||||||
|
if (chunk.coord == before.coord) stillHere = true;
|
||||||
|
if (!stillHere) entry.removed.push_back(before.coord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.id = nextId++;
|
||||||
|
chain.entries.push_back(std::move(entry));
|
||||||
|
return chain.entries.back().id;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t BackupStore::captureMigrated(BackupKind kind, const ByteBuffer& save, uint64_t tick) {
|
||||||
|
failure.clear();
|
||||||
|
SaveMigrator migrator;
|
||||||
|
MigrationResult migrated = migrator.migrateToCurrent(save);
|
||||||
|
if (!migrated.ok) {
|
||||||
|
failure = "migration to " + std::string(saveVersionName(currentSaveVersion)) +
|
||||||
|
" failed: " + migrated.failure;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return capture(kind, migrated.output, tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
RestoreResult BackupStore::restore(uint32_t id) const {
|
||||||
|
RestoreResult result;
|
||||||
|
result.fromId = id;
|
||||||
|
|
||||||
|
bool complete = false;
|
||||||
|
std::string walkFailure;
|
||||||
|
std::vector<uint32_t> walk = chain.chainTo(id, complete, walkFailure);
|
||||||
|
if (!complete) {
|
||||||
|
result.failure = walkFailure;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result.chain = walk;
|
||||||
|
|
||||||
|
std::vector<LiveChunk> live;
|
||||||
|
const BackupEntry* target = chain.find(id);
|
||||||
|
for (std::size_t step = 0; step < walk.size(); ++step) {
|
||||||
|
const BackupEntry* entry = chain.find(walk[step]);
|
||||||
|
for (const Vec3i& coord : entry->removed) {
|
||||||
|
eraseLive(live, coord);
|
||||||
|
++result.chunksRemoved;
|
||||||
|
}
|
||||||
|
for (const BackupChunk& chunk : entry->stored) {
|
||||||
|
insertLive(live, chunk.coord, chunk.checksum, chunk.payload);
|
||||||
|
if (entry->kind == BackupKind::Full) ++result.chunksFromFull;
|
||||||
|
else ++result.chunksFromIncrements;
|
||||||
|
}
|
||||||
|
if (entry->kind == BackupKind::Incremental) ++result.incrementsApplied;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (live.size() != target->state.size()) {
|
||||||
|
result.failure = "restored " + std::to_string(live.size()) + " chunks but backup " +
|
||||||
|
idText(id) + " recorded " + std::to_string(target->state.size());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
for (const BackupChunk& stamp : target->state) {
|
||||||
|
std::size_t at = findLive(live, stamp.coord);
|
||||||
|
if (at == live.size() || live[at].checksum != stamp.checksum) {
|
||||||
|
result.failure = "restored chunk (" + std::to_string(stamp.coord.x) + ", " +
|
||||||
|
std::to_string(stamp.coord.y) + ", " + std::to_string(stamp.coord.z) +
|
||||||
|
") does not match the checksum backup " + idText(id) + " recorded";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(live.begin(), live.end(), [](const LiveChunk& a, const LiveChunk& b) {
|
||||||
|
return saveCoordLess(a.coord, b.coord);
|
||||||
|
});
|
||||||
|
std::vector<Vec3i> coords;
|
||||||
|
std::vector<ByteBuffer> payloads;
|
||||||
|
coords.reserve(live.size());
|
||||||
|
payloads.reserve(live.size());
|
||||||
|
for (LiveChunk& chunk : live) {
|
||||||
|
coords.push_back(chunk.coord);
|
||||||
|
payloads.push_back(std::move(chunk.payload));
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user