Files
duplicati/Duplicati/Library/Main/Operation/RepairHandler.cs
T
2013-11-23 14:32:32 +01:00

307 lines
17 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Duplicati.Library.Main.Database;
using Duplicati.Library.Main.Volumes;
namespace Duplicati.Library.Main.Operation
{
internal class RepairHandler
{
private string m_backendurl;
private Options m_options;
private RepairResults m_result;
public RepairHandler(string backend, Options options, RepairResults result)
{
m_backendurl = backend;
m_options = options;
m_result = result;
if (options.AllowPassphraseChange)
throw new Exception(Strings.Foresthash.PassphraseChangeUnsupported);
}
public void Run()
{
if (!System.IO.File.Exists(m_options.Dbpath))
{
RunRepairLocal();
return;
}
long knownRemotes = -1;
try
{
using(var db = new LocalRepairDatabase(m_options.Dbpath))
knownRemotes = db.GetRemoteVolumes().Count();
}
catch (Exception ex)
{
m_result.AddWarning(string.Format("Failed to read local db {0}, error: {1}", m_options.Dbpath, ex.Message), ex);
}
if (knownRemotes <= 0)
{
if (m_options.Dryrun)
{
m_result.AddDryrunMessage("Performing dryrun recreate");
}
else
{
var baseName = System.IO.Path.ChangeExtension(m_options.Dbpath, "backup");
var i = 0;
while (System.IO.File.Exists(baseName) && i++ < 1000)
baseName = System.IO.Path.ChangeExtension(m_options.Dbpath, "backup-" + i.ToString());
m_result.AddMessage(string.Format("Renaming existing db from {0} to {1}", m_options.Dbpath, baseName));
System.IO.File.Move(m_options.Dbpath, baseName);
}
RunRepairLocal();
}
else
{
RunRepairRemote();
}
}
public void RunRepairLocal()
{
m_result.RecreateDatabaseResults = new RecreateDatabaseResults(m_result);
using(new Logging.Timer("Recreate database for repair"))
using(var f = m_options.Dryrun ? new Library.Utility.TempFile() : null)
{
if (f != null && System.IO.File.Exists(f))
System.IO.File.Delete(f);
new RecreateDatabaseHandler(m_backendurl, m_options, (RecreateDatabaseResults)m_result.RecreateDatabaseResults)
.Run(m_options.Dryrun ? (string)f : m_options.Dbpath);
}
}
public void RunRepairRemote()
{
if (!System.IO.File.Exists(m_options.Dbpath))
throw new Exception(string.Format("Database file does not exist: {0}", m_options.Dbpath));
using(var db = new LocalRepairDatabase(m_options.Dbpath))
using(var backend = new BackendManager(m_backendurl, m_options, m_result.BackendWriter, db))
{
m_result.SetDatabase(db);
Utility.VerifyParameters(db, m_options);
var tp = FilelistProcessor.RemoteListAnalysis(backend, m_options, db, m_result.BackendWriter);
var buffer = new byte[m_options.Blocksize];
var blockhasher = System.Security.Cryptography.HashAlgorithm.Create(m_options.BlockHashAlgorithm);
var hashsize = blockhasher.HashSize / 8;
if (blockhasher == null)
throw new Exception(string.Format(Strings.Foresthash.InvalidHashAlgorithm, m_options.BlockHashAlgorithm));
if (!blockhasher.CanReuseTransform)
throw new Exception(string.Format(Strings.Foresthash.InvalidCryptoSystem, m_options.BlockHashAlgorithm));
if (tp.ExtraVolumes.Count() > 0 || tp.MissingVolumes.Count() > 0)
{
if (m_options.Dryrun)
{
if (!tp.BackupPrefixes.Contains(m_options.Prefix) && tp.ParsedVolumes.Count() > 0)
{
if (tp.BackupPrefixes.Length == 1)
throw new Exception(string.Format("Found no backup files with prefix {0}, but files with prefix {1}, did you forget to set the backup-prefix?", m_options.Prefix, tp.BackupPrefixes[0]));
else
throw new Exception(string.Format("Found no backup files with prefix {0}, but files with prefixes {1}, did you forget to set the backup-prefix?", m_options.Prefix, string.Join(", ", tp.BackupPrefixes)));
}
else if (tp.ParsedVolumes.Count() == 0 && tp.ExtraVolumes.Count() > 0)
{
throw new Exception(string.Format("No files were missing, but {0} remote files were, found, did you mean to run recreate-database?", tp.ExtraVolumes.Count()));
}
}
// TODO: It is actually possible to use the extra files if we parse them
foreach(var n in tp.ExtraVolumes)
try
{
if (!m_options.Dryrun)
{
db.RegisterRemoteVolume(n.File.Name, n.FileType, RemoteVolumeState.Deleting);
backend.Delete(n.File.Name, n.File.Size);
}
else
m_result.AddDryrunMessage(string.Format("would delete file {0}", n.File.Name));
}
catch (Exception ex)
{
m_result.AddError(string.Format("Failed to perform cleanup for extra file: {0}, message: {1}", n.File.Name, ex.Message), ex);
}
foreach(var n in tp.MissingVolumes)
{
IDisposable newEntry = null;
try
{
if (n.Type == RemoteVolumeType.Files)
{
var filesetId = db.GetFilesetIdFromRemotename(n.Name);
var w = new FilesetVolumeWriter(m_options, DateTime.UtcNow);
newEntry = w;
w.SetRemoteFilename(n.Name);
db.WriteFileset(w, null, filesetId);
w.Close();
if (m_options.Dryrun)
m_result.AddDryrunMessage(string.Format("would re-upload fileset {0}, with size {1}, previous size {2}", n.Name, Library.Utility.Utility.FormatSizeString(new System.IO.FileInfo(w.LocalFilename).Length), Library.Utility.Utility.FormatSizeString(n.Size)));
else
{
db.UpdateRemoteVolume(w.RemoteFilename, RemoteVolumeState.Uploading, -1, null, null);
backend.Put(w);
}
}
else if (n.Type == RemoteVolumeType.Index)
{
var w = new IndexVolumeWriter(m_options);
newEntry = w;
w.SetRemoteFilename(n.Name);
foreach(var blockvolume in db.GetBlockVolumesFromIndexName(n.Name))
{
w.StartVolume(blockvolume.Name);
var volumeid = db.GetRemoteVolumeID(blockvolume.Name);
foreach(var b in db.GetBlocks(volumeid))
w.AddBlock(b.Hash, b.Size);
w.FinishVolume(blockvolume.Hash, blockvolume.Size);
if (m_options.IndexfilePolicy == Options.IndexFileStrategy.Full)
foreach(var b in db.GetBlocklists(volumeid, m_options.Blocksize, hashsize))
w.WriteBlocklist(b.Item1, b.Item2, 0, b.Item3);
}
w.Close();
if (m_options.Dryrun)
m_result.AddDryrunMessage(string.Format("would re-upload index file {0}, with size {1}, previous size {2}", n.Name, Library.Utility.Utility.FormatSizeString(new System.IO.FileInfo(w.LocalFilename).Length), Library.Utility.Utility.FormatSizeString(n.Size)));
else
{
db.UpdateRemoteVolume(w.RemoteFilename, RemoteVolumeState.Uploading, -1, null, null);
backend.Put(w);
}
}
else if (n.Type == RemoteVolumeType.Blocks)
{
var w = new BlockVolumeWriter(m_options);
newEntry = w;
w.SetRemoteFilename(n.Name);
using(var mbl = db.CreateBlockList(n.Name))
{
//First we grab all known blocks from local files
foreach(var block in mbl.GetSourceFilesWithBlocks(m_options.Blocksize))
{
var hash = block.Hash;
var size = (int)block.Size;
foreach(var source in block.Sources)
{
var file = source.File;
var offset = source.Offset;
try
{
if (System.IO.File.Exists(file))
using(var f = System.IO.File.OpenRead(file))
{
f.Position = offset;
if (size == Library.Utility.Utility.ForceStreamRead(f, buffer, size))
{
var newhash = Convert.ToBase64String(blockhasher.ComputeHash(buffer, 0, size));
if (newhash == hash)
{
if (mbl.SetBlockRestored(hash, size))
w.AddBlock(hash, buffer, 0, size, Duplicati.Library.Interface.CompressionHint.Default);
break;
}
}
}
}
catch (Exception ex)
{
m_result.AddError(string.Format("Failed to access file: {0}", file), ex);
}
}
}
//Then we grab all remote volumes that have the missing blocks
foreach(var vol in new AsyncDownloader(mbl.GetMissingBlockSources().ToList(), backend))
{
try
{
using(var tmpfile = vol.TempFile)
using(var f = new BlockVolumeReader(RestoreHandler.GetCompressionModule(vol.Name), tmpfile, m_options))
foreach(var b in f.Blocks)
if (mbl.SetBlockRestored(b.Key, b.Value))
if (f.ReadBlock(b.Key, buffer) == b.Value)
w.AddBlock(b.Key, buffer, 0, (int)b.Value, Duplicati.Library.Interface.CompressionHint.Default);
}
catch (Exception ex)
{
m_result.AddError(string.Format("Failed to access remote file: {0}", vol.Name), ex);
}
}
// If we managed to recover all blocks, NICE!
var missingBlocks = mbl.GetMissingBlocks().Count();
if (missingBlocks > 0)
{
//TODO: How do we handle this situation?
m_result.AddMessage(string.Format("Repair cannot acquire {0} required blocks for volume {1}, which are required by the following filesets: ", missingBlocks, n.Name));
foreach(var f in mbl.GetFilesetsUsingMissingBlocks())
m_result.AddMessage(f.Name);
if (!m_options.Dryrun)
{
m_result.AddMessage("This may be fixed by deleting the filesets and running repair again");
throw new Exception(string.Format("Repair not possible, missing {0} blocks!!!", missingBlocks));
}
}
else
{
if (m_options.Dryrun)
m_result.AddDryrunMessage(string.Format("would re-upload block file {0}, with size {1}, previous size {2}", n.Name, Library.Utility.Utility.FormatSizeString(new System.IO.FileInfo(w.LocalFilename).Length), Library.Utility.Utility.FormatSizeString(n.Size)));
else
{
db.UpdateRemoteVolume(w.RemoteFilename, RemoteVolumeState.Uploading, -1, null, null);
backend.Put(w);
}
}
}
}
}
catch (Exception ex)
{
if (newEntry != null)
try { newEntry.Dispose(); }
catch { }
finally { newEntry = null; }
m_result.AddError(string.Format("Failed to perform cleanup for missing file: {0}, message: {1}", n.Name, ex.Message), ex);
}
}
}
else
{
m_result.AddMessage("Destination and database are synchronized, not making any changes");
}
backend.WaitForComplete(db, null);
}
}
}
}