Files
duplicati/Duplicati/Library/Main/Operation/BackupHandler.cs
T

476 lines
23 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Duplicati.Library.Main.Database;
using Duplicati.Library.Main.Volumes;
using Duplicati.Library.Interface;
using System.Threading.Tasks;
using CoCoL;
using System.Threading;
using Duplicati.Library.Utility;
namespace Duplicati.Library.Main.Operation
{
internal class BackupHandler : IDisposable
{
private readonly Options m_options;
private string m_backendurl;
private byte[] m_blockbuffer;
private byte[] m_blocklistbuffer;
private System.Security.Cryptography.HashAlgorithm m_blockhasher;
private System.Security.Cryptography.HashAlgorithm m_filehasher;
private LocalBackupDatabase m_database;
private System.Data.IDbTransaction m_transaction;
private BlockVolumeWriter m_blockvolume;
private IndexVolumeWriter m_indexvolume;
private readonly IMetahash EMPTY_METADATA;
private Library.Utility.IFilter m_filter;
private Library.Utility.IFilter m_sourceFilter;
private BackupResults m_result;
// Speed up things by caching this
private int m_blocksize;
public BackupHandler(string backendurl, Options options, BackupResults results)
{
EMPTY_METADATA = Utility.WrapMetadata(new Dictionary<string, string>(), options);
m_options = options;
m_result = results;
m_backendurl = backendurl;
if (options.AllowPassphraseChange)
throw new Exception(Strings.Foresthash.PassphraseChangeUnsupported);
}
public static Snapshots.ISnapshotService GetSnapshot(string[] sources, Options options, ILogWriter log)
{
try
{
if (options.SnapShotStrategy != Options.OptimizationStrategy.Off)
return Duplicati.Library.Snapshots.SnapshotUtility.CreateSnapshot(sources, options.RawOptions);
}
catch (Exception ex)
{
if (options.SnapShotStrategy == Options.OptimizationStrategy.Required)
throw;
else if (options.SnapShotStrategy == Options.OptimizationStrategy.On)
log.AddWarning(Strings.RSyncDir.SnapshotFailedError(ex.ToString()), ex);
}
return Library.Utility.Utility.IsClientLinux ?
(Library.Snapshots.ISnapshotService)new Duplicati.Library.Snapshots.NoSnapshotLinux(sources, options.RawOptions)
:
(Library.Snapshots.ISnapshotService)new Duplicati.Library.Snapshots.NoSnapshotWindows(sources, options.RawOptions);
}
private static Task CountFilesHandler(Snapshots.ISnapshotService snapshot, BackupResults result, Options options, IFilter sourcefilter, IFilter filter, CancellationToken token = default(CancellationToken))
{
using(new ChannelScope())
{
var enumeratorTask = new Backup.FileEnumerationProcess(snapshot, options.FileAttributeFilter, sourcefilter, filter, options.SymlinkPolicy, options.HardlinkPolicy, options.ChangedFilelist).RunAsync();
var counterTask = AutomationExtensions.RunTask(new
{
Input = ChannelMarker.ForRead<string>("SourcePaths")
},
async self =>
{
var count = 0L;
var size = 0L;
try
{
while (!token.IsCancellationRequested)
{
var path = await self.Input.ReadAsync();
count++;
try
{
size += snapshot.GetFileSize(path);
}
catch
{
}
result.OperationProgressUpdater.UpdatefileCount(count, size, false);
}
}
finally
{
result.OperationProgressUpdater.UpdatefileCount(count, size, true);
}
}
);
return Task.WhenAll(enumeratorTask, counterTask);
}
}
private void PreBackupVerify(BackendManager backend)
{
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Backup_PreBackupVerify);
using(new Logging.Timer("PreBackupVerify"))
{
try
{
if (m_options.NoBackendverification)
FilelistProcessor.VerifyLocalList(backend, m_options, m_database, m_result.BackendWriter);
else
FilelistProcessor.VerifyRemoteList(backend, m_options, m_database, m_result.BackendWriter);
}
catch (Exception ex)
{
if (m_options.AutoCleanup)
{
m_result.AddWarning("Backend verification failed, attempting automatic cleanup", ex);
m_result.RepairResults = new RepairResults(m_result);
new RepairHandler(backend.BackendUrl, m_options, (RepairResults)m_result.RepairResults).Run();
m_result.AddMessage("Backend cleanup finished, retrying verification");
FilelistProcessor.VerifyRemoteList(backend, m_options, m_database, m_result.BackendWriter);
}
else
throw;
}
}
}
private static async Task RunMainOperation(Snapshots.ISnapshotService snapshot, Backup.BackupDatabase database, Common.BackendHandler backend, Backup.BackupStatsCollector stats, Options options, IFilter sourcefilter, IFilter filter, BackupResults result)
{
using(new Logging.Timer("BackupMainOperation"))
{
Task all;
using(new ChannelScope())
{
all = Task.WhenAll(
Backup.DataBlockProcessor.Run(database, options),
Backup.FileBlockProcessor.Start(snapshot, options, database, stats),
new Backup.FileEnumerationProcess(snapshot, options.FileAttributeFilter, sourcefilter, filter, options.SymlinkPolicy, options.HardlinkPolicy, options.ChangedFilelist).RunAsync(),
Backup.FilePreFilterProcess.Start(snapshot, options, stats),
new Backup.MetadataPreProcess(snapshot, options, database).RunAsync(),
Backup.SpillCollectorProcess.Run(options, database),
Backup.ProgressHandler.Run(result),
Backup.BackendUploader.Run(backend, options, database, result)
);
}
await all;
if (options.ChangedFilelist != null && options.ChangedFilelist.Length >= 1)
await database.AppendFilesFromPreviousSetAsync(options.DeletedFilelist);
result.OperationProgressUpdater.UpdatefileCount(result.ExaminedFiles, result.SizeOfExaminedFiles, true);
}
}
private void UploadRealFileList(BackendManager backend, FilesetVolumeWriter filesetvolume)
{
var changeCount =
m_result.AddedFiles + m_result.ModifiedFiles + m_result.DeletedFiles +
m_result.AddedFolders + m_result.ModifiedFolders + m_result.DeletedFolders +
m_result.AddedSymlinks + m_result.ModifiedSymlinks + m_result.DeletedSymlinks;
//Changes in the filelist triggers a filelist upload
if (m_options.UploadUnchangedBackups || changeCount > 0)
{
using(new Logging.Timer("Uploading a new fileset"))
{
if (!string.IsNullOrEmpty(m_options.ControlFiles))
foreach(var p in m_options.ControlFiles.Split(new char[] { System.IO.Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries))
filesetvolume.AddControlFile(p, m_options.GetCompressionHintFromFilename(p));
m_database.WriteFileset(filesetvolume, m_transaction);
filesetvolume.Close();
if (m_options.Dryrun)
m_result.AddDryrunMessage(string.Format("Would upload fileset volume: {0}, size: {1}", filesetvolume.RemoteFilename, Library.Utility.Utility.FormatSizeString(new FileInfo(filesetvolume.LocalFilename).Length)));
else
{
m_database.UpdateRemoteVolume(filesetvolume.RemoteFilename, RemoteVolumeState.Uploading, -1, null, m_transaction);
using(new Logging.Timer("CommitUpdateRemoteVolume"))
m_transaction.Commit();
m_transaction = m_database.BeginTransaction();
backend.Put(filesetvolume);
using(new Logging.Timer("CommitUpdateRemoteVolume"))
m_transaction.Commit();
m_transaction = m_database.BeginTransaction();
}
}
}
else
{
m_result.AddVerboseMessage("removing temp files, as no data needs to be uploaded");
m_database.RemoveRemoteVolume(filesetvolume.RemoteFilename, m_transaction);
}
}
private void CompactIfRequired(BackendManager backend, long lastVolumeSize)
{
var currentIsSmall = lastVolumeSize != -1 && lastVolumeSize <= m_options.SmallFileSize;
if (m_options.KeepTime.Ticks > 0 || m_options.KeepVersions != 0)
{
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Backup_Delete);
m_result.DeleteResults = new DeleteResults(m_result);
using(var db = new LocalDeleteDatabase(m_database))
new DeleteHandler(backend.BackendUrl, m_options, (DeleteResults)m_result.DeleteResults).DoRun(db, ref m_transaction, true, currentIsSmall);
}
else if (currentIsSmall && !m_options.NoAutoCompact)
{
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Backup_Compact);
m_result.CompactResults = new CompactResults(m_result);
using(var db = new LocalDeleteDatabase(m_database))
new CompactHandler(backend.BackendUrl, m_options, (CompactResults)m_result.CompactResults).DoCompact(db, true, ref m_transaction);
}
}
private void PostBackupVerification()
{
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Backup_PostBackupVerify);
using(var backend = new BackendManager(m_backendurl, m_options, m_result.BackendWriter, m_database))
{
using(new Logging.Timer("AfterBackupVerify"))
FilelistProcessor.VerifyRemoteList(backend, m_options, m_database, m_result.BackendWriter);
backend.WaitForComplete(m_database, null);
}
if (m_options.BackupTestSampleCount > 0 && m_database.GetRemoteVolumes().Count() > 0)
{
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Backup_PostBackupTest);
m_result.TestResults = new TestResults(m_result);
using(var testdb = new LocalTestDatabase(m_database))
using(var backend = new BackendManager(m_backendurl, m_options, m_result.BackendWriter, testdb))
new TestHandler(m_backendurl, m_options, new TestResults(m_result))
.DoRun(m_options.BackupTestSampleCount, testdb, backend);
}
}
private static async Task<long> CreateFilesetAsync(Backup.BackupDatabase database, string filename)
{
var filesetvolumeid = await database.RegisterRemoteVolumeAsync(filename, RemoteVolumeType.Files, RemoteVolumeState.Temporary);
await database.CreateFilesetAsync(filesetvolumeid, VolumeBase.ParseFilename(filename).Time);
return filesetvolumeid;
}
public void Run(string[] sources, Library.Utility.IFilter filter)
{
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Backup_Begin);
// New isolated scope for each operation
using(new ChannelScope(true))
using(m_database = new LocalBackupDatabase(m_options.Dbpath, m_options))
{
m_result.SetDatabase(m_database);
m_result.Dryrun = m_options.Dryrun;
Utility.UpdateOptionsFromDb(m_database, m_options);
Utility.VerifyParameters(m_database, m_options);
m_blocksize = m_options.Blocksize;
m_blockbuffer = new byte[m_options.Blocksize * Math.Max(1, m_options.FileReadBufferSize / m_options.Blocksize)];
m_blocklistbuffer = new byte[m_options.Blocksize];
m_blockhasher = System.Security.Cryptography.HashAlgorithm.Create(m_options.BlockHashAlgorithm);
m_filehasher = System.Security.Cryptography.HashAlgorithm.Create(m_options.FileHashAlgorithm);
if (m_blockhasher == null)
throw new Exception(Strings.Foresthash.InvalidHashAlgorithm(m_options.BlockHashAlgorithm));
if (m_filehasher == null)
throw new Exception(Strings.Foresthash.InvalidHashAlgorithm(m_options.FileHashAlgorithm));
if (!m_blockhasher.CanReuseTransform)
throw new Exception(Strings.Foresthash.InvalidCryptoSystem(m_options.BlockHashAlgorithm));
if (!m_filehasher.CanReuseTransform)
throw new Exception(Strings.Foresthash.InvalidCryptoSystem(m_options.FileHashAlgorithm));
m_database.VerifyConsistency(null, m_options.Blocksize, m_options.BlockhashSize);
// If there is no filter, we set an empty filter to simplify the code
// If there is a filter, we make sure that the sources are included
m_filter = filter ?? new Library.Utility.FilterExpression();
m_sourceFilter = new Library.Utility.FilterExpression(sources, true);
Task parallelScanner = null;
try
{
using(var backend = new BackendManager(m_backendurl, m_options, m_result.BackendWriter, m_database))
using(var filesetvolume = new FilesetVolumeWriter(m_options, m_database.OperationTimestamp))
using(var logtarget = ChannelManager.GetChannel<Common.LogMessage>("LogChannel").AsWriteOnly())
{
var lh = Common.LogHandler.Run(m_result);
long lastVolumeSize = -1L;
using(var snapshot = GetSnapshot(sources, m_options, m_result))
{
var counterToken = new CancellationTokenSource();
try
{
// Start the parallel scanner
parallelScanner = CountFilesHandler(snapshot, m_result, m_options, m_sourceFilter, m_filter, counterToken.Token);
// Do a remote verification, unless disabled
PreBackupVerify(backend);
using(var db = new Backup.BackupDatabase(m_database))
using(var stats = new Backup.BackupStatsCollector(m_result))
using(var bk = new Common.BackendHandler(m_options, m_backendurl, db, stats))
{
// Verify before uploading a synthetic list
m_database.VerifyConsistency(null, m_options.Blocksize, m_options.BlockhashSize);
var res = Backup.UploadSyntheticFilelist.Run(bk, db, m_options, m_result).WaitForTask();
if (res.IsFaulted)
{
if (res.Exception.Flatten().InnerExceptions.Count == 1)
throw res.Exception.Flatten().InnerExceptions.First();
else
throw res.Exception;
}
m_database.BuildLookupTable(m_options);
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Backup_ProcessingFiles);
var filesetvolumeid = CreateFilesetAsync(db, filesetvolume.RemoteFilename).Result;
res = RunMainOperation(snapshot, db, bk, stats, m_options, m_sourceFilter, m_filter, m_result).WaitForTask();
if (res.IsFaulted)
{
if (res.Exception.Flatten().InnerExceptions.Count == 1)
throw res.Exception.Flatten().InnerExceptions.First();
else
throw res.Exception;
}
}
}
finally
{
//If the scanner is still running for some reason, make sure we kill it now
counterToken.Cancel();
}
}
// TODO: Implement this
//var lastVolumeSize = FinalizeRemoteVolumes(backend);
m_transaction = m_database.BeginTransaction();
using(new Logging.Timer("UpdateChangeStatistics"))
m_database.UpdateChangeStatistics(m_result);
using(new Logging.Timer("VerifyConsistency"))
m_database.VerifyConsistency(m_transaction, m_options.Blocksize, m_options.BlockhashSize);
UploadRealFileList(backend, filesetvolume);
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Backup_WaitForUpload);
using(new Logging.Timer("Async backend wait"))
backend.WaitForComplete(m_database, m_transaction);
if (m_result.TaskControlRendevouz() != TaskControlState.Stop)
CompactIfRequired(backend, lastVolumeSize);
if (m_options.UploadVerificationFile)
{
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Backup_VerificationUpload);
FilelistProcessor.UploadVerificationFile(backend.BackendUrl, m_options, m_result.BackendWriter, m_database, m_transaction);
}
if (m_options.Dryrun)
{
m_transaction.Rollback();
m_transaction = null;
}
else
{
using(new Logging.Timer("CommitFinalizingBackup"))
m_transaction.Commit();
m_transaction = null;
m_database.Vacuum();
if (m_result.TaskControlRendevouz() != TaskControlState.Stop && !m_options.NoBackendverification)
{
PostBackupVerification();
}
}
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Backup_Complete);
m_database.WriteResults();
m_database.PurgeLogData(m_options.LogRetention);
return;
}
}
catch (Exception ex)
{
m_result.AddError("Fatal error", ex);
throw;
}
finally
{
if (parallelScanner != null && !parallelScanner.IsCompleted)
parallelScanner.Wait(500);
// TODO: We want to commit? always?
if (m_transaction != null)
try { m_transaction.Rollback(); }
catch (Exception ex) { m_result.AddError(string.Format("Rollback error: {0}", ex.Message), ex); }
}
}
}
private void UpdateIndexVolume()
{
if (m_indexvolume != null)
{
m_database.AddIndexBlockLink(m_indexvolume.VolumeID, m_blockvolume.VolumeID, m_transaction);
m_indexvolume.StartVolume(m_blockvolume.RemoteFilename);
foreach(var b in m_database.GetBlocks(m_blockvolume.VolumeID))
m_indexvolume.AddBlock(b.Hash, b.Size);
m_database.UpdateRemoteVolume(m_indexvolume.RemoteFilename, RemoteVolumeState.Uploading, -1, null, m_transaction);
}
}
public void Dispose()
{
if (m_blockvolume != null)
{
try { m_blockvolume.Dispose(); }
catch (Exception ex) { m_result.AddError("Failed disposing block volume", ex); }
finally { m_blockvolume = null; }
}
if (m_indexvolume != null)
{
try { m_indexvolume.Dispose(); }
catch (Exception ex) { m_result.AddError("Failed disposing index volume", ex); }
finally { m_indexvolume = null; }
}
m_result.EndTime = DateTime.UtcNow;
}
}
}