package speedytools.serverside.backup;
/*
* Contains sample code from Oracle:
*
* Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of Oracle nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import net.minecraft.nbt.CompressedStreamTools;
import net.minecraft.nbt.NBTTagCompound;
import speedytools.common.utilities.ErrorLog;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public class FolderBackup
{
/**
* Copies an entire save folder to a backup folder.
* destinationSaveFolder is created (it should not exist already)
* @param currentSaveFolder
* @param destinationSaveFolder
* @return true for success, false for failure
*/
static public boolean createBackupSave(Path currentSaveFolder, Path destinationSaveFolder, String comment)
{
try {
// if (!Files.exists(destinationSaveFolder)) {
// Files.createDirectory(destinationSaveFolder);
// }
TreeCopier tc = new TreeCopier(currentSaveFolder, destinationSaveFolder);
Files.walkFileTree(currentSaveFolder, tc);
NBTTagCompound backupFolderContentsListing = new NBTTagCompound();
backupFolderContentsListing.setString(COMMENT_TAG, comment);
backupFolderContentsListing.setTag(CONTENTS_TAG, tc.getAllFileInfo());
Path contentsFile = destinationSaveFolder.resolve(CONTENTS_FILENAME);
CompressedStreamTools.write(backupFolderContentsListing, contentsFile.toFile());
} catch (IOException e) {
ErrorLog.defaultLog().severe("FolderBackup::createBackupSave() failed to create backup save: %s", e);
return false;
}
return true;
}
/**
* Checks the specified backup folder to see if the size and timestamps of all the files still match
* the fileinfo file CONTENTS_FILENAME
* @param backupFolder
* @return true if all files match, false if any don't
*/
static public boolean isBackupSaveUnmodified(Path backupFolder)
{
NBTTagCompound nbt = readFileInfo(backupFolder);
if (nbt == null) return false;
try {
TreeCopier tc = new TreeCopier(backupFolder, null);
Files.walkFileTree(backupFolder, tc);
if (nbt.equals(tc.getAllFileInfo()) ) return true;
} catch (IOException e) {
ErrorLog.defaultLog().severe("FolderBackup::isBackupSaveUnmodified() failed to read: %s", e);
return false;
}
return false;
}
/**
* deletes the entire backup folder, checking each file against the list of expected files before deleting it
* @param backupSaveFolder
* @return true if there were no unexpected files and all the expected files were present and deleted
*/
static public boolean deleteBackupSave(Path backupSaveFolder)
{
try {
NBTTagCompound nbtFileInfo = readFileInfo(backupSaveFolder);
if (nbtFileInfo == null) return false;
if (!isBackupSaveUnmodified(backupSaveFolder)) return false;
TreeDeleter treeDeleter = new TreeDeleter(backupSaveFolder, nbtFileInfo);
Files.walkFileTree(backupSaveFolder, treeDeleter);
if (treeDeleter.lastWalkWasTerminated()) return false;
if (!treeDeleter.getAllFileInfo().hasNoTags()) return false;
} catch (IOException e) {
ErrorLog.defaultLog().severe("FolderBackup::deleteBackupSave() failed to delete backup save: %s", e);
return false;
}
return true;
}
/** read the file info listing from contents listing file in the specified folder
*
* @param backupFolder
* @return the NBT with the list of file information, or null if error
*/
static private NBTTagCompound readFileInfo(Path backupFolder)
{
Path contentsFile = backupFolder.resolve(CONTENTS_FILENAME);
if (!Files.isRegularFile(contentsFile) || !Files.isReadable(contentsFile)) return null;
NBTTagCompound nbt;
try {
nbt = CompressedStreamTools.read(contentsFile.toFile());
} catch (IOException ioe) {
ErrorLog.defaultLog().info("Failed to read contents file: " + contentsFile.toString());
return null;
}
if (!nbt.hasKey(CONTENTS_TAG)) { // !nbt.getName().equals(ROOT_TAG) || !nbt.hasKey(CONTENTS_TAG)) {
ErrorLog.defaultLog().info("tags missing from contents file: " + contentsFile.toString());
return null;
}
try {
nbt = nbt.getCompoundTag(CONTENTS_TAG);
} catch (Exception e) {
ErrorLog.defaultLog().info("invalid contents tag in contents file: " + contentsFile.toString());
return null;
}
return nbt;
}
/**
* Copy source file to destination location.
*/
static private void copyFile(Path source, Path target) throws IOException {
if (Files.notExists(target)) {
Files.copy(source, target);
}
}
private final static String ROOT_TAG = "BACKUP_FOLDER_CONTENTS";
private final static String COMMENT_TAG = "PATH";
private final static String CONTENTS_TAG = "LIST_OF_FILES";
private final static String CONTENTS_FILENAME = "fileinfo.dat";
static class TreeCopier implements FileVisitor<Path>
{
private final Path source;
private final Path destination; // destination folder, set to = source if copyFiles is false
private boolean copyFiles;
private boolean success;
public NBTTagCompound getAllFileInfo() {
return allFileInfo;
}
private NBTTagCompound allFileInfo;
/**
* create a FileVisitor to walk the folder tree, optionally copying each file & folder to a destination directory, and compiling a list
* of information about each file in the folder.
* @param i_source the source folder
* @param i_destination the destination folder, or if null - don't copy.
*/
TreeCopier(Path i_source, Path i_destination) {
this.source = i_source;
if (i_destination == null) {
copyFiles = false;
this.destination = this.source;
} else {
copyFiles = true;
this.destination = i_destination;
}
this.success = false;
allFileInfo = new NBTTagCompound(); //new NBTTagCompound(CONTENTS_TAG);
}
private final String PATH_TAG = "PATH";
private final String FILE_SIZE_TAG = "SIZE";
private final String FILE_CREATED_TAG = "CREATED";
private final String FILE_MODIFIED_TAG = "MODIFIED";
private NBTTagCompound createFileInfoEntry(Path path) throws IOException
{
BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);
// fileRecord.setString(PATH_TAG, path.toString());
NBTTagCompound nbt = new NBTTagCompound();
nbt.setLong(FILE_SIZE_TAG, attributes.size());
nbt.setLong(FILE_CREATED_TAG, attributes.lastModifiedTime().toMillis());
nbt.setLong(FILE_MODIFIED_TAG, attributes.lastModifiedTime().toMillis());
return nbt;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException
{
// before visiting entries in a directory we copy the directory
// (okay if directory already exists).
if (!copyFiles) return FileVisitResult.CONTINUE;
Path newdir = destination.resolve(source.relativize(dir));
// System.out.println("creating new directory: " + newdir.toString());
Files.copy(dir, newdir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
if (path.getFileName().equals(Paths.get(CONTENTS_FILENAME))) { // ignore the contents file
return FileVisitResult.CONTINUE;
}
if (copyFiles) {
copyFile(path, destination.resolve(source.relativize(path)));
}
// System.out.println("copying file " + path.toString() + " to " + destination.resolve(source.relativize(path)));
NBTTagCompound thisFileInfo = createFileInfoEntry(path);
allFileInfo.setTag(source.relativize(path).toString(), thisFileInfo); //.setCompoundTag(source.relativize(path).toString(), thisFileInfo);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException
{
throw exc;
}
}
static class TreeDeleter implements FileVisitor<Path>
{
/**
* create a FileVisitor to walk the folder tree, optionally copying each file & folder to a destination directory, and compiling a list
* of information about each file in the folder.
* @param expectedFileList = the Files that are expected to be in this folder
*/
TreeDeleter(Path i_backupFolderRoot, NBTTagCompound expectedFileList) {
allFileInfo = (NBTTagCompound)expectedFileList.copy();
backupFolderRoot = i_backupFolderRoot;
terminated = false;
}
public boolean lastWalkWasTerminated() {
return terminated;
}
public NBTTagCompound getAllFileInfo() {
return allFileInfo;
}
private final String FILE_SIZE_TAG = "SIZE";
private final String FILE_CREATED_TAG = "CREATED";
private final String FILE_MODIFIED_TAG = "MODIFIED";
private boolean isFileExpected(Path path, NBTTagCompound fileRecord) throws IOException
{
BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);
if ( fileRecord.getLong(FILE_SIZE_TAG) != attributes.size()
|| fileRecord.getLong(FILE_CREATED_TAG) != attributes.lastModifiedTime().toMillis()
|| fileRecord.getLong(FILE_MODIFIED_TAG) != attributes.lastModifiedTime().toMillis() ) {
return false;
} else {
return true;
}
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException
{
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
boolean okToDelete = false;
Path relativePath = backupFolderRoot.relativize(path);
if (path.getFileName().equals(Paths.get(CONTENTS_FILENAME))) { // delete the contents file
okToDelete = true;
} else {
okToDelete = isFileExpected(path, allFileInfo.getCompoundTag(relativePath.toString()));
}
if (!okToDelete) {
terminated = true;
return FileVisitResult.TERMINATE;
}
Files.delete(path);
allFileInfo.removeTag(relativePath.toString());
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException
{
throw exc;
}
private NBTTagCompound allFileInfo;
private Path backupFolderRoot;
private boolean terminated;
}
}