/* ********************************************************************** **
** Copyright notice **
** **
** (c) 2005-2009 RSSOwl Development Team **
** http://www.rssowl.org/ **
** **
** All rights reserved **
** **
** This program and the accompanying materials are made available under **
** the terms of the Eclipse Public License v1.0 which accompanies this **
** distribution, and is available at: **
** http://www.rssowl.org/legal/epl-v10.html **
** **
** A copy is found in the file epl-v10.html and important notices to the **
** license from the team is found in the textfile LICENSE.txt distributed **
** in this package. **
** **
** This copyright notice MUST APPEAR in all copies of the file! **
** **
** Contributors: **
** RSSOwl Development Team - initial API and implementation **
** **
** ********************************************************************** */
package org.rssowl.core.internal.persist.service;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.rssowl.core.internal.InternalOwl;
import org.rssowl.core.persist.service.PersistenceException;
import org.rssowl.core.util.LongOperationMonitor;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* The Backupservice is responsible for creating the RSSOwl Backups in a
* specific interval.
*/
public final class BackupService {
private static final int MIN_SIZE_FOR_PROGRESS = 1024 * 1024 * 100; //100 MB
interface BackupStrategy {
void backup(File originFile, File destinationFile, IProgressMonitor monitor);
}
interface BackupLayoutStrategy {
List<File> findBackupFiles();
/**
* Responsible for rotating back-up files to allow BackupService to save a
* new back-up.
* <p>
* Note that this method is only called if maxBackupsCount is higher than 1.
* If maxBackupsCount is equal to 1, the back-up files are simply deleted to
* create space for the new one.
* </p>
*
* @param backupFiles files to be rotated.
*/
void rotateBackups(List<File> backupFiles);
}
static class DefaultBackupLayoutStrategy implements BackupLayoutStrategy {
private final File fBackupFile;
DefaultBackupLayoutStrategy(File backupFile) {
fBackupFile = backupFile;
}
/*
* @see
* org.rssowl.core.internal.persist.service.BackupService.BackupLayoutStrategy
* #findBackupFiles()
*/
public List<File> findBackupFiles() {
int index = 0;
List<File> backupFiles = new ArrayList<File>(5);
File tempFile = fBackupFile;
while (tempFile.exists()) {
backupFiles.add(tempFile);
tempFile = new File(fBackupFile.getAbsolutePath() + "." + index++); //$NON-NLS-1$
}
return backupFiles;
}
/*
* @see
* org.rssowl.core.internal.persist.service.BackupService.BackupLayoutStrategy
* #rotateBackups(java.util.List)
*/
public void rotateBackups(List<File> backupFiles) {
int index;
while (backupFiles.size() > 0) {
index = backupFiles.size() - 1;
File fileToRename = backupFiles.remove(index);
/*
* index is correct here because filesToRename includes a back up file
* with no index as well as .0 index
*/
File newFile = new File(fBackupFile.getAbsolutePath() + "." + index); //$NON-NLS-1$
if (!fileToRename.renameTo(newFile)) {
throw new PersistenceException("Failed to rename file from " + fileToRename + " to " + newFile); //$NON-NLS-1$ //$NON-NLS-2$
}
}
}
}
private final File fFileToBackup;
private final String fBackupFileSuffix;
private final int fMaxBackupsCount;
private final File fBackupTimestampFile;
private final Long fBackupFrequency;
private BackupLayoutStrategy fLayoutStrategy;
private File fFileToBackupAlias;
private BackupStrategy fBackupStrategy;
public BackupService(File fileToBackup, String backupFileSuffix, int maxBackupsCount) {
this(fileToBackup, backupFileSuffix, maxBackupsCount, null, null);
}
public BackupService(File fileToBackup, String backupFileSuffix, int maxBackupsCount, File backupTimestampFile, Long backupFrequency) {
Assert.isNotNull(fileToBackup, "fileToBackup"); //$NON-NLS-1$
Assert.isLegal(fileToBackup.isFile(), "fileToBackup must be a file: " + fileToBackup.getAbsolutePath()); //$NON-NLS-1$
Assert.isLegal(backupFileSuffix != null && backupFileSuffix.length() > 0, "backupSuffix should contain a non-empty String"); //$NON-NLS-1$
Assert.isLegal(maxBackupsCount > 0, "filesKeptCount should be higher than 0"); //$NON-NLS-1$
if (backupFrequency != null)
Assert.isNotNull(backupTimestampFile, "backupTimestampFile should not be null if backupFrequency is not null"); //$NON-NLS-1$
fFileToBackup = fileToBackup;
fBackupFileSuffix = backupFileSuffix;
fMaxBackupsCount = maxBackupsCount;
fBackupTimestampFile = backupTimestampFile;
fBackupFrequency = backupFrequency;
fLayoutStrategy = new DefaultBackupLayoutStrategy(getBackupFile());
fBackupStrategy = new BackupStrategy() {
public void backup(File originFile, File destinationFile, IProgressMonitor monitor) {
/* Indicate that a long operation is starting if file is large */
if (originFile.length() > MIN_SIZE_FOR_PROGRESS && monitor instanceof LongOperationMonitor && !((LongOperationMonitor) monitor).isLongOperationRunning()) {
if (!InternalOwl.IS_ECLIPSE) //Avoid the Progress Dialog on Startup for Eclipse
((LongOperationMonitor) monitor).beginLongOperation(true);
int chunks = (int) (originFile.length() / DBHelper.BUFFER);
monitor.beginTask(Messages.DBManager_PROGRESS_WAIT, chunks);
monitor.subTask(Messages.DBManager_CREATING_DB_BACKUP);
}
/* Copy by IO */
DBHelper.copyFileIO(originFile, destinationFile, monitor);
if (monitor.isCanceled())
destinationFile.delete();
}
};
}
public void setBackupStrategy(BackupStrategy backupStrategy) {
fBackupStrategy = backupStrategy;
}
/**
* This overrides where the back-up is made from. It's useful if fileToBackup
* is being used, there's a copy available and the back-up file name should be
* relative to fileToBackup.
*
* @param alias
*/
public void setFileToBackupAlias(File alias) {
Assert.isLegal(alias.isFile(), "alias must be a file"); //$NON-NLS-1$
fFileToBackupAlias = alias;
}
public void setLayoutStrategy(BackupLayoutStrategy layoutStrategy) {
fLayoutStrategy = layoutStrategy;
}
public File getFileToBackup() {
return fFileToBackup;
}
/**
* Deletes old backups and rotates the existing ones as appropriate.
*/
private void prepareBackup() {
final File backupFile = getBackupFile();
List<File> backupFiles = fLayoutStrategy.findBackupFiles();
deleteOldBackups(backupFiles);
if (!backupFiles.isEmpty())
fLayoutStrategy.rotateBackups(backupFiles);
Assert.isLegal(!backupFile.exists(), "backupFile should have been rotated or deleted: " + backupFile); //$NON-NLS-1$
}
/**
* Backs up the file in any of the following: <li>force is {@code true}.</li>
* <li>backupTimestampFile is {@code null}.</li> <li>The time since the last
* backup is higher or equal than backupFrequency.</li>
*
* @param force if <code>true</code>, will not check for the last backup
* timestamp.
* @param monitor a {@link IProgressMonitor} to properly report progress.
* @return {@code true} if a backup took place.
* @throws PersistenceException if a problem occurs during back-up.
*/
public boolean backup(boolean force, IProgressMonitor monitor) throws PersistenceException {
if (!shouldBackup(force))
return false;
boolean backupSuccess = false;
try {
prepareBackup();
File sourceFile = fFileToBackup;
if (fFileToBackupAlias != null)
sourceFile = fFileToBackupAlias;
fBackupStrategy.backup(sourceFile, getBackupFile(), monitor);
backupSuccess = true;
} finally {
/*
* If an error occurs during backup we want to give the user a chance to
* restart without the backup failing over and over again, thereby we
* write the time of the backup attempt not only when the backup was
* successfull but also when the backup was scheduled on startup.
*/
if (backupSuccess || !force)
writeBackupTimestamp();
}
return true;
}
private boolean shouldBackup(boolean force) {
if (force)
return true;
if (!fBackupTimestampFile.exists()) {
writeBackupTimestamp();
return false;
}
try {
long lastBackupTimestamp = Long.parseLong(DBHelper.readFirstLineFromFile(fBackupTimestampFile));
long now = System.currentTimeMillis();
return (now - lastBackupTimestamp) >= fBackupFrequency.longValue();
} catch (NumberFormatException e) {
throw new PersistenceException(fBackupTimestampFile.getAbsolutePath() + " does not contain a number for the date as expected", e); //$NON-NLS-1$
}
}
private void writeBackupTimestamp() {
if (fBackupTimestampFile == null)
return;
if (!fBackupTimestampFile.exists()) {
try {
fBackupTimestampFile.createNewFile();
} catch (IOException e) {
throw new PersistenceException("Failed to create new file", e); //$NON-NLS-1$
}
}
DBHelper.writeToFile(fBackupTimestampFile, String.valueOf(System.currentTimeMillis()));
}
public File getBackupFile() {
String backupFilePath = fFileToBackup.getAbsolutePath() + fBackupFileSuffix;
File backupFile = new File(backupFilePath);
return backupFile;
}
public File getBackupFile(int index) {
List<File> backupFiles = fLayoutStrategy.findBackupFiles();
if (index >= backupFiles.size())
return null;
return backupFiles.get(index);
}
public File getTempBackupFile() {
return new File(getBackupFile().getAbsolutePath() + ".temp"); //$NON-NLS-1$
}
public File getWeeklyBackupFile() {
return new File(getBackupFile().getAbsolutePath() + ".weekly"); //$NON-NLS-1$
}
public File getCorruptedFile(Integer index) {
String fileName = getFileToBackup().getAbsolutePath() + ".corrupted"; //$NON-NLS-1$
if (index != null)
fileName += "." + index; //$NON-NLS-1$
return new File(fileName);
}
private void deleteOldBackups(List<File> backupFiles) {
/* We're creating a new back-up, so must leave one space available */
while (backupFiles.size() > (fMaxBackupsCount - 1)) {
File fileToDelete = backupFiles.remove(backupFiles.size() - 1);
if (!fileToDelete.delete()) {
throw new PersistenceException("Failed to delete file: " + fileToDelete); //$NON-NLS-1$
}
}
}
}