simpleserver.thread.AutoBackup.java Source code

Java tutorial

Introduction

Here is the source code for simpleserver.thread.AutoBackup.java

Source

/*
 * 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();
        }
    }
}