/*
blizzy's Backup - Easy to use personal file backup application
Copyright (C) 2011-2012 Maik Schreiber
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 3 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, see <http://www.gnu.org/licenses/>.
*/
package de.blizzy.backup.check;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.security.DigestOutputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.swt.widgets.Shell;
import org.jooq.Cursor;
import org.jooq.Record;
import org.jooq.impl.Factory;
import de.blizzy.backup.BackupPlugin;
import de.blizzy.backup.Compression;
import de.blizzy.backup.IStorageInterceptor;
import de.blizzy.backup.LengthOutputStream;
import de.blizzy.backup.Messages;
import de.blizzy.backup.StorageInterceptorDescriptor;
import de.blizzy.backup.Utils;
import de.blizzy.backup.database.Database;
import de.blizzy.backup.database.schema.Tables;
import de.blizzy.backup.settings.Settings;
public class CheckRun implements IRunnableWithProgress {
private static final class FileCheckResult {
static final FileCheckResult BROKEN = new FileCheckResult(false, null);
boolean ok;
String checksumSHA256;
FileCheckResult(boolean ok, String checksumSHA256) {
this.ok = ok;
this.checksumSHA256 = checksumSHA256;
}
}
private static final int SHA256_LENGTH = DigestUtils.sha256Hex(StringUtils.EMPTY).length();
private Settings settings;
private String outputFolder;
private Shell parentShell;
private Database database;
private boolean backupOk = true;
private List<IStorageInterceptor> storageInterceptors = new ArrayList<>();
public CheckRun(Settings settings, Shell parentShell) {
this.settings = settings;
this.parentShell = parentShell;
outputFolder = settings.getOutputFolder();
}
public void runCheck() {
boolean canceled = false;
boolean errors = false;
try {
ProgressMonitorDialog dlg = new ProgressMonitorDialog(parentShell);
dlg.run(true, true, this);
} catch (InvocationTargetException e) {
BackupPlugin.getDefault().logError("error while checking backup integrity", e.getCause()); //$NON-NLS-1$
errors = true;
} catch (RuntimeException e) {
BackupPlugin.getDefault().logError("error while checking backup integrity", e); //$NON-NLS-1$
errors = true;
} catch (InterruptedException e) {
canceled = true;
}
if (!canceled) {
if (errors) {
MessageDialog.openError(parentShell, Messages.Title_BackupIntegrityCheck, Messages.ErrorsWhileCheckingBackup);
} else {
if (backupOk) {
MessageDialog.openInformation(parentShell, Messages.Title_BackupIntegrityCheck, Messages.BackupIntegrityIntact);
} else {
MessageDialog.openError(parentShell, Messages.Title_BackupIntegrityCheck, Messages.BackupIntegrityNotIntact);
}
}
}
}
@Override
public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
database = new Database(settings, false);
final boolean[] ok = { true };
List<StorageInterceptorDescriptor> descs = BackupPlugin.getDefault().getStorageInterceptors();
for (final StorageInterceptorDescriptor desc : descs) {
final IStorageInterceptor interceptor = desc.getStorageInterceptor();
SafeRunner.run(new ISafeRunnable() {
@Override
public void run() {
IDialogSettings settings = Utils.getChildSection(
Utils.getSection("storageInterceptors"), desc.getId()); //$NON-NLS-1$
if (!interceptor.initialize(parentShell, settings)) {
ok[0] = false;
}
}
@Override
public void handleException(Throwable t) {
ok[0] = false;
interceptor.showErrorMessage(t, parentShell);
BackupPlugin.getDefault().logError(
"error while initializing storage interceptor '" + desc.getName() + "'", t); //$NON-NLS-1$ //$NON-NLS-2$
}
});
storageInterceptors.add(interceptor);
}
if (!ok[0]) {
monitor.done();
throw new InterruptedException();
}
try {
database.open(storageInterceptors);
database.initialize();
int numFiles = database.factory()
.select(Factory.count())
.from(Tables.FILES)
.fetchOne(Factory.count())
.intValue();
monitor.beginTask(Messages.Title_CheckBackupIntegrity, numFiles);
Cursor<Record> cursor = null;
try {
cursor = database.factory()
.select(Tables.FILES.ID, Tables.FILES.BACKUP_PATH, Tables.FILES.CHECKSUM, Tables.FILES.LENGTH, Tables.FILES.COMPRESSION)
.from(Tables.FILES)
.fetchLazy();
while (cursor.hasNext()) {
if (monitor.isCanceled()) {
throw new InterruptedException();
}
Record record = cursor.fetchOne();
String backupPath = record.getValue(Tables.FILES.BACKUP_PATH);
String checksum = record.getValue(Tables.FILES.CHECKSUM);
long length = record.getValue(Tables.FILES.LENGTH).longValue();
Compression compression = Compression.fromValue(record.getValue(Tables.FILES.COMPRESSION).intValue());
FileCheckResult checkResult = checkFile(backupPath, checksum, length, compression);
if (!checkResult.ok) {
backupOk = false;
break;
}
if (checksum.length() != SHA256_LENGTH) {
Integer id = record.getValue(Tables.FILES.ID);
database.factory()
.update(Tables.FILES)
.set(Tables.FILES.CHECKSUM, checkResult.checksumSHA256)
.where(Tables.FILES.ID.equal(id))
.execute();
}
monitor.worked(1);
}
} finally {
database.closeQuietly(cursor);
}
} catch (SQLException | IOException e) {
boolean handled = false;
for (IStorageInterceptor interceptor : storageInterceptors) {
if (interceptor.showErrorMessage(e, parentShell)) {
handled = true;
}
}
if (handled) {
throw new InterruptedException();
}
throw new InvocationTargetException(e);
} finally {
database.close();
for (final IStorageInterceptor interceptor : storageInterceptors) {
SafeRunner.run(new ISafeRunnable() {
@Override
public void run() {
interceptor.destroy();
}
@Override
public void handleException(Throwable t) {
BackupPlugin.getDefault().logError("error while destroying storage interceptor", t); //$NON-NLS-1$
}
});
}
System.gc();
monitor.done();
}
}
private FileCheckResult checkFile(String backupPath, String checksum,
long length, Compression compression) throws IOException {
File backupFile = Utils.toBackupFile(backupPath, outputFolder);
if (backupFile.isFile()) {
InputStream in = null;
OutputStream out = null;
try {
InputStream fileIn = new BufferedInputStream(new FileInputStream(backupFile));
InputStream interceptIn = fileIn;
for (IStorageInterceptor interceptor : storageInterceptors) {
interceptIn = interceptor.interceptInputStream(interceptIn, length);
}
InputStream compressIn = compression.getInputStream(interceptIn);
LengthOutputStream lengthOut = new LengthOutputStream(new NullOutputStream());
MessageDigest digest = MessageDigest.getInstance("SHA-256"); //$NON-NLS-1$
out = new DigestOutputStream(lengthOut, digest);
MessageDigest md5Digest = null;
if (checksum.length() != SHA256_LENGTH) {
md5Digest = MessageDigest.getInstance("MD5"); //$NON-NLS-1$
out = new DigestOutputStream(out, md5Digest);
}
in = compressIn;
IOUtils.copy(in, out);
out.flush();
String fileChecksum = Hex.encodeHexString(digest.digest());
String fileChecksumMD5 = (md5Digest != null) ? Hex.encodeHexString(md5Digest.digest()) : null;
long fileLength = lengthOut.getLength();
boolean ok = (fileLength == length) &&
checksum.equals((checksum.length() == SHA256_LENGTH) ? fileChecksum : fileChecksumMD5);
return new FileCheckResult(ok, fileChecksum);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
} finally {
IOUtils.closeQuietly(in);
IOUtils.closeQuietly(out);
}
}
return FileCheckResult.BROKEN;
}
}