Skip to content

Commit

Permalink
add a way to restore incremental backups
Browse files Browse the repository at this point in the history
closes #20
  • Loading branch information
MelanX committed Feb 20, 2024
1 parent 0ee293e commit 036d5c3
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 5 deletions.
15 changes: 15 additions & 0 deletions src/main/java/de/melanx/simplebackups/BackupData.java
Expand Up @@ -12,6 +12,7 @@ public class BackupData extends SavedData {
private long lastSaved;
private long lastFullBackup;
private boolean paused;
private boolean merging;

private BackupData() {
// use BackupData.get
Expand All @@ -29,6 +30,7 @@ public BackupData load(@Nonnull CompoundTag nbt) {
this.lastSaved = nbt.getLong("lastSaved");
this.lastFullBackup = nbt.getLong("lastFullBackup");
this.paused = nbt.getBoolean("paused");
this.merging = nbt.getBoolean("merging");
return this;
}

Expand All @@ -38,6 +40,7 @@ public CompoundTag save(@Nonnull CompoundTag nbt) {
nbt.putLong("lastSaved", this.lastSaved);
nbt.putLong("lastFullBackup", this.lastFullBackup);
nbt.putBoolean("paused", this.paused);
nbt.putBoolean("merging", this.merging);
return nbt;
}

Expand Down Expand Up @@ -67,4 +70,16 @@ public void updateFullBackupTime(long time) {
this.lastFullBackup = time;
this.setDirty();
}

public boolean isMerging() {
return merging;
}

public void startMerging() {
this.merging = true;
}

public void stopMerging() {
this.merging = false;
}
}
4 changes: 3 additions & 1 deletion src/main/java/de/melanx/simplebackups/EventListener.java
@@ -1,6 +1,7 @@
package de.melanx.simplebackups;

import de.melanx.simplebackups.commands.BackupCommand;
import de.melanx.simplebackups.commands.MergeCommand;
import de.melanx.simplebackups.commands.PauseCommand;
import de.melanx.simplebackups.config.CommonConfig;
import de.melanx.simplebackups.config.ServerConfig;
Expand All @@ -20,7 +21,8 @@ public void registerCommands(RegisterCommandsEvent event) {
event.getDispatcher().register(Commands.literal(SimpleBackups.MODID)
.requires(stack -> ServerConfig.commandsCheatsDisabled() || stack.hasPermission(2))
.then(BackupCommand.register())
.then(PauseCommand.register()));
.then(PauseCommand.register())
.then(MergeCommand.register()));
}

@SubscribeEvent
Expand Down
149 changes: 149 additions & 0 deletions src/main/java/de/melanx/simplebackups/commands/MergeCommand.java
@@ -0,0 +1,149 @@
package de.melanx.simplebackups.commands;

import com.mojang.brigadier.Command;
import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import de.melanx.simplebackups.BackupData;
import de.melanx.simplebackups.config.BackupType;
import de.melanx.simplebackups.config.CommonConfig;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

public class MergeCommand implements Command<CommandSourceStack> {


public static ArgumentBuilder<CommandSourceStack, ?> register() {
return Commands.literal("mergeBackups")
.executes(new MergeCommand());
}

@Override
public int run(CommandContext<CommandSourceStack> commandContext) throws CommandSyntaxException {
// Check if only modified files should be backed up
if (CommonConfig.backupType() == BackupType.FULL_BACKUPS) {
throw new SimpleCommandExceptionType(Component.translatable("simplebackups.commands.only_modified")).create();
}

BackupData data = BackupData.get(commandContext.getSource().getServer());

// Check if a merge operation is already in progress
if (data.isMerging()) {
throw new SimpleCommandExceptionType(Component.translatable("simplebackups.commands.is_merging")).create();
}

MergingThread mergingThread = new MergingThread(commandContext);
try {
data.startMerging();
mergingThread.start();
} catch (Exception e) {
e.printStackTrace();
data.stopMerging();
return 0;
}

data.stopMerging();
return 1;
}

private static class MergingThread extends Thread {

private final CommandContext<CommandSourceStack> commandContext;

public MergingThread(CommandContext<CommandSourceStack> commandContext) {
this.commandContext = commandContext;
}

@Override
public void run() {
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("merged_backup-" + UUID.randomUUID() + ".zip"))) {
Map<String, Path> zipFiles = new HashMap<>();

// Walk the file tree of the output path
Files.walkFileTree(CommonConfig.getOutputPath(), new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
MergingThread.this.processFile(file, zipFiles);
return FileVisitResult.CONTINUE;
}
});

// Write the merged zip file
this.writeMergedZipFile(zos, zipFiles);
} catch (IOException e) {
throw new IllegalStateException("Error while processing backups", e);
} finally {
commandContext.getSource().sendSuccess(() -> Component.translatable("simplebackups.commands.finished"), false);
}
}

private void processFile(Path file, Map<String, Path> zipFiles) throws IOException {
if (file.toString().endsWith(".zip")) {
try (ZipFile zipFile = new ZipFile(file.toFile())) {
Enumeration<? extends ZipEntry> entries = zipFile.entries();

while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String name = entry.getName();

zipFiles.merge(name, file, this::getLatestModifiedFile);
}
}
}
}

private Path getLatestModifiedFile(Path existingFile, Path newFile) {
try {
FileTime existingFileTime = Files.getLastModifiedTime(existingFile);
FileTime newFileTime = Files.getLastModifiedTime(newFile);
return existingFileTime.compareTo(newFileTime) > 0 ? existingFile : newFile;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

private void writeMergedZipFile(ZipOutputStream zos, Map<String, Path> zipFiles) throws IOException {
for (Map.Entry<String, Path> entry : zipFiles.entrySet()) {
String fileName = entry.getKey();
Path zipFilePath = entry.getValue();

try (ZipFile zipFile = new ZipFile(zipFilePath.toFile())) {
ZipEntry zipEntry = zipFile.getEntry(fileName);
if (zipEntry != null) {
zos.putNextEntry(new ZipEntry(fileName));

try (InputStream is = zipFile.getInputStream(zipEntry)) {
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) > 0) {
zos.write(buffer, 0, len);
}
}

zos.closeEntry();
}
}
}
}
}
}
5 changes: 4 additions & 1 deletion src/main/resources/assets/simplebackups/lang/de_de.json
@@ -1,5 +1,8 @@
{
"simplebackups.backup_started": "Backup gestartet...",
"simplebackups.backup_finished": "Backup fertiggestellt in %s (%s | %s)",
"simplebackups.backups_paused": "Backups pausiert"
"simplebackups.backups_paused": "Backups pausiert",
"simplebackups.commands.only_modified": "Es werden nicht nur veränderte Dateien gesichert, prüfe deine Konfigurationsdatei",
"simplebackups.commands.is_merging": "Ein Zusammenführungsvorgang ist bereits im Gange",
"simplebackups.commands.finished": "Zusammenführen der Backups erfolgreich abgeschlossen"
}
5 changes: 4 additions & 1 deletion src/main/resources/assets/simplebackups/lang/en_us.json
@@ -1,5 +1,8 @@
{
"simplebackups.backup_started": "Backup started...",
"simplebackups.backup_finished": "Backup completed in %s (%s | %s)",
"simplebackups.backups_paused": "Backups paused"
"simplebackups.backups_paused": "Backups paused",
"simplebackups.commands.only_modified": "Not only modified files are being backed up, please check your configuration file",
"simplebackups.commands.is_merging": "A merge operation is already in progress",
"simplebackups.commands.finished": "Merging backups completed successfully"
}
5 changes: 4 additions & 1 deletion src/main/resources/assets/simplebackups/lang/pt_br.json
@@ -1,5 +1,8 @@
{
"simplebackups.backup_started": "Backup iniciado...",
"simplebackups.backup_finished": "Backup concluído em %s (%s | %s)",
"simplebackups.backups_paused": "Backups pausados"
"simplebackups.backups_paused": "Backups pausados",
"simplebackups.commands.only_modified": "Não apenas arquivos modificados estão sendo salvos, por favor, verifique seu arquivo de configuração",
"simplebackups.commands.is_merging": "Uma operação de mesclagem já está em andamento",
"simplebackups.commands.finished": "Mesclagem de backups concluída com sucesso"
}
5 changes: 4 additions & 1 deletion src/main/resources/assets/simplebackups/lang/ru_ru.json
@@ -1,5 +1,8 @@
{
"simplebackups.backup_started": "Запущено резервное копирование...",
"simplebackups.backup_finished": "Резервное копирование завершено в %s (%s | %s)",
"simplebackups.backups_paused": "Резервное копирование приостановлено"
"simplebackups.backups_paused": "Резервное копирование приостановлено",
"simplebackups.commands.only_modified": "Не только изменённые файлы сохраняются, пожалуйста, проверьте ваш файл конфигурации",
"simplebackups.commands.is_merging": "Операция слияния уже выполняется",
"simplebackups.commands.finished": "Слияние резервных копий успешно завершено"
}

0 comments on commit 036d5c3

Please sign in to comment.