Merge pull request #102 from Szum123321/safe_restore

Safe restore
2.x-1.17
Szum123321 2022-06-22 20:04:25 +02:00 committed by GitHub
commit 267776789d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 320 additions and 288 deletions

View File

@ -6,7 +6,7 @@ yarn_mappings=1.19+build.4
loader_version=0.14.8
#Fabric api
fabric_version=0.56.0+1.19
fabric_version=0.56.1+1.19
#Cloth Config
cloth_version=7.0.72
@ -15,12 +15,12 @@ cloth_version=7.0.72
modmenu_version=4.0.0
#Lazy DFU for faster dev start
lazydfu_version=0.1.2
lazydfu_version=v0.1.3
#Hash of commit form which parallel gzip will be build
pgzip_commit_hash=af5f5c297e735f3f2df7aa4eb0e19a5810b8aff6
# Mod Properties
mod_version = 2.3.1
mod_version = 2.4.0
maven_group = net.szum123321
archives_base_name = textile_backup

View File

@ -20,7 +20,7 @@ package net.szum123321.textile_backup;
import net.szum123321.textile_backup.core.restore.AwaitThread;
import java.io.File;
import java.nio.file.Path;
import java.time.format.DateTimeFormatter;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
@ -35,6 +35,6 @@ public class Statics {
public static final AtomicBoolean globalShutdownBackupFlag = new AtomicBoolean(true);
public static boolean disableWatchdog = false;
public static AwaitThread restoreAwaitThread = null;
public static Optional<File> untouchableFile = Optional.empty();
public static Optional<Path> untouchableFile = Optional.empty();
public static boolean disableTMPFiles = false;
}

View File

@ -85,12 +85,16 @@ public class TextileLogger {
log(Level.ERROR, msg, data);
}
void error(String message, Throwable throwable) {
logger.error(prefix + message, throwable);
}
public void fatal(String msg, Object... data) {
log(Level.FATAL, msg, data);
}
boolean sendFeedback(Level level, ServerCommandSource source, String msg, Object... args) {
if(source != null && source.getEntity() instanceof PlayerEntity) {
if(source != null && source.isExecutedByPlayer()) {
MutableText text = Text.literal(messageFactory.newMessage(msg, args).getFormattedMessage());
if(level.intLevel() == Level.TRACE.intLevel()) text.formatted(Formatting.GREEN);
@ -116,7 +120,7 @@ public class TextileLogger {
}
public void sendInfo(BackupContext context, String msg, Object... args) {
sendInfo(context.getCommandSource(), msg, args);
sendInfo(context.commandSource(), msg, args);
}
public void sendError(ServerCommandSource source, String msg, Object... args) {
@ -124,7 +128,7 @@ public class TextileLogger {
}
public void sendError(BackupContext context, String msg, Object... args) {
sendError(context.getCommandSource(), msg, args);
sendError(context.commandSource(), msg, args);
}
public void sendToPlayerAndLog(Level level, ServerCommandSource source, String msg, Object... args) {
@ -138,7 +142,7 @@ public class TextileLogger {
}
public void sendInfoAL(BackupContext context, String msg, Object... args) {
sendInfoAL(context.getCommandSource(), msg, args);
sendInfoAL(context.commandSource(), msg, args);
}
public void sendErrorAL(ServerCommandSource source, String msg, Object... args) {
@ -146,6 +150,6 @@ public class TextileLogger {
}
public void sendErrorAL(BackupContext context, String msg, Object... args) {
sendErrorAL(context.getCommandSource(), msg, args);
sendErrorAL(context.commandSource(), msg, args);
}
}

View File

@ -21,7 +21,6 @@ package net.szum123321.textile_backup.commands.manage;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.TextileBackup;
@ -31,11 +30,13 @@ import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.commands.FileSuggestionProvider;
import net.szum123321.textile_backup.core.Utilities;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Stream;
public class DeleteCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
@ -57,28 +58,28 @@ public class DeleteCommand {
throw CommandExceptions.DATE_TIME_PARSE_COMMAND_EXCEPTION_TYPE.create(e);
}
File root = Utilities.getBackupRootPath(Utilities.getLevelName(source.getServer()));
Path root = Utilities.getBackupRootPath(Utilities.getLevelName(source.getServer()));
Optional<File> optionalFile = Arrays.stream(root.listFiles())
.filter(Utilities::isValidBackup)
try (Stream<Path> stream = Files.list(root)) {
stream.filter(Utilities::isValidBackup)
.filter(file -> Utilities.getFileCreationTime(file).orElse(LocalDateTime.MIN).equals(dateTime))
.findFirst();
.findFirst().ifPresent(file -> {
if(Statics.untouchableFile.isEmpty() || !Statics.untouchableFile.get().equals(file)) {
try {
Files.delete(file);
log.sendInfo(source, "File {} successfully deleted!", file);
if(optionalFile.isPresent()) {
if(Statics.untouchableFile.isEmpty() || !Statics.untouchableFile.get().equals(optionalFile.get())) {
if(optionalFile.get().delete()) {
log.sendInfo(source, "File {} successfully deleted!", optionalFile.get().getName());
if(source.getEntity() instanceof PlayerEntity)
log.info("Player {} deleted {}.", source.getPlayer().getName(), optionalFile.get().getName());
} else {
if(source.isExecutedByPlayer())
log.info("Player {} deleted {}.", source.getPlayer().getName(), file);
} catch (IOException e) {
log.sendError(source, "Something went wrong while deleting file!");
}
} else {
log.sendError(source, "Couldn't delete the file because it's being restored right now.");
log.sendHint(source, "If you want to abort restoration then use: /backup killR");
}
} else {
});
} catch (IOException ignored) {
log.sendError(source, "Couldn't find file by this name.");
log.sendHint(source, "Maybe try /backup list");
}

View File

@ -82,7 +82,7 @@ public class RestoreBackupCommand {
Optional<RestoreHelper.RestoreableFile> backupFile = RestoreHelper.findFileAndLockIfPresent(dateTime, source.getServer());
if(backupFile.isPresent()) {
log.info("Found file to restore {}", backupFile.get().getFile().getName());
log.info("Found file to restore {}", backupFile.get().getFile().getFileName().toString());
} else {
log.sendInfo(source, "No file created on {} was found!", dateTime.format(Statics.defaultDateTimeFormatter));

View File

@ -32,18 +32,20 @@ import net.szum123321.textile_backup.config.ConfigPOJO;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.mixin.MinecraftServerSessionAccessor;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.file.SimplePathVisitor;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Optional;
import java.util.UUID;
public class Utilities {
private final static ConfigHelper config = ConfigHelper.INSTANCE;
@ -60,28 +62,47 @@ public class Utilities {
return ((MinecraftServerSessionAccessor)server).getSession().getDirectoryName();
}
public static File getWorldFolder(MinecraftServer server) {
public static Path getWorldFolder(MinecraftServer server) {
return ((MinecraftServerSessionAccessor)server)
.getSession()
.getWorldDirectory(World.OVERWORLD)
.toFile();
.getWorldDirectory(World.OVERWORLD);
}
public static File getBackupRootPath(String worldName) {
File path = new File(config.get().path).getAbsoluteFile();
public static Path getBackupRootPath(String worldName) {
Path path = Path.of(config.get().path).toAbsolutePath();
if (config.get().perWorldBackup) path = path.toPath().resolve(worldName).toFile();
if (config.get().perWorldBackup) path = path.resolve(worldName);
if (!path.exists()) path.mkdirs();
try {
Files.createDirectories(path);
} catch (IOException e) {
//I REALLY shouldn't be handling this here
}
return path;
}
public static void deleteDirectory(Path path) throws IOException {
Files.walkFileTree(path, new SimplePathVisitor() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}
public static void updateTMPFSFlag(MinecraftServer server) {
boolean flag = false;
Path tmp_dir = Path.of(System.getProperty("java.io.tmpdir"));
if(
FileUtils.sizeOfDirectory(Utilities.getWorldFolder(server)) >=
FileUtils.sizeOfDirectory(Utilities.getWorldFolder(server).toFile()) >=
tmp_dir.toFile().getUsableSpace()
) {
log.error("Not enough space left in TMP directory! ({})", tmp_dir);
@ -130,11 +151,11 @@ public class Utilities {
.findAny();
}
public static Optional<ConfigPOJO.ArchiveFormat> getArchiveExtension(File f) {
return getArchiveExtension(f.getName());
public static Optional<ConfigPOJO.ArchiveFormat> getArchiveExtension(Path f) {
return getArchiveExtension(f.getFileName().toString());
}
public static Optional<LocalDateTime> getFileCreationTime(File file) {
public static Optional<LocalDateTime> getFileCreationTime(Path file) {
LocalDateTime creationTime = null;
if(getArchiveExtension(file).isPresent()) {
@ -143,7 +164,7 @@ public class Utilities {
try {
creationTime = LocalDateTime.from(
Utilities.getDateTimeFormatter().parse(
file.getName().split(fileExtension)[0].split("#")[0]
file.getFileName().toString().split(fileExtension)[0].split("#")[0]
)
);
} catch (Exception ignored) {}
@ -152,7 +173,7 @@ public class Utilities {
try {
creationTime = LocalDateTime.from(
Utilities.getBackupDateTimeFormatter().parse(
file.getName().split(fileExtension)[0].split("#")[0]
file.getFileName().toString().split(fileExtension)[0].split("#")[0]
)
);
} catch (Exception ignored2){}
@ -160,7 +181,7 @@ public class Utilities {
if(creationTime == null) {
try {
FileTime fileTime = (FileTime) Files.getAttribute(file.toPath(), "creationTime");
FileTime fileTime = (FileTime) Files.getAttribute(file, "creationTime");
creationTime = LocalDateTime.ofInstant(fileTime.toInstant(), ZoneOffset.systemDefault());
} catch (IOException ignored3) {}
}
@ -169,7 +190,7 @@ public class Utilities {
return Optional.ofNullable(creationTime);
}
public static boolean isValidBackup(File f) {
public static boolean isValidBackup(Path f) {
return getArchiveExtension(f).isPresent() && getFileCreationTime(f).isPresent() && isFileOk(f);
}
@ -177,6 +198,10 @@ public class Utilities {
return f.exists() && f.isFile();
}
public static boolean isFileOk(Path f) {
return Files.exists(f) && Files.isRegularFile(f);
}
public static DateTimeFormatter getDateTimeFormatter() {
return DateTimeFormatter.ofPattern(config.get().dateTimeFormat);
}

View File

@ -33,18 +33,6 @@ public record BackupContext(@NotNull MinecraftServer server,
boolean save,
String comment) {
public MinecraftServer getServer() {
return server;
}
public ServerCommandSource getCommandSource() {
return commandSource;
}
public ActionInitiator getInitiator() {
return initiator;
}
public boolean startedByPlayer() {
return initiator == ActionInitiator.Player;
}
@ -53,14 +41,6 @@ public record BackupContext(@NotNull MinecraftServer server,
return save;
}
public String getComment() {
return comment;
}
public UUID getInitiatorUUID() {
return initiator.equals(ActionInitiator.Player) && commandSource.getEntity() != null ? commandSource.getEntity().getUuid(): Util.NIL_UUID;
}
public static class Builder {
private MinecraftServer server;
private ServerCommandSource commandSource;

View File

@ -24,13 +24,16 @@ import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.core.Utilities;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.Comparator;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Stream;
public class BackupHelper {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
@ -38,7 +41,7 @@ public class BackupHelper {
public static Runnable create(BackupContext ctx) {
if(config.get().broadcastBackupStart) {
Utilities.notifyPlayers(ctx.getServer(),
Utilities.notifyPlayers(ctx.server(),
"Warning! Server backup will begin shortly. You may experience some lag."
);
} else {
@ -49,12 +52,12 @@ public class BackupHelper {
builder.append("Backup started ");
builder.append(ctx.getInitiator().getPrefix());
builder.append(ctx.initiator().getPrefix());
if(ctx.startedByPlayer())
builder.append(ctx.getCommandSource().getDisplayName().getString());
builder.append(ctx.commandSource().getDisplayName().getString());
else
builder.append(ctx.getInitiator().getName());
builder.append(ctx.initiator().getName());
builder.append(" on: ");
builder.append(Utilities.getDateTimeFormatter().format(LocalDateTime.now()));
@ -64,10 +67,10 @@ public class BackupHelper {
if (ctx.shouldSave()) {
log.sendInfoAL(ctx, "Saving server...");
ctx.getServer().getPlayerManager().saveAllPlayerData();
ctx.server().getPlayerManager().saveAllPlayerData();
try {
ctx.getServer().save(false, true, true);
ctx.server().save(false, true, true);
} catch (Exception e) {
log.sendErrorAL(ctx,"An exception occurred when trying to save the world!");
}
@ -77,52 +80,105 @@ public class BackupHelper {
}
public static int executeFileLimit(ServerCommandSource ctx, String worldName) {
File root = Utilities.getBackupRootPath(worldName);
Path root = Utilities.getBackupRootPath(worldName);
int deletedFiles = 0;
if (root.isDirectory() && root.exists() && root.listFiles() != null) {
if (Files.isDirectory(root) && Files.exists(root) && !isEmpty(root)) {
if (config.get().maxAge > 0) { // delete files older that configured
final LocalDateTime now = LocalDateTime.now();
deletedFiles += Arrays.stream(root.listFiles())
try(Stream<Path> stream = Files.list(root)) {
deletedFiles += stream
.filter(Utilities::isValidBackup)// We check if we can get file's creation date so that the next line won't throw an exception
.filter(f -> now.toEpochSecond(ZoneOffset.UTC) - Utilities.getFileCreationTime(f).get().toEpochSecond(ZoneOffset.UTC) > config.get().maxAge)
.map(f -> deleteFile(f, ctx))
.filter(b -> b).count(); //a bit awkward
.mapToInt(f -> deleteFile(f, ctx))
.sum();
} catch (IOException e) {
log.error("An exception occurred while trying to delete old files!", e);
}
}
if (config.get().backupsToKeep > 0 && root.listFiles().length > config.get().backupsToKeep) {
deletedFiles += Arrays.stream(root.listFiles())
.filter(Utilities::isValidBackup)
.sorted(Comparator.comparing(f -> Utilities.getFileCreationTime((File) f).get()).reversed())
.skip(config.get().backupsToKeep)
.map(f -> deleteFile(f, ctx))
.filter(b -> b).count();
}
int noToKeep = config.get().backupsToKeep > 0 ? config.get().backupsToKeep : Integer.MAX_VALUE;
long maxSize = config.get().maxSize > 0 ? config.get().maxSize : Long.MAX_VALUE;
if (config.get().maxSize > 0 && FileUtils.sizeOfDirectory(root) / 1024 > config.get().maxSize) {
deletedFiles += Arrays.stream(root.listFiles())
AtomicInteger currentNo = new AtomicInteger(countBackups(root));
AtomicLong currentSize = new AtomicLong(countSize(root));
try(Stream<Path> stream = Files.list(root)) {
deletedFiles += stream
.filter(Utilities::isValidBackup)
.sorted(Comparator.comparing(f -> Utilities.getFileCreationTime(f).get()))
.takeWhile(f -> FileUtils.sizeOfDirectory(root) / 1024 > config.get().maxSize)
.map(f -> deleteFile(f, ctx))
.filter(b -> b).count();
.takeWhile(f -> (currentNo.get() > noToKeep) || (currentSize.get() > maxSize))
.peek(f -> currentNo.decrementAndGet())
.peek(f -> {
try {
currentSize.addAndGet(Files.size(f));
} catch (IOException e) {
currentSize.set(0);
}
})
.mapToInt(f -> deleteFile(f, ctx))
.sum();
} catch (IOException e) {
log.error("An exception occurred while trying to delete old files!", e);
}
}
return deletedFiles;
}
private static boolean deleteFile(File f, ServerCommandSource ctx) {
if(Statics.untouchableFile.isEmpty()|| !Statics.untouchableFile.get().equals(f)) {
if(f.delete()) {
log.sendInfoAL(ctx, "Deleting: {}", f.getName());
return true;
} else {
log.sendErrorAL(ctx, "Something went wrong while deleting: {}.", f.getName());
private static int countBackups(Path path) {
try(Stream<Path> stream = Files.list(path)) {
return (int) stream
.filter(Utilities::isValidBackup)
.count();
} catch (IOException ignored) {}
return 0;
}
private static long countSize(Path path) {
try(Stream<Path> stream = Files.list(path)) {
return (int) stream
.filter(Utilities::isValidBackup)
.mapToLong(f -> {
try {
return Files.size(f);
} catch (IOException e) {
return 0;
}
})
.sum();
} catch (IOException ignored) {}
return 0;
}
private static boolean isEmpty(Path path) {
if (Files.isDirectory(path)) {
try (Stream<Path> entries = Files.list(path)) {
return entries.findFirst().isEmpty();
} catch (IOException e) {
return false;
}
}
return false;
}
//1 -> ok, 0 -> err
private static int deleteFile(Path f, ServerCommandSource ctx) {
if(Statics.untouchableFile.isEmpty()|| !Statics.untouchableFile.get().equals(f)) {
try {
Files.delete(f);
log.sendInfoAL(ctx, "Deleting: {}", f);
} catch (IOException e) {
if(ctx.isExecutedByPlayer()) log.sendError(ctx, "Something went wrong while deleting: {}.", f);
log.error("Something went wrong while deleting: {}.", f, e);
return 0;
}
return 1;
}
return 0;
}
}

View File

@ -30,9 +30,10 @@ import net.szum123321.textile_backup.core.create.compressors.tar.ParallelBZip2Co
import net.szum123321.textile_backup.core.create.compressors.tar.ParallelGzipCompressor;
import org.apache.commons.compress.compressors.lzma.LZMACompressorOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
public class MakeBackupRunnable implements Runnable {
@ -48,33 +49,30 @@ public class MakeBackupRunnable implements Runnable {
@Override
public void run() {
try {
Utilities.disableWorldSaving(context.getServer());
Utilities.disableWorldSaving(context.server());
Statics.disableWatchdog = true;
Utilities.updateTMPFSFlag(context.getServer());
Utilities.updateTMPFSFlag(context.server());
log.sendInfoAL(context, "Starting backup");
File world = Utilities.getWorldFolder(context.getServer());
Path world = Utilities.getWorldFolder(context.server());
log.trace("Minecraft world is: {}", world);
File outFile = Utilities
.getBackupRootPath(Utilities.getLevelName(context.getServer()))
.toPath()
.resolve(getFileName())
.toFile();
Path outFile = Utilities
.getBackupRootPath(Utilities.getLevelName(context.server()))
.resolve(getFileName());
log.trace("Outfile is: {}", outFile);
outFile.getParentFile().mkdirs();
try {
outFile.createNewFile();
Files.createDirectories(outFile.getParent());
Files.createFile(outFile);
} catch (IOException e) {
log.error("An exception occurred when trying to create new backup file!", e);
if(context.getInitiator() == ActionInitiator.Player)
if(context.initiator() == ActionInitiator.Player)
log.sendError(context, "An exception occurred when trying to create new backup file!");
return;
@ -92,10 +90,13 @@ public class MakeBackupRunnable implements Runnable {
switch (config.get().format) {
case ZIP -> {
if (coreCount > 1 && !Statics.disableTMPFiles)
if (coreCount > 1 && !Statics.disableTMPFiles) {
ParallelZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
else
log.trace("Using PARALLEL Zip Compressor. Threads: {}", coreCount);
} else {
ZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
log.trace("Using REGULAR Zip Compressor. Threads: {}");
}
}
case BZIP2 -> ParallelBZip2Compressor.getInstance().createArchive(world, outFile, context, coreCount);
case GZIP -> ParallelGzipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
@ -105,26 +106,20 @@ public class MakeBackupRunnable implements Runnable {
}
}.createArchive(world, outFile, context, coreCount);
case TAR -> new AbstractTarArchiver().createArchive(world, outFile, context, coreCount);
default -> {
log.warn("Specified compressor ({}) is not supported! Zip will be used instead!", config.get().format);
if (context.getInitiator() == ActionInitiator.Player)
log.sendError(context.getCommandSource(), "Error! No correct compression format specified! Using default compressor!");
ZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
}
}
BackupHelper.executeFileLimit(context.getCommandSource(), Utilities.getLevelName(context.getServer()));
BackupHelper.executeFileLimit(context.commandSource(), Utilities.getLevelName(context.server()));
if(config.get().broadcastBackupDone) {
Utilities.notifyPlayers(
context.getServer(),
context.server(),
"Done!"
);
} else {
log.sendInfoAL(context, "Done!");
}
} finally {
Utilities.enableWorldSaving(context.getServer());
Utilities.enableWorldSaving(context.server());
Statics.disableWatchdog = false;
}
}
@ -133,7 +128,7 @@ public class MakeBackupRunnable implements Runnable {
LocalDateTime now = LocalDateTime.now();
return Utilities.getDateTimeFormatter().format(now) +
(context.getComment() != null ? "#" + context.getComment().replaceAll("[\\\\/:*?\"<>|#]", "") : "") +
(context.comment() != null ? "#" + context.comment().replaceAll("[\\\\/:*?\"<>|#]", "") : "") +
config.get().format.getCompleteString();
}
}

View File

@ -31,29 +31,29 @@ import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ExecutionException;
import java.util.stream.Stream;
public abstract class AbstractCompressor {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public void createArchive(File inputFile, File outputFile, BackupContext ctx, int coreLimit) {
public void createArchive(Path inputFile, Path outputFile, BackupContext ctx, int coreLimit) {
Instant start = Instant.now();
try (FileOutputStream outStream = new FileOutputStream(outputFile);
try (OutputStream outStream = Files.newOutputStream(outputFile);
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outStream);
OutputStream arc = createArchiveOutputStream(bufferedOutputStream, ctx, coreLimit)) {
OutputStream arc = createArchiveOutputStream(bufferedOutputStream, ctx, coreLimit);
Stream<Path> fileStream = Files.walk(inputFile)) {
Files.walk(inputFile.toPath())
.filter(path -> !Utilities.isBlacklisted(inputFile.toPath().relativize(path)))
.map(Path::toFile)
.filter(File::isFile)
.forEach(file -> {
fileStream
.filter(path -> !Utilities.isBlacklisted(inputFile.relativize(path)))
.filter(Files::isRegularFile).forEach(file -> {
try {
//hopefully one broken file won't spoil the whole archive
addEntry(file, inputFile.toPath().relativize(file.toPath()).toString(), arc);
addEntry(file, inputFile.relativize(file).toString(), arc);
} catch (IOException e) {
log.error("An exception occurred while trying to compress: {}", inputFile.toPath().relativize(file.toPath()).toString(), e);
log.error("An exception occurred while trying to compress: {}", inputFile.relativize(file).toString(), e);
if (ctx.getInitiator() == ActionInitiator.Player)
if (ctx.initiator() == ActionInitiator.Player)
log.sendError(ctx, "Something went wrong while compressing files!");
}
});
@ -67,14 +67,14 @@ public abstract class AbstractCompressor {
For help see: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems
In case this isn't it here's also the exception itself""", e);
if(ctx.getInitiator() == ActionInitiator.Player) {
if(ctx.initiator() == ActionInitiator.Player) {
log.sendError(ctx, "Backup failed. The file is corrupt.");
log.error("For help see: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems");
}
} catch (IOException | InterruptedException | ExecutionException e) {
log.error("An exception occurred!", e);
} catch (Exception e) {
if(ctx.getInitiator() == ActionInitiator.Player)
if(ctx.initiator() == ActionInitiator.Player)
log.sendError(ctx, "Something went wrong while compressing files!");
} finally {
close();
@ -86,13 +86,13 @@ public abstract class AbstractCompressor {
}
protected abstract OutputStream createArchiveOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) throws IOException;
protected abstract void addEntry(File file, String entryName, OutputStream arc) throws IOException;
protected abstract void addEntry(Path file, String entryName, OutputStream arc) throws IOException;
protected void finish(OutputStream arc) throws InterruptedException, ExecutionException, IOException {
;//Basically this function is only needed for the ParallelZipCompressor to write out ParallelScatterZipCreator
//Basically this function is only needed for the ParallelZipCompressor to write out ParallelScatterZipCreator
}
protected void close() {
;//Same as above, just for ParallelGzipCompressor to shutdown ExecutorService
//Same as above, just for ParallelGzipCompressor to shut down ExecutorService
}
}

View File

@ -26,6 +26,8 @@ import org.apache.commons.compress.archivers.zip.*;
import org.apache.commons.compress.parallel.InputStreamSupplier;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.zip.ZipEntry;
@ -65,13 +67,13 @@ public class ParallelZipCompressor extends ZipCompressor {
}
@Override
protected void addEntry(File file, String entryName, OutputStream arc) throws IOException {
protected void addEntry(Path file, String entryName, OutputStream arc) throws IOException {
ZipArchiveEntry entry = (ZipArchiveEntry)((ZipArchiveOutputStream)arc).createArchiveEntry(file, entryName);
if(ZipCompressor.isDotDat(file.getName())) {
entry.setMethod(ZipArchiveOutputStream.STORED);
entry.setSize(file.length());
entry.setCompressedSize(file.length());
if(ZipCompressor.isDotDat(file.getFileName().toString())) {
entry.setMethod(ZipEntry.STORED);
entry.setSize(Files.size(file));
entry.setCompressedSize(Files.size(file));
entry.setCrc(getCRC(file));
} else entry.setMethod(ZipEntry.DEFLATED);
@ -126,12 +128,12 @@ public class ParallelZipCompressor extends ZipCompressor {
}
}
record FileInputStreamSupplier(File sourceFile) implements InputStreamSupplier {
record FileInputStreamSupplier(Path sourceFile) implements InputStreamSupplier {
public InputStream get() {
try {
return new FileInputStream(sourceFile);
return Files.newInputStream(sourceFile);
} catch (IOException e) {
log.error("An exception occurred while trying to create an input stream from file: {}!", sourceFile.getName(), e);
log.error("An exception occurred while trying to create an input stream from file: {}!", sourceFile.toString(), e);
}
return null;

View File

@ -27,9 +27,12 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.utils.IOUtils;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.zip.CRC32;
import java.util.zip.Checksum;
import java.util.zip.ZipEntry;
public class ZipCompressor extends AbstractCompressor {
private final static ConfigHelper config = ConfigHelper.INSTANCE;
@ -51,14 +54,14 @@ public class ZipCompressor extends AbstractCompressor {
}
@Override
protected void addEntry(File file, String entryName, OutputStream arc) throws IOException {
try (FileInputStream fileInputStream = new FileInputStream(file)){
protected void addEntry(Path file, String entryName, OutputStream arc) throws IOException {
try (InputStream fileInputStream = Files.newInputStream(file)){
ZipArchiveEntry entry = (ZipArchiveEntry)((ZipArchiveOutputStream)arc).createArchiveEntry(file, entryName);
if(isDotDat(file.getName())) {
entry.setMethod(ZipArchiveOutputStream.STORED);
entry.setSize(file.length());
entry.setCompressedSize(file.length());
if(isDotDat(file.getFileName().toString())) {
entry.setMethod(ZipEntry.STORED);
entry.setSize(Files.size(file));
entry.setCompressedSize(Files.size(file));
entry.setCrc(getCRC(file));
}
@ -76,15 +79,15 @@ public class ZipCompressor extends AbstractCompressor {
return arr[arr.length - 1].contains("dat"); //includes dat_old
}
protected static long getCRC(File file) throws IOException {
protected static long getCRC(Path file) throws IOException {
Checksum sum = new CRC32();
byte[] buffer = new byte[8192];
int len;
try (InputStream stream = new FileInputStream(file)) {
try (InputStream stream = Files.newInputStream(file)) {
while ((len = stream.read(buffer)) != -1) sum.update(buffer, 0, len);
} catch (IOException e) {
throw new IOException("Error while calculating CRC of: " + file.getAbsolutePath(), e);
throw new IOException("Error while calculating CRC of: " + file.toAbsolutePath(), e);
}
return sum.getValue();

View File

@ -24,10 +24,9 @@ import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.utils.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
public class AbstractTarArchiver extends AbstractCompressor {
protected OutputStream getCompressorOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) throws IOException {
@ -44,8 +43,8 @@ public class AbstractTarArchiver extends AbstractCompressor {
}
@Override
protected void addEntry(File file, String entryName, OutputStream arc) throws IOException {
try (FileInputStream fileInputStream = new FileInputStream(file)){
protected void addEntry(Path file, String entryName, OutputStream arc) throws IOException {
try (InputStream fileInputStream = Files.newInputStream(file)){
TarArchiveEntry entry = (TarArchiveEntry)((TarArchiveOutputStream) arc).createArchiveEntry(file, entryName);
((TarArchiveOutputStream)arc).putArchiveEntry(entry);

View File

@ -31,7 +31,9 @@ import net.szum123321.textile_backup.core.create.BackupHelper;
import net.szum123321.textile_backup.core.restore.decompressors.GenericTarDecompressor;
import net.szum123321.textile_backup.core.restore.decompressors.ZipDecompressor;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class RestoreBackupRunnable implements Runnable {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
@ -49,54 +51,57 @@ public class RestoreBackupRunnable implements Runnable {
log.info("Shutting down server...");
ctx.getServer().stop(false);
ctx.server().stop(false);
awaitServerShutdown();
if(config.get().backupOldWorlds) {
BackupHelper.create(
BackupContext.Builder
.newBackupContextBuilder()
.setServer(ctx.getServer())
.setServer(ctx.server())
.setInitiator(ActionInitiator.Restore)
.setComment("Old_World" + (ctx.getComment() != null ? "_" + ctx.getComment() : ""))
.setComment("Old_World" + (ctx.comment() != null ? "_" + ctx.comment() : ""))
.build()
).run();
}
File worldFile = Utilities.getWorldFolder(ctx.getServer());
Path worldFile = Utilities.getWorldFolder(ctx.server());
log.info("Deleting old world...");
if(!deleteDirectory(worldFile))
log.error("Something went wrong while deleting old world!");
worldFile.mkdirs();
try {
Path tmp = Files.createTempDirectory(
worldFile.getParent(),
ctx.restoreableFile().getFile().getFileName().toString());
log.info("Starting decompression...");
if(ctx.getFile().getArchiveFormat() == ConfigPOJO.ArchiveFormat.ZIP)
ZipDecompressor.decompress(ctx.getFile().getFile(), worldFile);
if (ctx.restoreableFile().getArchiveFormat() == ConfigPOJO.ArchiveFormat.ZIP)
ZipDecompressor.decompress(ctx.restoreableFile().getFile(), tmp);
else
GenericTarDecompressor.decompress(ctx.getFile().getFile(), worldFile);
GenericTarDecompressor.decompress(ctx.restoreableFile().getFile(), tmp);
if(config.get().deleteOldBackupAfterRestore) {
log.info("Deleting old world...");
Utilities.deleteDirectory(worldFile);
Files.move(tmp, worldFile);
if (config.get().deleteOldBackupAfterRestore) {
log.info("Deleting old backup");
if(!ctx.getFile().getFile().delete()) log.info("Something went wrong while deleting old backup");
Files.delete(ctx.restoreableFile().getFile());
}
} catch (IOException e) {
log.error("An exception occurred while trying to restore a backup!", e);
}
//in case we're playing on client
Statics.globalShutdownBackupFlag.set(true);
log.info("Done!");
//Might solve #37
//Idk if it's a good idea...
//Runtime.getRuntime().exit(0);
}
private void awaitServerShutdown() {
while(((LivingServer)ctx.getServer()).isAlive()) {
while(((LivingServer)ctx.server()).isAlive()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
@ -104,15 +109,4 @@ public class RestoreBackupRunnable implements Runnable {
}
}
}
private static boolean deleteDirectory(File f) {
boolean state = true;
if(f.isDirectory()) {
for(File f2 : f.listFiles())
state &= deleteDirectory(f2);
}
return f.delete() && state;
}
}

View File

@ -21,43 +21,15 @@ package net.szum123321.textile_backup.core.restore;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.util.Util;
import net.szum123321.textile_backup.core.ActionInitiator;
import javax.annotation.Nullable;
import java.util.UUID;
public record RestoreContext(RestoreHelper.RestoreableFile file,
public record RestoreContext(RestoreHelper.RestoreableFile restoreableFile,
MinecraftServer server,
@Nullable String comment,
ActionInitiator initiator,
ServerCommandSource commandSource) {
public RestoreHelper.RestoreableFile getFile() {
return file;
}
public MinecraftServer getServer() {
return server;
}
@Nullable
public String getComment() {
return comment;
}
public ActionInitiator getInitiator() {
return initiator;
}
public UUID getInitiatorUUID() {
return initiator.equals(ActionInitiator.Player) && commandSource.getEntity() != null ? commandSource.getEntity().getUuid(): Util.NIL_UUID;
}
public ServerCommandSource getCommandSource() {
return commandSource;
}
public static final class Builder {
private RestoreHelper.RestoreableFile file;
private MinecraftServer server;

View File

@ -28,23 +28,31 @@ import net.szum123321.textile_backup.core.ActionInitiator;
import net.szum123321.textile_backup.core.Utilities;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class RestoreHelper {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE;
public static Optional<RestoreableFile> findFileAndLockIfPresent(LocalDateTime backupTime, MinecraftServer server) {
File root = Utilities.getBackupRootPath(Utilities.getLevelName(server));
Path root = Utilities.getBackupRootPath(Utilities.getLevelName(server));
Optional<RestoreableFile> optionalFile = Arrays.stream(root.listFiles())
Optional<RestoreableFile> optionalFile;
try (Stream<Path> stream = Files.list(root)) {
optionalFile = stream
.map(RestoreableFile::newInstance)
.flatMap(Optional::stream)
.filter(rf -> rf.getCreationTime().equals(backupTime))
.findFirst();
} catch (IOException e) {
throw new RuntimeException(e);
}
Statics.untouchableFile = optionalFile.map(RestoreableFile::getFile);
@ -52,8 +60,8 @@ public class RestoreHelper {
}
public static AwaitThread create(RestoreContext ctx) {
if(ctx.getInitiator() == ActionInitiator.Player)
log.info("Backup restoration was initiated by: {}", ctx.getCommandSource().getName());
if(ctx.initiator() == ActionInitiator.Player)
log.info("Backup restoration was initiated by: {}", ctx.commandSource().getName());
else
log.info("Backup restoration was initiated form Server Console");
@ -69,28 +77,32 @@ public class RestoreHelper {
}
public static List<RestoreableFile> getAvailableBackups(MinecraftServer server) {
File root = Utilities.getBackupRootPath(Utilities.getLevelName(server));
Path root = Utilities.getBackupRootPath(Utilities.getLevelName(server));
return Arrays.stream(root.listFiles())
.filter(Utilities::isValidBackup)
try (Stream<Path> stream = Files.list(root)) {
return stream.filter(Utilities::isValidBackup)
.map(RestoreableFile::newInstance)
.flatMap(Optional::stream)
.collect(Collectors.toList());
} catch (IOException e) {
log.error("Error while listing available backups", e);
return new LinkedList<>();
}
}
public static class RestoreableFile implements Comparable<RestoreableFile> {
private final File file;
private final Path file;
private final ConfigPOJO.ArchiveFormat archiveFormat;
private final LocalDateTime creationTime;
private final String comment;
private RestoreableFile(File file) throws NoSuchElementException {
private RestoreableFile(Path file) throws NoSuchElementException {
this.file = file;
archiveFormat = Utilities.getArchiveExtension(file).orElseThrow(() -> new NoSuchElementException("Couldn't get file extension!"));
String extension = archiveFormat.getCompleteString();
creationTime = Utilities.getFileCreationTime(file).orElseThrow(() -> new NoSuchElementException("Couldn't get file creation time!"));
final String filename = file.getName();
final String filename = file.getFileName().toString();
if(filename.split("#").length > 1) {
this.comment = filename.split("#")[1].split(extension)[0];
@ -99,7 +111,7 @@ public class RestoreHelper {
}
}
public static Optional<RestoreableFile> newInstance(File file) {
public static Optional<RestoreableFile> newInstance(Path file) {
try {
return Optional.of(new RestoreableFile(file));
} catch (NoSuchElementException ignored) {}
@ -107,7 +119,7 @@ public class RestoreHelper {
return Optional.empty();
}
public File getFile() {
public Path getFile() {
return file;
}

View File

@ -29,16 +29,17 @@ import org.apache.commons.compress.utils.IOUtils;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
public class GenericTarDecompressor {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static void decompress(File input, File target) {
public static void decompress(Path input, Path target) throws IOException {
Instant start = Instant.now();
try (InputStream fileInputStream = new FileInputStream(input);
try (InputStream fileInputStream = Files.newInputStream(input);
InputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
InputStream compressorInputStream = getCompressorInputStream(bufferedInputStream);
TarArchiveInputStream archiveInputStream = new TarArchiveInputStream(compressorInputStream)) {
@ -50,27 +51,20 @@ public class GenericTarDecompressor {
continue;
}
File file = target.toPath().resolve(entry.getName()).toFile();
Path file = target.resolve(entry.getName());
if(entry.isDirectory()) {
file.mkdirs();
Files.createDirectories(file);
} else {
File parent = file.getParentFile();
if (!parent.isDirectory() && !parent.mkdirs()) {
log.error("Failed to create {}", parent);
} else {
try (OutputStream outputStream = Files.newOutputStream(file.toPath());
Files.createDirectories(file.getParent());
try (OutputStream outputStream = Files.newOutputStream(file);
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) {
IOUtils.copy(archiveInputStream, bufferedOutputStream);
} catch (IOException e) {
log.error("An exception occurred while trying to decompress file: {}", file.getName(), e);
}
}
}
}
} catch (IOException | CompressorException e) {
log.error("An exception occurred! ", e);
} catch (CompressorException e) {
throw new IOException(e);
}
log.info("Decompression took {} seconds.", Utilities.formatDuration(Duration.between(start, Instant.now())));

View File

@ -21,43 +21,38 @@ package net.szum123321.textile_backup.core.restore.decompressors;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.Utilities;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.apache.commons.compress.utils.IOUtils;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.Iterator;
public class ZipDecompressor {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static void decompress(File inputFile, File target) {
public static void decompress(Path inputFile, Path target) throws IOException {
Instant start = Instant.now();
try(ZipFile zipFile = new ZipFile(inputFile)) {
zipFile.getEntries().asIterator().forEachRemaining(entry -> {
File file = target.toPath().resolve(entry.getName()).toFile();
try(ZipFile zipFile = new ZipFile(inputFile.toFile())) {
for (Iterator<ZipArchiveEntry> it = zipFile.getEntries().asIterator(); it.hasNext(); ) {
ZipArchiveEntry entry = it.next();
Path file = target.resolve(entry.getName());
if(entry.isDirectory()) {
file.mkdirs();
Files.createDirectories(file);
} else {
File parent = file.getParentFile();
if (!parent.isDirectory() && !parent.mkdirs()) {
log.error("Failed to create {}", parent);
} else {
try (OutputStream outputStream = Files.newOutputStream(file.toPath());
Files.createDirectories(file.getParent());
try (OutputStream outputStream = Files.newOutputStream(file);
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) {
IOUtils.copy(zipFile.getInputStream(entry), bufferedOutputStream);
} catch (IOException e) {
log.error("An exception occurred while trying to decompress file: {}", entry.getName(), e);
}
}
}
});
} catch (IOException e) {
log.error("An exception occurred! ", e);
}
log.info("Decompression took: {} seconds.", Utilities.formatDuration(Duration.between(start, Instant.now())));