/* * Copyright 2011 Tyler Blair. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are * permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of * conditions and the following disclaimer. * * 2. 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. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''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 AUTHOR 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. * * The views and conclusions contained in the software and documentation are those of the * authors and contributors and should not be interpreted as representing official policies, * either expressed or implied, of anybody else. */ package com.griefcraft.io; import com.griefcraft.lwc.LWC; import com.griefcraft.model.Protection; import com.griefcraft.sql.Database; import com.griefcraft.sql.PhysDB; import org.bukkit.Bukkit; import org.bukkit.Server; import org.bukkit.block.Block; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitScheduler; import java.io.File; import java.io.IOException; import java.sql.ResultSet; import java.sql.Statement; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.EnumSet; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.Future; public class BackupManager { /** * The result for a backup operationMode */ public enum Result { OK, FAILURE } /** * The folder where backups are stored at */ public static String BACKUP_FOLDER = "plugins/LWC/backups/"; /** * The date format to name backup files with by default */ private static String DATE_FORMAT = "MM-dd-yyyy-HHmm"; /** * The file extension for compressed backups */ private static String FILE_EXTENSION_COMPRESSED = ".lwc.gz"; /** * The file extension of uncompressed backups */ private static String FILE_EXTENSION_UNCOMPRESSED = ".lwc"; /** * The amount of protection block gets to batch at once */ private static int BATCH_SIZE = 250; /** * The folder backups are stored in */ private final File backupFolder = new File(BACKUP_FOLDER); /** * Backup creation flags */ public enum Flag { /** * Backup protection objects */ BACKUP_PROTECTIONS, /** * Backup blocks along with their inventory contents (if applicable) also */ BACKUP_BLOCKS, /** * Compress the backup using GZip */ COMPRESSION } public BackupManager() { if (!backupFolder.exists()) { backupFolder.mkdir(); } } /** * Begin restoring a backup. This should be ran in a separate thread. * Any world calls are offloaded to the world thread using the scheduler. No world reads are done, only writes. * * @param name * @return OK if successful, otherwise FAILURE */ public Result restoreBackup(String name) { try { Backup backup = loadBackup(name); if (backup == null) { return Result.FAILURE; } return restoreBackup(backup); } catch (IOException e) { System.out.println("[BackupManager] Caught: " + e.getMessage()); return Result.FAILURE; } } /** * Begin restoring a backup. This should be ran in a separate thread. * Any world calls are offloaded to the world thread using the scheduler. No world reads are done, only writes. * * @param backup * @return OK if successful, otherwise FAILURE */ public Result restoreBackup(Backup backup) { try { // Read in the backup's header backup.readHeader(); // begin restoring :) Restorable restorable; int count = 0; int protectionCount = 0; int blockCount = 0; while ((restorable = backup.readRestorable()) != null) { restorable.restore(); if (count % 2000 == 0) { System.out.println("[Backup] Restored restorables: " + count); } count ++; // TODO THIS IS HACKS :-( ALSO ENUM ENUM if (restorable.getType() == 0) { protectionCount ++; } else if (restorable.getType() == 1) { blockCount ++; } } System.out.println(String.format("[BackupManager] Restored %d restorables. %d were protections, %d blocks.", count, protectionCount, blockCount)); return Result.OK; } catch (IOException e) { e.printStackTrace(); return Result.FAILURE; } } /** * Load a backup * * @param name * @return */ public Backup loadBackup(String name) throws IOException { File file; // Try to load the compressed version file = new File(BACKUP_FOLDER, name + FILE_EXTENSION_COMPRESSED); if (file.exists()) { // Bingo return new Backup(file, Backup.OperationMode.READ, EnumSet.of(Flag.COMPRESSION)); } // Try uncompressed file = new File(BACKUP_FOLDER, name + FILE_EXTENSION_UNCOMPRESSED); if (file.exists()) { return new Backup(file, Backup.OperationMode.READ, EnumSet.noneOf(Flag.class)); } // Nothing :-( return null; } /** * Create a backup of the given objects. * When this returns, it is not guaranteed that the backup is fully written to the disk. * * @param name * @param flags * @return */ public Backup createBackup(String name, final EnumSet<Flag> flags) { final LWC lwc = LWC.getInstance(); final Plugin plugin = lwc.getPlugin(); Server server = Bukkit.getServer(); final BukkitScheduler scheduler = server.getScheduler(); String extension = flags.contains(Flag.COMPRESSION) ? FILE_EXTENSION_COMPRESSED : FILE_EXTENSION_UNCOMPRESSED; File backupFile = new File(backupFolder, name + extension); // Our backup file try { final Backup backup = new Backup(backupFile, Backup.OperationMode.WRITE, flags); scheduler.scheduleAsyncDelayedTask(plugin, new Runnable() { public void run() { try { System.out.println("Processing backup request now in a separate thread"); // the list of protections work off of. We batch updates to the world // so we can more than 20 results/second. final List<Protection> protections = new ArrayList<Protection>(BATCH_SIZE); // amount of protections int totalProtections = lwc.getPhysicalDatabase().getProtectionCount(); // Write the header backup.writeHeader(); // TODO separate stream logic to somewhere else :) // Create a new database connection, we are just reading PhysDB database = new PhysDB(); database.connect(); database.load(); // TODO separate stream logic to somewhere else :) Statement resultStatement = database.getConnection().createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); if (lwc.getPhysicalDatabase().getType() == Database.Type.MySQL) { resultStatement.setFetchSize(Integer.MIN_VALUE); } String prefix = lwc.getPhysicalDatabase().getPrefix(); ResultSet result = resultStatement.executeQuery("SELECT id, owner, type, x, y, z, data, blockId, world, password, date, last_accessed FROM " + prefix + "protections"); int count = 0; while (result.next()) { final Protection tprotection = database.resolveProtection(result); if (count % 2000 == 0) { System.out.println("[Backup] Parsed protections: " + count + "/" + totalProtections); } count ++; if (protections.size() != BATCH_SIZE) { // Wait until we have BATCH_SIZE protections protections.add(tprotection); if (protections.size() != totalProtections) { continue; } } // Get all of the blocks in the world Future<Void> getBlocks = scheduler.callSyncMethod(plugin, new Callable<Void>() { public Void call() throws Exception { for (Protection protection : protections) { protection.getBlock(); // this will cache it also :D } return null; } }); // Get all of the blocks getBlocks.get(); for (Protection protection : protections) { try { // if we are writing the block to the backup, do that before we write the protection if (flags.contains(Flag.BACKUP_BLOCKS)) { // now we can get the block from the world Block block = protection.getBlock(); // Wrap the block object in a RestorableBlock object RestorableBlock rblock = RestorableBlock.wrapBlock(block); // Write it backup.writeRestorable(rblock); } // Now write the protection after the block if we are writing protections if (flags.contains(Flag.BACKUP_PROTECTIONS)) { RestorableProtection rprotection = RestorableProtection.wrapProtection(protection); // Write it backup.writeRestorable(rprotection); } } catch (Exception e) { System.out.println("Caught: " + e.getMessage() + ". Carrying on..."); } } // Clear the protection set, we are done with them protections.clear(); } // close the sql statements result.close(); resultStatement.close(); // close the backup file backup.close(); System.out.println("Backup completed!"); } catch (Exception e) { // database.connect() throws Exception System.out.println("Backup exception caught: " + e.getMessage()); } } }); return backup; } catch (IOException e) { e.printStackTrace(); } return null; } /** * Create a backup of all protections, blocks, and their contents * When this returns, it is not guaranteed that the backup is fully written to the disk. * * @param flags * @return */ public Backup createBackup(EnumSet<Flag> flags) { return createBackup(new SimpleDateFormat(DATE_FORMAT).format(new Date()), flags); } /** * Create a backup of all protections, blocks, and their contents * When this returns, it is not guaranteed that the backup is fully written to the disk. * * @return */ public Backup createBackup() { return createBackup(EnumSet.of(Flag.COMPRESSION, Flag.BACKUP_BLOCKS, Flag.BACKUP_PROTECTIONS)); } }