2013-03-27 16:06:45 +01:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Text ;
2013-05-08 20:17:07 +02:00
using Duplicati.Library.Main.Database ;
using Duplicati.Library.Main.Volumes ;
2013-03-27 16:06:45 +01:00
2013-05-08 20:17:07 +02:00
namespace Duplicati.Library.Main.Operation
2013-03-27 16:06:45 +01:00
{
2013-05-25 16:40:15 +02:00
internal class RepairHandler
2013-03-27 16:06:45 +01:00
{
private string m_backendurl ;
2013-05-06 09:58:19 +02:00
private Options m_options ;
2013-05-25 16:40:15 +02:00
private RepairResults m_result ;
2013-03-27 16:06:45 +01:00
2013-05-25 16:40:15 +02:00
public RepairHandler ( string backend , Options options , RepairResults result )
2013-03-27 16:06:45 +01:00
{
m_backendurl = backend ;
m_options = options ;
2013-05-25 16:40:15 +02:00
m_result = result ;
2013-10-29 11:43:44 +01:00
if ( options . AllowPassphraseChange )
throw new Exception ( Strings . Foresthash . PassphraseChangeUnsupported );
2013-03-27 16:06:45 +01:00
}
2013-06-16 23:35:46 +02:00
2013-03-27 16:06:45 +01:00
public void Run ()
2013-06-16 23:35:46 +02:00
{
if (! System . IO . File . Exists ( m_options . Dbpath ))
{
RunRepairLocal ();
return ;
}
2013-06-20 09:19:57 +02:00
long knownRemotes = - 1 ;
2013-06-16 23:35:46 +02:00
try
{
using ( var db = new LocalRepairDatabase ( m_options . Dbpath ))
2013-07-01 11:58:33 +02:00
knownRemotes = db . GetRemoteVolumes (). Count ();
2013-06-16 23:35:46 +02:00
}
catch ( Exception ex )
{
m_result . AddWarning ( string . Format ( "Failed to read local db {0}, error: {1}" , m_options . Dbpath , ex . Message ), ex );
}
2013-06-20 09:19:57 +02:00
if ( knownRemotes <= 0 )
{
if ( m_options . Dryrun )
{
2013-06-20 20:20:03 +02:00
m_result . AddDryrunMessage ( "Performing dryrun recreate" );
2013-06-20 09:19:57 +02:00
}
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 );
}
2013-06-16 23:35:46 +02:00
2013-06-20 09:19:57 +02:00
RunRepairLocal ();
2013-06-16 23:35:46 +02:00
}
else
{
RunRepairRemote ();
}
}
public void RunRepairLocal ()
{
m_result . RecreateDatabaseResults = new RecreateDatabaseResults ( m_result );
using ( new Logging . Timer ( "Recreate database for repair" ))
2013-06-20 20:20:03 +02:00
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 );
}
2013-06-16 23:35:46 +02:00
}
public void RunRepairRemote ()
2013-03-27 16:06:45 +01:00
{
2013-06-20 20:20:03 +02:00
if (! System . IO . File . Exists ( m_options . Dbpath ))
throw new Exception ( string . Format ( "Database file does not exist: {0}" , m_options . Dbpath ));
2013-03-30 15:23:09 +01:00
2013-06-20 20:20:03 +02:00
using ( var db = new LocalRepairDatabase ( m_options . Dbpath ))
using ( var backend = new BackendManager ( m_backendurl , m_options , m_result . BackendWriter , db ))
{
2013-05-25 16:40:15 +02:00
m_result . SetDatabase ( db );
2013-06-20 20:20:03 +02:00
Utility . VerifyParameters ( db , m_options );
2013-03-31 19:28:46 +02:00
2013-06-20 20:20:03 +02:00
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 );
2013-07-24 21:25:41 +02:00
var hashsize = blockhasher . HashSize / 8 ;
2013-03-30 15:23:09 +01:00
2013-06-20 20:20:03 +02:00
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 ));
2013-03-30 15:23:09 +01:00
2013-06-20 20:20:03 +02:00
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 ()));
}
}
2013-04-09 20:43:27 +02:00
2013-06-20 09:19:57 +02:00
// TODO: It is actually possible to use the extra files if we parse them
2013-06-20 20:20:03 +02:00
foreach ( var n in tp . ExtraVolumes )
try
{
2014-05-15 12:47:16 +02:00
if ( m_result . TaskControlRendevouz () == TaskControlState . Stop )
{
backend . WaitForComplete ( db , null );
return ;
}
2013-06-20 20:20:03 +02:00
if (! m_options . Dryrun )
{
db . RegisterRemoteVolume ( n . File . Name , n . FileType , RemoteVolumeState . Deleting );
2013-08-25 07:49:31 +02:00
backend . Delete ( n . File . Name , n . File . Size );
2013-06-20 20:20:03 +02:00
}
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 );
2014-05-15 12:47:16 +02:00
if ( ex is System . Threading . ThreadAbortException )
throw ;
2013-06-20 20:20:03 +02:00
}
2013-03-30 15:23:09 +01:00
2013-06-20 20:20:03 +02:00
foreach ( var n in tp . MissingVolumes )
{
2013-06-24 20:49:32 +02:00
IDisposable newEntry = null ;
2013-06-20 20:20:03 +02:00
try
2014-05-15 12:47:16 +02:00
{
if ( m_result . TaskControlRendevouz () == TaskControlState . Stop )
{
backend . WaitForComplete ( db , null );
return ;
}
2013-06-20 20:20:03 +02:00
if ( n . Type == RemoteVolumeType . Files )
{
var filesetId = db . GetFilesetIdFromRemotename ( n . Name );
var w = new FilesetVolumeWriter ( m_options , DateTime . UtcNow );
2013-06-24 20:49:32 +02:00
newEntry = w ;
2013-06-20 20:20:03 +02:00
w . SetRemoteFilename ( n . Name );
2013-04-09 20:43:27 +02:00
2013-06-20 20:20:03 +02:00
db . WriteFileset ( w , null , filesetId );
2013-04-09 20:43:27 +02:00
2013-06-20 20:20:03 +02:00
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 );
2013-06-24 20:49:32 +02:00
newEntry = w ;
2013-06-20 20:20:03 +02:00
w . SetRemoteFilename ( n . Name );
2013-03-30 15:23:09 +01:00
2013-06-20 20:20:03 +02:00
foreach ( var blockvolume in db . GetBlockVolumesFromIndexName ( n . Name ))
{
w . StartVolume ( blockvolume . Name );
var volumeid = db . GetRemoteVolumeID ( blockvolume . Name );
2013-04-09 20:43:27 +02:00
2013-06-20 20:20:03 +02:00
foreach ( var b in db . GetBlocks ( volumeid ))
w . AddBlock ( b . Hash , b . Size );
2013-04-09 20:43:27 +02:00
2013-06-20 20:20:03 +02:00
w . FinishVolume ( blockvolume . Hash , blockvolume . Size );
2013-07-24 21:25:41 +02:00
if ( m_options . IndexfilePolicy == Options . IndexFileStrategy . Full )
foreach ( var b in db . GetBlocklists ( volumeid , m_options . Blocksize , hashsize ))
2013-08-30 22:46:34 +02:00
w . WriteBlocklist ( b . Item1 , b . Item2 , 0 , b . Item3 );
2013-06-20 20:20:03 +02:00
}
2013-04-09 20:43:27 +02:00
2013-06-20 20:20:03 +02:00
w . Close ();
2013-04-09 20:43:27 +02:00
2013-06-20 20:20:03 +02:00
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 )
2013-06-24 20:49:32 +02:00
{
2013-06-20 20:20:03 +02:00
var w = new BlockVolumeWriter ( m_options );
2013-06-24 20:49:32 +02:00
newEntry = w ;
2013-06-20 20:20:03 +02:00
w . SetRemoteFilename ( n . Name );
2013-06-24 20:49:32 +02:00
using ( var mbl = db . CreateBlockList ( n . Name ))
2013-06-20 20:20:03 +02:00
{
2013-06-24 20:49:32 +02:00
//First we grab all known blocks from local files
foreach ( var block in mbl . GetSourceFilesWithBlocks ( m_options . Blocksize ))
2013-06-20 20:20:03 +02:00
{
2013-06-24 20:49:32 +02:00
var hash = block . Hash ;
var size = ( int ) block . Size ;
foreach ( var source in block . Sources )
2013-06-20 20:20:03 +02:00
{
2013-06-24 20:49:32 +02:00
var file = source . File ;
var offset = source . Offset ;
2013-06-20 20:20:03 +02:00
try
{
2013-06-24 20:49:32 +02:00
if ( System . IO . File . Exists ( file ))
using ( var f = System . IO . File . OpenRead ( file ))
2013-06-20 20:20:03 +02:00
{
2013-06-24 20:49:32 +02:00
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 ))
2013-08-30 22:46:34 +02:00
w . AddBlock ( hash , buffer , 0 , size , Duplicati . Library . Interface . CompressionHint . Default );
2013-06-24 20:49:32 +02:00
break ;
}
}
2013-06-20 20:20:03 +02:00
}
}
catch ( Exception ex )
{
2013-06-24 20:49:32 +02:00
m_result . AddError ( string . Format ( "Failed to access file: {0}" , file ), ex );
}
2013-06-20 20:20:03 +02:00
}
}
2013-06-24 20:49:32 +02:00
//Then we grab all remote volumes that have the missing blocks
foreach ( var vol in new AsyncDownloader ( mbl . GetMissingBlockSources (). ToList (), backend ))
2013-06-20 20:20:03 +02:00
{
2013-06-24 20:49:32 +02:00
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 )
2013-08-30 22:46:34 +02:00
w . AddBlock ( b . Key , buffer , 0 , ( int ) b . Value , Duplicati . Library . Interface . CompressionHint . Default );
2013-06-24 20:49:32 +02:00
}
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 ())
2013-06-20 20:20:03 +02:00
m_result . AddMessage ( f . Name );
2013-06-24 20:49:32 +02:00
if (! m_options . Dryrun )
{
2013-11-23 14:32:32 +01:00
m_result . AddMessage ( "This may be fixed by deleting the filesets and running repair again" );
2013-06-24 20:49:32 +02:00
throw new Exception ( string . Format ( "Repair not possible, missing {0} blocks!!!" , missingBlocks ));
}
2013-06-20 20:20:03 +02:00
}
else
{
2013-06-24 20:49:32 +02:00
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 );
}
2013-06-20 20:20:03 +02:00
}
}
}
}
catch ( Exception ex )
{
2013-06-24 20:49:32 +02:00
if ( newEntry != null )
try { newEntry . Dispose (); }
catch { }
finally { newEntry = null ; }
2013-06-20 20:20:03 +02:00
m_result . AddError ( string . Format ( "Failed to perform cleanup for missing file: {0}, message: {1}" , n . Name , ex . Message ), ex );
2014-05-15 12:47:16 +02:00
if ( ex is System . Threading . ThreadAbortException )
throw ;
2013-06-20 20:20:03 +02:00
}
}
}
else
{
m_result . AddMessage ( "Destination and database are synchronized, not making any changes" );
}
2013-03-30 15:23:09 +01:00
2013-04-09 20:43:27 +02:00
backend . WaitForComplete ( db , null );
2014-07-16 00:57:57 +02:00
db . WriteResults ();
2013-03-30 15:23:09 +01:00
}
2013-03-27 16:06:45 +01:00
}
}
}