package speedytools.serverside.backup;
import net.minecraft.nbt.CompressedStreamTools;
import net.minecraft.nbt.NBTBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagString;
import speedytools.common.utilities.ErrorLog;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
/**
* User: The Grey Ghost
* Date: 1/03/14
* Maintains a record of the backed up save folders
*/
public class StoredBackups
{
public StoredBackups() {
this(new HashMap<Integer, Path>());
}
public StoredBackups(HashMap<Integer, Path> i_backupListing) {
backupListing = i_backupListing;
}
private static final String BACKUP_LISTING_VERSION_TAG = "VERSION";
private static final String BACKUP_LISTING_PATHS_TAG = "PATHS";
private static final String BACKUP_LISTING_NAME_VALUE = "BACKUPLISTING";
private static final String BACKUP_LISTING_VERSION_VALUE = "V1";
/**
* parses the backuplisting file into a map of backup number vs path
* in case of error, parses as much as possible.
* does not check if the paths actually exist
* If there is no backupListing file, the backupListing will be initialised to empty
* If the file is incomplete or has an error, the backupListing will contain any valid entries read from the file
* @param backupListingPath
* @return true for success, false otherwise
*/
public boolean retrieveBackupListing(Path backupListingPath)
{
backupListing = new HashMap<Integer, Path>();
if (!Files.isRegularFile(backupListingPath) || !Files.isReadable(backupListingPath)) {
return false;
}
try {
NBTTagCompound backupListingNBT = CompressedStreamTools.read(backupListingPath.toFile());
if (!backupListingNBT.hasKey(BACKUP_LISTING_VERSION_TAG) || !backupListingNBT.hasKey(BACKUP_LISTING_PATHS_TAG)) {
ErrorLog.defaultLog().info("Invalid backuplisting file (missing tag): " + backupListingPath.toString());
return false;
}
if (!backupListingNBT.getString(BACKUP_LISTING_VERSION_TAG).equals(BACKUP_LISTING_VERSION_VALUE)) {
ErrorLog.defaultLog().info("Invalid backuplisting file (version wrong): " + backupListingPath.toString());
return false;
}
backupListingNBT = backupListingNBT.getCompoundTag(BACKUP_LISTING_PATHS_TAG);
Set<String> tagKeys = backupListingNBT.getKeySet();
Iterator<String> iterator = tagKeys.iterator(); // backupListingNBT.getTags().iterator();
while (iterator.hasNext())
{
String key = iterator.next();
String value = backupListingNBT.getString(key);
Integer backupNumber;
try {
backupNumber = new Integer(Integer.parseInt(key));
} catch (NumberFormatException nfe) {
ErrorLog.defaultLog().info("Invalid backupNumber tag in backuplisting file: " + backupListingPath.toString());
return false;
}
if (backupListing.containsKey(backupNumber)) {
ErrorLog.defaultLog().info("Duplicate backupNumber tag in backuplisting file: " + backupListingPath.toString());
return false;
}
backupListing.put(backupNumber, Paths.get(value));
}
} catch (IOException ioe) {
ErrorLog.defaultLog().info("Failure while reading backuplisting file (" + backupListingPath.toString() + ") :" + ioe.toString());
return false;
}
return true;
}
/**
* writes the given backups to the given Path; overwrites automatically if already exists
* @return true if successful, false otherwise.
*/
public boolean saveBackupListing(Path fileToCreate)
{
try {
NBTTagCompound backupListingNBT = new NBTTagCompound(); //BACKUP_LISTING_NAME_VALUE
backupListingNBT.setString(BACKUP_LISTING_VERSION_TAG, BACKUP_LISTING_VERSION_VALUE);
NBTTagCompound paths = new NBTTagCompound(); //"dummy"
for (Map.Entry<Integer, Path> entry : backupListing.entrySet()) {
paths.setString(entry.getKey().toString(), entry.getValue().toString());
}
backupListingNBT.setTag(BACKUP_LISTING_PATHS_TAG, paths);
OutputStream out = null;
try {
CompressedStreamTools.write(backupListingNBT, fileToCreate.toFile());
} catch (IOException e) {
ErrorLog.defaultLog().severe("StoredBackups::saveBackupListing() failed to create backup save: %s", e.toString());
return false;
} finally {
if (out != null) {
try {
out.close();
} catch (Exception e) {
// ignore
}
}
}
} catch (Exception e) {
ErrorLog.defaultLog().severe("StoredBackups::saveBackupListing() failed to create backup save: %s", e.toString());
return false;
}
return true;
}
public HashMap<Integer, Path> getBackupListing() {
return backupListing;
}
/**
* Copies the source folder to a new backup folder and adds it to the list of StoredBackups
* @param folderToBeBackedUp the Minecraft Save folder to be backup up
* @param rootSavesFolder the root folder that Minecraft stores its saves into
* @param comment comment for the file
* @return the Path to the backup if backup was successfully created, null otherwise.
*/
public Path createBackupSave(Path folderToBeBackedUp, Path rootSavesFolder, String comment) {
boolean success;
Path newBackupName = getNextSaveFolder(rootSavesFolder, folderToBeBackedUp.getFileName().toString());
success = FolderBackup.createBackupSave(folderToBeBackedUp, newBackupName, comment);
if (success) {
addStoredBackup(newBackupName);
return newBackupName;
} else {
return null;
}
}
/**
* Looks through the StoredBackups and culls any which are surplus
* The deletion is performed to keep a series of backups with increasing spacing as they get older
* Up to six backups will be kept, the oldest will be at least 14 saves old (up to 21)
* @return the Path to the backup which was deleted; or null if none deleted
*/
public Path cullSurplus() {
// If the saves are numbered from 1, 2, 3, etc and savenumber is the number of the
// current save,
// then savenumber - deletionSchedule[savenumber%8] is to be deleted
// This sequence leads to a fairly evenly increasing gap between saves, up to 6 saves deep
// The oldest save will be between 13 - 20 ago; there are always at least the previous two saves
// In the diagram below, the leftmost column is the newest save. # = save, O = deletion
// 1: #
// 2: ##
// 3: ###
// 4: ##O#
// 5: ###.#
// 6: ##O#.#
// 7: ###.O.#
// 8: ##O#...#
// 9: ###.#...#
// 10: ##O#.#...#
// 11: ###.O.#...#
// 12: ##O#...#...#
// 13: ###.#...#...#
// 14: ##O#.#...#...#
// 15: ###.O.#...#...#
// 16: ##O#...#...#...#
// 17: ###.#...#...O...#
// 18: ##O#.#...#.......#
// 19: ###.O.#...#.......#
// 20: ##O#...#...#.......#
// 21: ###.#...#...#.......O
int[] deletionSchedule = {2, 12, 2, 4, 2, 20, 2, 4};
int maximumStoredBackupNumber = getMaximumStoredBackupNumber();
int backupNumToDelete = maximumStoredBackupNumber - deletionSchedule[maximumStoredBackupNumber%8];
Path backupToDelete = backupListing.get(backupNumToDelete);
if (backupToDelete == null) return null;
boolean success = FolderBackup.deleteBackupSave(backupToDelete);
if (!success) return null;
backupListing.remove(backupNumToDelete);
return backupToDelete;
}
/**
* provides a suitable folder name for the next save backup
* @param basePath and baseStem the base path and filename stem eg /saves and GreysWorld
* @return the folder name to be used for the next save backup, or null for failure
*/
public Path getNextSaveFolder(Path basePath, String baseStem) {
int nextBackupNumber = getNextBackupNumber();
char backupLetter = 'a';
boolean backupFolderExistsAlready;
Path folderToTry;
do {
folderToTry = basePath.resolve((baseStem + "-bk" + nextBackupNumber) + backupLetter);
backupFolderExistsAlready = Files.exists(folderToTry);
++backupLetter;
} while (backupFolderExistsAlready && backupLetter <= 'z');
if (backupFolderExistsAlready) {
ErrorLog.defaultLog().info("StoredBackups::getNextSaveFolder couldn't find a suitable name");
return null;
}
return folderToTry;
}
/**
* Adds the given path to the listing of stored backups
* @param newBackup
* @return true for success
*/
public boolean addStoredBackup(Path newBackup) {
backupListing.put(new Integer(getNextBackupNumber()), newBackup);
return true;
}
/**
* the number of the most recent backup (highest number)
* @return the number or Integer.MIN_VALUE if no backups stored
*/
private int getMaximumStoredBackupNumber()
{
int highestBackupNumber = Integer.MIN_VALUE;
for (Integer entry : backupListing.keySet()) {
highestBackupNumber = Math.max(highestBackupNumber, entry);
}
return highestBackupNumber;
}
private static final int STARTING_BACKUP_NUMBER = 1;
private int getNextBackupNumber()
{
if (backupListing.isEmpty()) return STARTING_BACKUP_NUMBER;
return getMaximumStoredBackupNumber() + 1;
}
private HashMap<Integer, Path> backupListing;
}