package chatty.util; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; /** * Can perform a backup (used on start) of certain files (settings). It just * copies them and gives them an appropriate name, rotating through different * numbers. * * <p> * The backup is only performed in certain intervals. Both the last number as * well as the time of the last backup are stored in a meta info file. If the * file can't be read or the format is invalid, then the backup might always * be performed, but it will also use the default number, so only one set of * backups should be overwritten with possibly invalid data (if the user starts * again and again and it performs a backup everytime).</p> * * @author tduva */ public class BackupManager { private static final Logger LOGGER = Logger.getLogger(BackupManager.class.getName()); private static final Charset CHARSET = Charset.forName("UTF-8"); /** * File where some meta information is stored. */ private static final String META_FILE = "backup_meta"; /** * Path used to resolve the files to backup, in case not a full path is * specified. */ private final Path defaultSourcePath; /** * Path to backup the files to. */ private final Path backupPath; /** * List of files to backup. */ private final List<Path> files; private long lastBackup; private int number; /** * Creates a new backup manager with the given {@code backupPath}, where it * saves the files to, and {@code defaultSourcePath}, which is used for * resolving the files to backup in case they are not a full path. * * @param backupPath The path to save the files to * @param defaultSourcePath The path to load the files from, if no full path * is specified for the files. */ public BackupManager(Path backupPath, Path defaultSourcePath) { this.backupPath = backupPath; this.defaultSourcePath = defaultSourcePath; this.files = new ArrayList<>(); } /** * Adds a file to backup. Can be either a full path or just a part, which * will be resolved with the {@code defaultSourcePath}. * * @param file The {@code Path} of the file */ public void addFile(Path file) { files.add(file); } /** * Adds a file to backup. Can be either a full path or just a part, which * will be resolved with the {@code defaultSourcePath}. * * @param fileName The name of the file or path as a {@code String} * @see addFile(Path) */ public void addFile(String fileName) { addFile(Paths.get(fileName)); } /** * Backups the previously added files if the {@code delay} has passed and * {@code count} is greater than 0, rotating the filenames between numbers 0 * and {@code count}-1. * * @param delay The minimum time in between backups, in seconds * @param count How many sets of backups to keep. Backups are numbered 0 to * n-1 based on this parameter */ public void performBackup(int delay, int count) { loadMetadata(); if (!checkBackupDelay(delay*1000) || count <= 0) { return; } try { Files.createDirectories(backupPath); } catch (IOException ex) { LOGGER.warning("Could not create backup dir: "+ex.getLocalizedMessage()); return; } number = (number + 1) % count; LOGGER.info("Performing backup ("+number+").."); for (Path sourceFile : files) { sourceFile = defaultSourcePath.resolve(sourceFile); if (!Files.isRegularFile(sourceFile)) { continue; } String targetFileName = "backup_"+number+"_"+sourceFile.getFileName().toString(); Path targetFile = backupPath.resolve(targetFileName); try { Files.copy(sourceFile, targetFile, REPLACE_EXISTING); } catch (IOException ex) { LOGGER.warning("Could not perform backup: "+ex); } } lastBackup = System.currentTimeMillis(); saveMetadata(); } /** * Checks if the given delay has passed since the last backup. Should be * checked only after loading the metadata. * * @param backupDelay The minimum delay in between backups, in milliseconds * @return {@code true} if the delay has passed, {@code false} otherwise */ private boolean checkBackupDelay(long backupDelay) { long ago = System.currentTimeMillis() - lastBackup; return ago > backupDelay; } /** * Loads some meta information from a file. * * <p> * Format (on one line): * <LastNumber> <LastBackup></p> */ private void loadMetadata() { Path f = backupPath.resolve(META_FILE); try (BufferedReader reader = Files.newBufferedReader(f, CHARSET)) { String line = reader.readLine(); String[] split = line.split(" "); number = Integer.parseInt(split[0]); lastBackup = Long.parseLong(split[1]); } catch (IOException ex) { LOGGER.warning("No backup meta file, using default. "+ex); } catch (NumberFormatException | ArrayIndexOutOfBoundsException ex) { LOGGER.warning("Backup meta file invalid format, using default. "+ex); number = 0; lastBackup = 0; } } /** * Saves the current meta information to a file. */ private void saveMetadata() { Path f = backupPath.resolve(META_FILE); try (BufferedWriter writer = Files.newBufferedWriter(f, CHARSET)) { writer.write(String.valueOf(number)+" "+String.valueOf(lastBackup)); } catch (IOException | NumberFormatException ex) { LOGGER.warning("Error writing backup meta file: "+ex); } } }