Java tutorial
/* ================================================================== * FileSystemBackupService.java - Mar 27, 2013 11:38:08 AM * * Copyright 2007-2013 SolarNetwork.net Dev Team * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA * 02111-1307 USA * ================================================================== */ package net.solarnetwork.node.backup; import static net.solarnetwork.node.backup.BackupStatus.Configured; import static net.solarnetwork.node.backup.BackupStatus.Error; import static net.solarnetwork.node.backup.BackupStatus.RunningBackup; import static net.solarnetwork.node.backup.BackupStatus.Unconfigured; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.FilterOutputStream; import java.io.IOException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.Enumeration; import java.util.GregorianCalendar; import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; import net.solarnetwork.node.Constants; import net.solarnetwork.node.settings.SettingSpecifier; import net.solarnetwork.node.settings.SettingSpecifierProvider; import net.solarnetwork.node.settings.support.BasicSliderSettingSpecifier; import net.solarnetwork.node.settings.support.BasicTextFieldSettingSpecifier; import net.solarnetwork.node.settings.support.BasicTitleSettingSpecifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.MessageSource; import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.util.FileCopyUtils; /** * {@link BackupService} implementation that copies files to another location in * the file system. * * <p> * The configurable properties of this class are: * </p> * * <dl class="class-properties"> * <dt>backupDir</dt> * <dd>The directory to backup to.</dd> * * <dt>additionalBackupCount</dt> * <dd>The number of additional backups to maintain. If greater than zero, then * this service will maintain this many copies of past backups. * </dl> * * @author matt * @version 1.0 */ public class FileSystemBackupService implements BackupService, SettingSpecifierProvider { private static final String ARCHIVE_NAME_DATE_FORMAT = "yyyyMMdd'T'HHmmss"; /** The value returned by {@link #getKey()}. */ public static final String KEY = FileSystemBackupService.class.getName(); /** * A format for turning a {@link Backup#getKey()} value into a zip file * name. */ public static final String ARCHIVE_KEY_NAME_FORMAT = "node-backup-%s.zip"; private static final MessageSource MESSAGE_SOURCE = getMessageSourceInstance(); private static final String ARCHIVE_NAME_FORMAT = "node-backup-%1$tY%1$tm%1$tdT%1$tH%1$tM%1$tS.zip"; private static final Pattern ARCHIVE_NAME_PAT = Pattern.compile("node-backup-(\\d{8}T\\d{6})\\.zip"); private final Logger log = LoggerFactory.getLogger(getClass()); private File backupDir = defaultBackuprDir(); private int additionalBackupCount = 1; private BackupStatus status = Configured; private static File defaultBackuprDir() { String path = System.getProperty(Constants.SYSTEM_PROP_NODE_HOME, null); if (path == null) { path = System.getProperty("java.io.tmpdir"); } else { if (!path.endsWith("/")) { path += "/"; } path += "var/backups"; } return new File(path); } private static MessageSource getMessageSourceInstance() { ResourceBundleMessageSource source = new ResourceBundleMessageSource(); source.setBundleClassLoader(FileSystemBackupService.class.getClassLoader()); source.setBasename(FileSystemBackupService.class.getName()); return source; } @Override public String getSettingUID() { return getClass().getName(); } @Override public String getDisplayName() { return "File System Backup Service"; } @Override public MessageSource getMessageSource() { return MESSAGE_SOURCE; } @Override public List<SettingSpecifier> getSettingSpecifiers() { List<SettingSpecifier> results = new ArrayList<SettingSpecifier>(20); FileSystemBackupService defaults = new FileSystemBackupService(); results.add(new BasicTitleSettingSpecifier("status", getStatus().toString(), true)); results.add(new BasicTextFieldSettingSpecifier("backupDir", defaults.getBackupDir().getAbsolutePath())); results.add(new BasicSliderSettingSpecifier("additionalBackupCount", (double) defaults.getAdditionalBackupCount(), 0.0, 10.0, 1.0)); return results; } @Override public String getKey() { return KEY; } @Override public BackupServiceInfo getInfo() { return new SimpleBackupServiceInfo(null, getStatus()); } private String getArchiveKey(String archiveName) { Matcher m = ARCHIVE_NAME_PAT.matcher(archiveName); if (m.matches()) { return m.group(1); } return archiveName; } @Override public Backup backupForKey(String key) { final File archiveFile = new File(backupDir, String.format(ARCHIVE_KEY_NAME_FORMAT, key)); if (!archiveFile.canRead()) { return null; } return createBackupForFile(archiveFile, new SimpleDateFormat(ARCHIVE_NAME_DATE_FORMAT)); } @Override public Backup performBackup(final Iterable<BackupResource> resources) { if (resources == null) { return null; } final Iterator<BackupResource> itr = resources.iterator(); if (!itr.hasNext()) { log.debug("No resources provided, nothing to backup"); return null; } BackupStatus status = setStatusIf(RunningBackup, Configured); if (status != RunningBackup) { return null; } final Calendar now = new GregorianCalendar(); now.set(Calendar.MILLISECOND, 0); final String archiveName = String.format(ARCHIVE_NAME_FORMAT, now); final File archiveFile = new File(backupDir, archiveName); final String archiveKey = getArchiveKey(archiveName); log.info("Starting backup to archive {}", archiveName); log.trace("Backup archive: {}", archiveFile.getAbsolutePath()); Backup backup = null; ZipOutputStream zos = null; try { zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(archiveFile))); while (itr.hasNext()) { BackupResource r = itr.next(); log.debug("Backup up resource {} to archive {}", r.getBackupPath(), archiveName); zos.putNextEntry(new ZipEntry(r.getBackupPath())); FileCopyUtils.copy(r.getInputStream(), new FilterOutputStream(zos) { @Override public void close() throws IOException { // FileCopyUtils closes the stream, which we don't want } }); } zos.flush(); zos.finish(); log.info("Backup complete to archive {}", archiveName); backup = new SimpleBackup(now.getTime(), archiveKey, archiveFile.length(), true); // clean out older backups File[] backupFiles = getAvailableBackupFiles(); if (backupFiles != null && backupFiles.length > additionalBackupCount + 1) { // delete older files for (int i = additionalBackupCount + 1; i < backupFiles.length; i++) { log.info("Deleting old backup archive {}", backupFiles[i].getName()); if (!backupFiles[i].delete()) { log.warn("Unable to delete backup archive {}", backupFiles[i].getAbsolutePath()); } } } } catch (IOException e) { log.error("IO error creating backup: {}", e.getMessage()); setStatus(Error); } catch (RuntimeException e) { log.error("Error creating backup: {}", e.getMessage()); setStatus(Error); } finally { if (zos != null) { try { zos.close(); } catch (IOException e) { // ignore this } } status = setStatusIf(Configured, RunningBackup); if (status != Configured) { // clean up if we encountered an error if (archiveFile.exists()) { archiveFile.delete(); } } } return backup; } @Override public BackupResourceIterable getBackupResources(Backup backup) { final File archiveFile = new File(backupDir, String.format(ARCHIVE_KEY_NAME_FORMAT, backup.getKey())); if (!(archiveFile.isFile() && archiveFile.canRead())) { log.warn("No backup archive exists for key [{}]", backup.getKey()); Collection<BackupResource> col = Collections.emptyList(); return new CollectionBackupResourceIterable(col); } try { final ZipFile zf = new ZipFile(archiveFile); Enumeration<? extends ZipEntry> entries = zf.entries(); List<BackupResource> result = new ArrayList<BackupResource>(20); while (entries.hasMoreElements()) { result.add(new ZipEntryBackupResource(zf, entries.nextElement())); } return new CollectionBackupResourceIterable(result) { @Override public void close() throws IOException { zf.close(); } }; } catch (IOException e) { log.error("Error extracting backup archive entries: {}", e.getMessage()); } Collection<BackupResource> col = Collections.emptyList(); return new CollectionBackupResourceIterable(col); } /** * Delete any existing backups. */ public void removeAllBackups() { File[] archives = backupDir.listFiles(new ArchiveFilter()); if (archives == null) { return; } for (File archive : archives) { log.debug("Deleting backup archive {}", archive.getName()); if (!archive.delete()) { log.warn("Unable to delete archive file {}", archive.getAbsolutePath()); } } } /** * Get all available backup files, ordered in desending backup order (newest * to oldest). * * @return ordered array of backup files, or <em>null</em> if directory does * not exist */ private File[] getAvailableBackupFiles() { File[] archives = backupDir.listFiles(new ArchiveFilter()); if (archives != null) { Arrays.sort(archives, new Comparator<File>() { @Override public int compare(File o1, File o2) { // sort in reverse order, so most recent backup first return o2.getName().compareTo(o1.getName()); } }); } return archives; } private SimpleBackup createBackupForFile(File f, SimpleDateFormat sdf) { Matcher m = ARCHIVE_NAME_PAT.matcher(f.getName()); if (m.matches()) { try { Date d = sdf.parse(m.group(1)); return new SimpleBackup(d, m.group(1), f.length(), true); } catch (ParseException e) { log.error("Error parsing date from archive " + f.getName() + ": " + e.getMessage()); } } return null; } @Override public Collection<Backup> getAvailableBackups() { File[] archives = getAvailableBackupFiles(); if (archives == null) { return Collections.emptyList(); } List<Backup> result = new ArrayList<Backup>(archives.length); SimpleDateFormat sdf = new SimpleDateFormat(ARCHIVE_NAME_DATE_FORMAT); for (File f : archives) { SimpleBackup b = createBackupForFile(f, sdf); if (b != null) { result.add(b); } } return result; } @Override public SettingSpecifierProvider getSettingSpecifierProvider() { return this; } private static class ArchiveFilter implements FilenameFilter { @Override public boolean accept(File dir, String name) { return ARCHIVE_NAME_PAT.matcher(name).matches(); } } private BackupStatus getStatus() { synchronized (status) { if (backupDir == null) { return Unconfigured; } if (!backupDir.exists()) { if (!backupDir.mkdirs()) { log.warn("Could not create backup dir {}", backupDir.getAbsolutePath()); return Unconfigured; } } if (!backupDir.isDirectory()) { log.error("Configured backup location is not a directory: {}", backupDir.getAbsolutePath()); return Unconfigured; } return status; } } private void setStatus(BackupStatus newStatus) { synchronized (status) { status = newStatus; } } private BackupStatus setStatusIf(BackupStatus newStatus, BackupStatus ifStatus) { synchronized (status) { if (status == ifStatus) { status = newStatus; } return status; } } public File getBackupDir() { return backupDir; } public void setBackupDir(File backupDir) { this.backupDir = backupDir; } public int getAdditionalBackupCount() { return additionalBackupCount; } public void setAdditionalBackupCount(int additionalBackupCount) { this.additionalBackupCount = additionalBackupCount; } }