Java tutorial
/* * Copyright (c) 2010 SimpleServer authors (see CONTRIBUTORS) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package simpleserver.thread; import static simpleserver.lang.Translations.t; import static simpleserver.util.Util.println; import java.io.BufferedReader; import java.io.File; import java.io.FileFilter; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import org.apache.commons.io.FileUtils; import simpleserver.Server; import simpleserver.command.RollbackCommand; import simpleserver.util.IO; public class AutoBackup { private static final String VERSION = "1"; // version of the backup system for // compatibility private static final long MILLISECONDS_PER_MINUTE = 1000 * 60; private static final long MILLISECONDS_PER_HOUR = MILLISECONDS_PER_MINUTE * 60; private static final String NAME_FORMAT = "%tF-%1$tH-%1$tM"; private static final File BACKUP_BASE_DIRECTORY = new File("backups"); private static final File BACKUP_AUTO_DIRECTORY = new File(BACKUP_BASE_DIRECTORY, "auto"); private static final File BACKUP_TAGGED_DIRECTORY = new File(BACKUP_BASE_DIRECTORY, "tagged"); private static final File TEMP_DIRECTORY = new File("tmp"); private static final String BACKUP_CONFIG_FOLDER = "config"; private static final String BACKUP_MAP_FOLDER = "map"; // bukkit depending files and directories private static final List<File> RESOURCE_DIRS_CONFIG_BUKKIT = new ArrayList<File>( Arrays.asList(new File("bukkit.yml"))); /* * Directories and files to backup and restore with the current configuration (bukkit yes/no) * Added here are the settings-independant resources * Resources are devided into CONFIG and MAP */ private static final List<File> RESOURCES_CONFIG = new ArrayList<File>( Arrays.asList(new File("simpleserver"), new File("simpleserver.properties"))); private static final List<File> RESOURCES_MAP = new ArrayList<File>(); // Filter to exclude unimportant files private static final FileFilter filter = new FileFilter() { public boolean accept(File pathname) { return (!pathname.getName().equals("level.dat_old")); } }; private final Server server; private final Archiver archiver; private volatile boolean run = true; private volatile boolean forceBackup = false; private volatile boolean pauseBackup = false; // private volatile boolean rollback = false; private volatile String tag = null; // tag for next backup ('null' means // date/no tagged backup) private volatile File rollback = null; // backup to roll back to (initiate // rollback by setting != null) private volatile RollbackCommand.ExecCom com = null; // communication // interface for feedback // messages public AutoBackup(Server server) { this.server = server; // Create backup directories if not present BACKUP_AUTO_DIRECTORY.mkdirs(); BACKUP_TAGGED_DIRECTORY.mkdirs(); // initialize resource directories RESOURCES_MAP.add(server.getMapDirectory()); if (server.isBukkitServer()) { RESOURCES_CONFIG.addAll(RESOURCE_DIRS_CONFIG_BUKKIT); } purgeOldBackups(); archiver = new Archiver(); archiver.start(); archiver.setName("AutoBackup"); } /** * Stop the system / thread. Note that it does not stop immediately if a * rollback is being peformed. */ public void stop() { run = false; if (rollback == null) { archiver.interrupt(); } } public void forceBackup() { forceBackup(null); } public void forceBackup(String tag) { this.tag = tag; forceBackup = true; archiver.interrupt(); } // TODO do not overwrite backups! private void backup() throws IOException { if (server.config.properties.getBoolean("announceBackup")) { println("Backing up server..."); } announce(t("Backing up...")); File copy; try { copy = makeTemporaryCopy(); server.runCommand("save-on", null); zipBackup(copy); // create actual backup file tag = null; // reset tag switch } finally { deleteRecursively(TEMP_DIRECTORY); } purgeOldBackups(); announce(t("Backup Complete!")); } public void announce(String message) { if (server.config.properties.getBoolean("announceBackup")) { server.runCommand("say", message); } } private boolean needsBackup() { long backupPeriod = MILLISECONDS_PER_MINUTE * server.config.properties.getInt("autoBackupMins"); return server.config.properties.getBoolean("autoBackup") && backupPeriod < lastBackupAge() && !pauseBackup || forceBackup; } private long lastBackupAge() { long age = age(newestBackup()); return (age >= 0) ? age : Long.MAX_VALUE; } private void purgeOldBackups() { long maxAge = MILLISECONDS_PER_HOUR * server.config.properties.getInt("keepBackupHours"); File file; while (age(file = oldestBackup()) > maxAge) { deleteRecursively(file); } } /** * Prepare the backup archive. * * @return * @throws IOException */ private File makeTemporaryCopy() throws IOException { Date date = new Date(); File backup = new File(TEMP_DIRECTORY, String.format(NAME_FORMAT, date)); File backupConfig = new File(backup, BACKUP_CONFIG_FOLDER); File backupMap = new File(backup, BACKUP_MAP_FOLDER); for (File file : RESOURCES_CONFIG) { copy(file, new File(backupConfig, file.getName())); } for (File file : RESOURCES_MAP) { copy(file, new File(backupMap, file.getName())); } // Create backup info file PrintWriter out = new PrintWriter(new File(backup, "backup.info")); out.println("Backup system version: " + VERSION); out.print("Bukkit used: "); if (server.isBukkitServer()) { out.println("yes"); } else { out.println("no"); } out.print("Backup date: " + new SimpleDateFormat("dd/MM/yyyy HH:mm").format(date)); out.close(); return backup; } /** * Zip and name the temporary created backup. * * @param source Directory to zip * @throws IOException */ private void zipBackup(File source) throws IOException { File dir = BACKUP_TAGGED_DIRECTORY; if (tag == null) { dir = BACKUP_AUTO_DIRECTORY; tag = String.format(NAME_FORMAT, new Date()); } File backup = new File(dir, tag + ".zip"); IO.zip(source, backup); println("Backup saved: " + backup.getPath()); } /** * Copy file or directory 'source' to 'target'-directoy, which is created if * non-existent. Files not accepted by 'filter' are ignored. * * @param source * @param target * @throws IOException */ private void copy(File source, File target) throws IOException { if (source.isDirectory()) { FileUtils.copyDirectory(source, target, filter); } else { if (filter.accept(source)) { FileUtils.copyFile(source, target); } } } private void deleteRecursively(File path) { if (path.exists() && path.isDirectory()) { for (File file : path.listFiles()) { if (file.isDirectory()) { deleteRecursively(file); } else { file.delete(); } } } path.delete(); } public static File newestBackup() { return getBackup(false); } public static File oldestBackup() { return getBackup(true); } /** * Get newest / oldest backup (auto backup). * * @param old * @return */ private static File getBackup(boolean old) { // Search for backups in BACKUP_AUTO_DIRECTORY File[] files = getAutoBackups(); long firstCreatedTime = old ? Long.MAX_VALUE : 0; File firstCreatedFile = null; for (File file : files) { long date; try { date = dateMillis(file); } catch (ParseException e) { continue; } if ((old && date < firstCreatedTime) || (!old && date > firstCreatedTime)) { firstCreatedFile = file; firstCreatedTime = date; } } return firstCreatedFile; } private static File[] getAutoBackups() { return BACKUP_AUTO_DIRECTORY.listFiles(new FileFilter() { public boolean accept(File file) { return file.isFile() && file.getPath().contains(".zip"); } }); } /** * Like 'getAutoBackups()', but sorted from newest to oldest. * * @return */ private static File[] getSortedAutoBackups() { // sort files by date (newest to oldest) File[] files = getAutoBackups(); java.util.Arrays.sort(files, new Comparator<File>() { public int compare(File o1, File o2) { try { return date(o2).compareTo(date(o1)); } catch (ParseException ex) { // should not be thrown return 0; } } }); return files; } public static String listLastAutoBackups(int n) { StringBuilder sb = new StringBuilder(); sb.append("Last ").append(n).append(" auto backups:"); File[] files = getSortedAutoBackups(); for (int i = 1; i <= n && i <= files.length; i++) { try { sb.append("\n").append("@").append(i).append(" ").append(dateFormatted(files[i - 1])); } catch (ParseException ex) { continue; } } return sb.toString(); } private static Date date(File file) throws ParseException { return new SimpleDateFormat("yyyy-MM-dd-HH-mm").parse(file.getName().split(".zip")[0]); } private static String dateFormatted(File file) throws ParseException { return new SimpleDateFormat("dd/MM/yyyy HH:mm").format(date(file)); } private static long dateMillis(File file) throws ParseException { GregorianCalendar cal = new GregorianCalendar(); cal.setTime(date(file)); return cal.getTimeInMillis(); } private static long age(File file) { try { if (file == null) { return -1; } else { return System.currentTimeMillis() - dateMillis(file); } } catch (ParseException e) { return System.currentTimeMillis() - file.lastModified(); } } public void setCom(RollbackCommand.ExecCom com) { this.com = com; } /** * Rollback to n-th last auto backup. * * @param n * @return */ public void rollback(RollbackCommand.ExecCom com, int n) throws Exception { File[] backups = getSortedAutoBackups(); try { rollback = backups[n - 1]; this.com = com; archiver.interrupt(); } catch (ArrayIndexOutOfBoundsException ex) { throw new Exception("Wrong backup number!"); } } /** * Rollback to backup with tag 'tag'. * * @param tag * @return */ public void rollback(RollbackCommand.ExecCom com, String tag) throws Exception { rollback = new File(BACKUP_TAGGED_DIRECTORY, tag + ".zip"); if (!rollback.isFile()) { rollback = null; throw new Exception("Backup does not exist!"); } this.com = com; archiver.interrupt(); } private boolean prepareRollback() { try { IO.unzip(rollback, TEMP_DIRECTORY); } catch (IOException ex) { com.sendWarningRollbackAborted("Error while unzipping backup: " + ex.getMessage()); return false; } return true; } /** * Check to be performed just before calling 'rollback()'. Makes a * compatibility check. * * @return true - ok false do not rollback */ private boolean canRollback() { // check for compatibility: read backup.info File info = new File(TEMP_DIRECTORY, "backup.info"); BufferedReader in = null; try { in = new BufferedReader(new FileReader(info)); String[] lines = new String[3]; for (int i = 0; i < 3; i++) { lines[i] = in.readLine(); lines[i] = lines[i].substring(lines[i].indexOf(':') + 2); } if (!lines[0].equals(VERSION)) { throw new Exception("Backup was made with a different backup system: backup=" + lines[0] + " current=" + VERSION); } if (lines[1].equals("yes") && !server.isBukkitServer()) { throw new Exception("The backup was made with a bukkit server, but now bukkit isn't used!"); } else if (lines[1].equals("no") && server.isBukkitServer()) { throw new Exception("The backup was made without a bukkit server, but now bukkit is used!"); } } catch (FileNotFoundException ex) { com.sendWarningRollbackAborted("Warning: file \"backup.info\" was not found in the backup archive."); return false; } catch (Exception ex) { com.sendWarningRollbackAborted( ex.getMessage() + " The SimpleServer backup system works differently with/without bukkit."); return false; } finally { try { in.close(); } catch (Exception e) { } } return true; } /** * Rollback to server status at backup 'rollback'. */ private void rollback() { com.sendMsg("Rolling back..."); // collect files to restore, delete present files List<File> backup = new ArrayList<File>(); File backupConfig = new File(TEMP_DIRECTORY, BACKUP_CONFIG_FOLDER); for (File file : RESOURCES_CONFIG) { deleteRecursively(file); backup.add(new File(backupConfig, file.getName())); } File backupMap = new File(TEMP_DIRECTORY, BACKUP_MAP_FOLDER); for (File file : RESOURCES_MAP) { deleteRecursively(file); backup.add(new File(backupMap, file.getName())); } File dest = new File("."); try { for (File file : backup) { if (file.isDirectory()) { FileUtils.copyDirectoryToDirectory(file, dest); } else { FileUtils.copyFileToDirectory(file, dest); } } } catch (IOException ex) { // critical: rollback could not be completed com.sendErrorRollbackFail(ex.getMessage()); } } private final class Archiver extends Thread { @Override public void run() { while (run) { if (needsBackup()) { doBackup(); } else if (rollback != null) { tag = null; doBackup(); doRollback(); rollback = null; } if (pauseBackup && server.numPlayers() > 0) { pauseBackup = false; } try { Thread.sleep(60000); } catch (InterruptedException e) { } } } private void doBackup() { try { server.saveLock.acquire(); } catch (InterruptedException e) { return; } forceBackup = false; if (server.config.properties.getBoolean("announceSave")) { server.runCommand("say", t("Saving Map...")); } server.setSaving(true); server.runCommand("save-all", null); while (server.isSaving()) { try { Thread.sleep(100); } catch (InterruptedException e) { } } server.runCommand("save-off", null); server.autoSpaceCheck.check(true); try { backup(); // does enable saving } catch (IOException e) { server.errorLog(e, "Server Backup Failure"); println(e); println("Automated Server Backup Failure!"); } server.saveLock.release(); if (server.numPlayers() == 0) { pauseBackup = true; } } private void doRollback() { prepareRollback(); if (!com.isForce() && !canRollback()) { deleteRecursively(TEMP_DIRECTORY); return; } server.manualRestart(); rollback(); deleteRecursively(TEMP_DIRECTORY); server.continueRestart(); } } }