package com.forgeessentials.backup;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.IChatComponent;
import net.minecraft.util.IProgressUpdate;
import net.minecraft.world.MinecraftException;
import net.minecraft.world.WorldServer;
import net.minecraftforge.common.DimensionManager;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.config.ConfigCategory;
import net.minecraftforge.common.config.Configuration;
import net.minecraftforge.common.config.Property;
import net.minecraftforge.event.world.WorldEvent;
import net.minecraftforge.permission.PermissionLevel;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import com.forgeessentials.api.APIRegistry;
import com.forgeessentials.api.UserIdent;
import com.forgeessentials.core.ForgeEssentials;
import com.forgeessentials.core.misc.TaskRegistry;
import com.forgeessentials.core.moduleLauncher.FEModule;
import com.forgeessentials.core.moduleLauncher.config.ConfigLoader.ConfigLoaderBase;
import com.forgeessentials.util.ServerUtil;
import com.forgeessentials.util.events.FEModuleEvent.FEModuleInitEvent;
import com.forgeessentials.util.events.FEModuleEvent.FEModuleServerInitEvent;
import com.forgeessentials.util.output.ChatOutputHandler;
import com.forgeessentials.util.output.LoggingHandler;
import cpw.mods.fml.common.FMLCommonHandler;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
@FEModule(name = "Backups", parentMod = ForgeEssentials.class)
public class ModuleBackup extends ConfigLoaderBase
{
public static final String PERM = "fe.backup";
public static final String PERM_NOTIFY = PERM + ".notify";
public static final String CONFIG_CAT = "Backup";
public static final String CONFIG_CAT_WORLDS = CONFIG_CAT + ".Worlds";
public static final String WORLDS_HELP = "Add world configurations in the format \"B:1=true\"";
private static final String EXCLUDE_PATTERNS_HELP = "Define file patterns (regex) that should be excluded from each backup";
public static final String[] DEFAULT_EXCLUDE_PATTERNS = new String[] { "DIM-?\\d+", "FEMultiworld", "FEData_backup", "DimensionalDoors", };
public static final SimpleDateFormat FILE_FORMAT = new SimpleDateFormat("yyyy-MM-dd_HH-mm");
/* ------------------------------------------------------------ */
public static boolean backupDefault;
public static int backupInterval;
public static boolean backupOnUnload;
public static boolean backupOnLoad;
public static int keepBackups;
public static int dailyBackups;
public static int weeklyBackups;
public static Map<Integer, Boolean> backupOverrides = new HashMap<>();
public static List<Pattern> exludePatterns = new ArrayList<>();
private static Runnable backupTask = new Runnable() {
@Override
public void run()
{
backupAll();
}
};
private static Thread backupThread;
/* ------------------------------------------------------------ */
@FEModule.ModuleDir
public static File moduleDir;
public static File baseFolder;
/* ------------------------------------------------------------ */
@SubscribeEvent
public void load(FEModuleInitEvent e)
{
// FECommandManager.registerCommand(new CommandBackup());
MinecraftForge.EVENT_BUS.register(this);
}
@SubscribeEvent
public void serverStarting(FEModuleServerInitEvent e)
{
APIRegistry.perms.registerPermission(PERM_NOTIFY, PermissionLevel.OP, "Backup notification permission");
registerBackupTask();
cleanBackups();
}
private void registerBackupTask()
{
TaskRegistry.getInstance().remove(backupTask);
if (backupInterval > 0)
TaskRegistry.getInstance().scheduleRepeated(backupTask, 1000 * 60 * backupInterval);
}
@SubscribeEvent
public void worldLoadEvent(WorldEvent.Load event)
{
if (!FMLCommonHandler.instance().getEffectiveSide().isServer() || !backupOnLoad)
return;
final WorldServer world = (WorldServer) event.world;
if (shouldBackup(world))
{
Thread thread = new Thread(new Runnable() {
@Override
public void run()
{
backup(world, true);
}
});
thread.start();
}
}
@SubscribeEvent
public void worldUnloadEvent(WorldEvent.Unload event)
{
if (!FMLCommonHandler.instance().getEffectiveSide().isServer() || !backupOnUnload)
return;
final WorldServer world = (WorldServer) event.world;
if (shouldBackup(world))
{
Thread thread = new Thread(new Runnable() {
@Override
public void run()
{
backup(world, true);
}
});
thread.start();
}
}
@Override
public void load(Configuration config, boolean isReload)
{
backupDefault = config.get(CONFIG_CAT, "backup_default", true, "Backup all worlds by default").getBoolean();
backupInterval = config.get(CONFIG_CAT, "backup_interval", 60, "Automatic backup interval in minutes (0 to disable)").getInt();
backupOnLoad = config.get(CONFIG_CAT, "backup_on_load", true, "Always backup worlds when loaded (server starts)").getBoolean();
backupOnUnload = config.get(CONFIG_CAT, "backup_on_unload", true, "Always backup when a world is unloaded").getBoolean();
keepBackups = config.get(CONFIG_CAT, "keep_backups", 12, "Keep at least this amount of last backups").getInt();
dailyBackups = config.get(CONFIG_CAT, "keep_daily_backups", 7, "Keep at least one daily backup for this last number of last days").getInt();
weeklyBackups = config.get(CONFIG_CAT, "keep_weekly_backups", 8, "Keep at least one weekly backup for this last number of weeks").getInt();
config.get(CONFIG_CAT_WORLDS, "0", true).getBoolean(); // Create default entry
ConfigCategory worldCat = config.getCategory(CONFIG_CAT_WORLDS);
worldCat.setComment(WORLDS_HELP);
for (Entry<String, Property> world : worldCat.entrySet())
{
try
{
if (world.getValue().isBooleanValue())
backupOverrides.put(Integer.parseInt(world.getKey()), world.getValue().getBoolean());
}
catch (NumberFormatException e)
{
LoggingHandler.felog.error("Invalid backup override entry!");
}
}
String[] exludePatternValues = config.get(CONFIG_CAT, "exclude_patterns", DEFAULT_EXCLUDE_PATTERNS, EXCLUDE_PATTERNS_HELP).getStringList();
exludePatterns.clear();
for (String pattern : exludePatternValues)
{
try
{
exludePatterns.add(Pattern.compile(pattern, Pattern.CASE_INSENSITIVE));
}
catch (PatternSyntaxException e)
{
LoggingHandler.felog.error(String.format("Invalid backup exclude pattern %s", pattern));
}
}
if (MinecraftServer.getServer() != null && MinecraftServer.getServer().isServerRunning())
registerBackupTask();
}
/* ------------------------------------------------------------ */
public static void backupAll()
{
if (backupThread != null)
return;
backupThread = new Thread(new Runnable() {
@Override
public void run()
{
try
{
List<Integer> backupDims = new ArrayList<>();
List<WorldServer> backupWorlds = new ArrayList<>();
for (WorldServer world : DimensionManager.getWorlds())
if (shouldBackup(world))
{
backupDims.add(world.provider.dimensionId);
backupWorlds.add(world);
}
ModuleBackup.notify(String.format("Starting backup of dimensions %s", StringUtils.join(backupDims, ", ")));
for (WorldServer worldServer : backupWorlds)
backup(worldServer, false);
cleanBackups();
ModuleBackup.notify("Backup finished!");
}
finally
{
backupThread = null;
}
}
});
backupThread.start();
}
protected static boolean shouldBackup(WorldServer world)
{
Boolean shouldBackup = backupOverrides.get(world.provider.dimensionId);
if (shouldBackup == null)
return backupDefault;
else
return shouldBackup;
}
private static synchronized void backup(WorldServer world, boolean notify)
{
if (notify)
notify(String.format("Starting backup of dim %d...", world.provider.dimensionId));
// Save world
if (!saveWorld(world))
{
notify(String.format("Backup of dim %s failed: Could not save world", world.provider.dimensionId));
return;
}
// Prepare directory
URI baseUri = ServerUtil.getWorldPath().toURI();
File backupFile = getBackupFile(world);
File backupDir = backupFile.getParentFile();
if (!backupDir.exists())
if (!backupDir.mkdirs())
{
notify(String.format("Backup of dim %s failed: Could not create backup directory", world.provider.dimensionId));
return;
}
// Save files
try (FileOutputStream fileStream = new FileOutputStream(backupFile); //
ZipOutputStream zipStream = new ZipOutputStream(fileStream);)
{
LoggingHandler.felog.info(String.format("Listing files for backup of world %d", world.provider.dimensionId));
for (File file : enumWorldFiles(world, world.getChunkSaveLocation(), null))
{
String relativePath = baseUri.relativize(file.toURI()).getPath();
try (FileInputStream in = new FileInputStream(file))
{
ZipEntry ze = new ZipEntry(relativePath);
zipStream.putNextEntry(ze);
IOUtils.copy(in, zipStream);
}
catch (IOException e)
{
LoggingHandler.felog.warn(String.format("Unable to backup file %s", relativePath));
}
}
zipStream.closeEntry();
}
catch (Exception ex)
{
LoggingHandler.felog.error(String.format("Severe error during backup of dim %d", world.provider.dimensionId));
ex.printStackTrace();
if (notify)
notify(String.format("Error during backup of dim %d", world.provider.dimensionId));
}
if (notify)
notify("Backup finished");
}
private static List<File> enumWorldFiles(WorldServer world, File dir, List<File> files)
{
if (files == null)
files = new ArrayList<>();
mainLoop: for (File file : dir.listFiles())
{
if (!file.isDirectory())
{
files.add(file);
continue;
}
// Exclude directories of other worlds
for (WorldServer otherWorld : DimensionManager.getWorlds())
if (otherWorld.provider.dimensionId != world.provider.dimensionId && otherWorld.getChunkSaveLocation().equals(file))
continue mainLoop;
for (Pattern pattern : exludePatterns)
if (pattern.matcher(file.getName()).matches())
continue mainLoop;
enumWorldFiles(world, file, files);
}
return files;
}
private static File getBackupFile(WorldServer world)
{
return new File(moduleDir, String.format("%s/DIM_%d/%s.zip", //
world.getWorldInfo().getWorldName(), //
world.provider.dimensionId, //
FILE_FORMAT.format(new Date())));
}
private static boolean saveWorld(WorldServer world)
{
boolean oldLevelSaving = world.levelSaving;
world.levelSaving = false;
try
{
world.saveAllChunks(true, (IProgressUpdate) null);
return true;
}
catch (MinecraftException e)
{
LoggingHandler.felog.error(String.format("Could not save world %d", world.provider.dimensionId));
return false;
}
catch (Exception e)
{
LoggingHandler.felog.error("Error while saving world");
return false;
}
finally
{
world.levelSaving = oldLevelSaving;
}
}
private static void cleanBackups()
{
File baseDir = new File(moduleDir, DimensionManager.getWorld(0).getWorldInfo().getWorldName());
if (!baseDir.exists())
return;
for (File backupDir : baseDir.listFiles())
{
if (!backupDir.isDirectory())
continue;
SortedMap<Calendar, File> files = new TreeMap<>();
for (File backupFile : backupDir.listFiles())
{
try
{
Calendar date = Calendar.getInstance();
date.setTime(FILE_FORMAT.parse(FilenameUtils.getBaseName(backupFile.getName())));
files.put(date, backupFile);
}
catch (ParseException e)
{
LoggingHandler.felog.error(String.format("Could not parse backup file %s", backupFile.getAbsolutePath()));
}
}
Calendar now = Calendar.getInstance();
Calendar oldestDailyBackup = Calendar.getInstance();
oldestDailyBackup.set(Calendar.MILLISECOND, 0);
oldestDailyBackup.set(Calendar.SECOND, 0);
oldestDailyBackup.set(Calendar.HOUR_OF_DAY, 4);
oldestDailyBackup.add(Calendar.DAY_OF_YEAR, dailyBackups <= 0 ? -1000 : -dailyBackups);
Calendar oldestWeeklyBackup = Calendar.getInstance();
oldestDailyBackup.set(Calendar.MILLISECOND, 0);
oldestDailyBackup.set(Calendar.SECOND, 0);
oldestDailyBackup.set(Calendar.HOUR_OF_DAY, 4);
oldestWeeklyBackup.set(Calendar.DAY_OF_WEEK, 0);
oldestWeeklyBackup.add(Calendar.WEEK_OF_YEAR, weeklyBackups <= 0 ? -1000 : -weeklyBackups);
Calendar oldestBackup = oldestDailyBackup.before(oldestWeeklyBackup) ? oldestDailyBackup : oldestWeeklyBackup;
int index = 0;
for (Iterator<Entry<Calendar, File>> it = files.entrySet().iterator(); it.hasNext();)
{
Entry<Calendar, File> backup = it.next();
if (index++ > files.size() - keepBackups)
{
it.remove();
}
else if (backup.getKey().before(oldestBackup))
{
if (!backup.getValue().delete())
LoggingHandler.felog.error(String.format("Could not delete backup file %s", backup.getValue().getAbsolutePath()));
it.remove();
}
}
while (oldestDailyBackup.before(now))
{
Calendar nextDate = (Calendar) oldestDailyBackup.clone();
nextDate.add(Calendar.DAY_OF_YEAR, 1);
boolean first = true;
for (Iterator<Entry<Calendar, File>> it = files.entrySet().iterator(); it.hasNext();)
{
Entry<Calendar, File> backup = it.next();
if (backup.getKey().before(oldestDailyBackup))
continue;
if (first)
{
first = false;
continue;
}
if (backup.getKey().after(nextDate))
break;
if (!backup.getValue().delete())
LoggingHandler.felog.error(String.format("Could not delete backup file %s", backup.getValue().getAbsolutePath()));
it.remove();
}
oldestDailyBackup = nextDate;
}
while (oldestWeeklyBackup.before(now))
{
Calendar nextDate = (Calendar) oldestWeeklyBackup.clone();
nextDate.add(Calendar.WEEK_OF_YEAR, 1);
boolean first = true;
for (Iterator<Entry<Calendar, File>> it = files.entrySet().iterator(); it.hasNext();)
{
Entry<Calendar, File> backup = it.next();
if (backup.getKey().before(oldestWeeklyBackup))
continue;
if (first)
{
first = false;
continue;
}
if (backup.getKey().after(nextDate))
break;
if (!backup.getValue().delete())
LoggingHandler.felog.error(String.format("Could not delete backup file %s", backup.getValue().getAbsolutePath()));
it.remove();
}
oldestWeeklyBackup = nextDate;
}
}
}
private static void notify(String message)
{
IChatComponent messageComponent = ChatOutputHandler.notification(message);
if (!MinecraftServer.getServer().isServerStopped())
for (EntityPlayerMP player : ServerUtil.getPlayerList())
if (UserIdent.get(player).checkPermission(PERM_NOTIFY))
ChatOutputHandler.sendMessage(player, messageComponent);
ChatOutputHandler.sendMessage(MinecraftServer.getServer(), messageComponent);
}
}