2015-01-20 21:44:52 +01:00
// Copyright (C) 2015, The Duplicati Team
2013-05-08 21:29:59 +02:00
2015-01-20 21:44:52 +01:00
// http://www.duplicati.com, info@duplicati.com
2013-05-08 21:29:59 +02:00
//
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
using System ;
using Duplicati.Library.Main.Database ;
using System.Collections.Generic ;
using System.Linq ;
2013-05-25 16:40:15 +02:00
namespace Duplicati.Library.Main.Operation
2013-05-08 21:29:59 +02:00
{
2013-08-20 22:16:30 +02:00
internal static class FilelistProcessor
2013-05-08 21:29:59 +02:00
{
2014-12-30 17:29:29 +01:00
/// <summary>
/// Helper method that verifies uploaded volumes and updates their state in the database.
/// Throws an error if there are issues with the remote storage
/// </summary>
/// <param name="options">The options used</param>
/// <param name="database">The database to compare with</param>
/// <param name="log">The log instance to use</param>
public static void VerifyLocalList ( BackendManager backend , Options options , LocalDatabase database , IBackendWriter log )
{
var locallist = database . GetRemoteVolumes ();
foreach ( var i in locallist )
{
switch ( i . State )
{
case RemoteVolumeState . Uploaded :
case RemoteVolumeState . Verified :
case RemoteVolumeState . Deleted :
break ;
case RemoteVolumeState . Temporary :
case RemoteVolumeState . Deleting :
case RemoteVolumeState . Uploading :
log . AddMessage ( string . Format ( "removing remote file listed as {0}: {1}" , i . State , i . Name ));
try
{
backend . Delete ( i . Name , i . Size , true );
}
catch ( Exception ex )
{
log . AddWarning ( string . Format ( "Failed to erase file {0}, treating as deleted: {1}" , i . Name , ex . Message ), ex );
}
break ;
default :
log . AddWarning ( string . Format ( "unknown state for remote file listed as {0}: {1}" , i . State , i . Name ), null );
break ;
}
backend . FlushDbMessages ();
}
}
2013-05-08 21:29:59 +02:00
/// <summary>
/// Helper method that verifies uploaded volumes and updates their state in the database.
/// Throws an error if there are issues with the remote storage
/// </summary>
/// <param name="backend">The backend instance to use</param>
/// <param name="options">The options used</param>
/// <param name="database">The database to compare with</param>
2014-12-30 17:29:29 +01:00
/// <param name="log">The log instance to use</param>
2013-05-25 16:40:15 +02:00
public static void VerifyRemoteList ( BackendManager backend , Options options , LocalDatabase database , IBackendWriter log )
{
var tp = RemoteListAnalysis ( backend , options , database , log );
long extraCount = 0 ;
long missingCount = 0 ;
2013-05-08 21:29:59 +02:00
2013-05-25 16:40:15 +02:00
foreach ( var n in tp . ExtraVolumes )
{
log . AddWarning ( string . Format ( "Extra unknown file: {0}" , n . File . Name ), null );
extraCount ++;
}
2013-05-08 21:29:59 +02:00
2013-05-25 16:40:15 +02:00
foreach ( var n in tp . MissingVolumes )
{
log . AddWarning ( string . Format ( "Missing file: {0}" , n . Name ), null );
missingCount ++;
}
2013-05-08 21:29:59 +02:00
2013-05-25 16:40:15 +02:00
if ( extraCount > 0 )
{
2013-07-01 14:40:45 +02:00
var s = string . Format ( "Found {0} remote files that are not recorded in local storage, please run repair" , extraCount );
2013-05-25 16:40:15 +02:00
log . AddError ( s , null );
throw new Exception ( s );
}
2013-05-08 21:29:59 +02:00
2016-01-06 21:52:30 +01:00
var lookup = new Dictionary < string , string >();
var doubles = new Dictionary < string , string >();
foreach ( var v in tp . ParsedVolumes )
{
if ( lookup . ContainsKey ( v . File . Name ))
doubles [ v . File . Name ] = null ;
else
lookup [ v . File . Name ] = null ;
}
if ( doubles . Count > 0 )
{
2016-03-10 21:53:00 +01:00
var s = string . Format ( "Found remote files reported as duplicates, either the backend module is broken or you need to manually remove the extra copies.\nThe following files were found multiple times: {0}" , string . Join ( ", " , doubles . Keys ));
2016-01-06 21:52:30 +01:00
log . AddError ( s , null );
throw new Exception ( s );
}
2013-05-08 21:29:59 +02:00
if ( missingCount > 0 )
{
2013-05-25 16:40:15 +02:00
string s ;
2013-05-13 22:32:05 +02:00
if (! tp . BackupPrefixes . Contains ( options . Prefix ) && tp . BackupPrefixes . Length > 0 )
2013-05-25 16:40:15 +02:00
s = string . Format ( "Found {0} files that are missing from the remote storage, and no files with the backup prefix {1}, but found the following backup prefixes: {2}" , missingCount , options . Prefix , string . Join ( ", " , tp . BackupPrefixes ));
2013-05-08 21:29:59 +02:00
else
2013-07-01 14:40:45 +02:00
s = string . Format ( "Found {0} files that are missing from the remote storage, please run repair" , missingCount );
2013-05-25 16:40:15 +02:00
log . AddError ( s , null );
throw new Exception ( s );
2013-07-01 14:40:45 +02:00
}
2013-05-08 21:29:59 +02:00
}
public struct RemoteAnalysisResult
{
public IEnumerable < Volumes . IParsedVolume > ParsedVolumes ;
public IEnumerable < Volumes . IParsedVolume > ExtraVolumes ;
2015-01-07 09:38:42 +01:00
public IEnumerable < Volumes . IParsedVolume > OtherVolumes ;
2013-05-08 21:29:59 +02:00
public IEnumerable < RemoteVolumeEntry > MissingVolumes ;
2014-08-26 12:51:09 +02:00
public IEnumerable < RemoteVolumeEntry > VerificationRequiredVolumes ;
2015-01-07 09:38:42 +01:00
public string [] BackupPrefixes { get { return ParsedVolumes . Union ( ExtraVolumes ). Union ( OtherVolumes ). Select ( x => x . Prefix ). Distinct (). ToArray (); } }
2013-05-08 21:29:59 +02:00
}
2013-07-01 14:40:45 +02:00
/// <summary>
/// Creates a temporary verification file.
/// </summary>
/// <returns>The verification file.</returns>
/// <param name="db">The database instance</param>
/// <param name="stream">The stream to write to</param>
public static void CreateVerificationFile ( LocalDatabase db , System . IO . StreamWriter stream )
{
var s = new Newtonsoft . Json . JsonSerializer ();
s . Serialize ( stream , db . GetRemoteVolumes (). Cast < IRemoteVolume >(). ToArray ());
}
/// <summary>
/// Uploads the verification file.
/// </summary>
/// <param name="backendurl">The backend url</param>
/// <param name="options">The options to use</param>
/// <param name="result">The result writer</param>
/// <param name="db">The attached database</param>
/// <param name="transaction">An optional transaction object</param>
public static void UploadVerificationFile ( string backendurl , Options options , IBackendWriter result , LocalDatabase db , System . Data . IDbTransaction transaction )
{
using ( var backend = new BackendManager ( backendurl , options , result , db ))
using ( var tempfile = new Library . Utility . TempFile ())
{
var remotename = options . Prefix + "-verification.json" ;
using ( var stream = new System . IO . StreamWriter ( tempfile , false , System . Text . Encoding . UTF8 ))
FilelistProcessor . CreateVerificationFile ( db , stream );
if ( options . Dryrun )
{
result . AddDryrunMessage ( string . Format ( "Would upload verification file: {0}, size: {1}" , remotename , Library . Utility . Utility . FormatSizeString ( new System . IO . FileInfo ( tempfile ). Length )));
}
else
{
backend . PutUnencrypted ( remotename , tempfile );
backend . WaitForComplete ( db , transaction );
}
}
}
2013-05-08 21:29:59 +02:00
/// <summary>
/// Helper method that verifies uploaded volumes and updates their state in the database.
/// Throws an error if there are issues with the remote storage
/// </summary>
/// <param name="backend">The backend instance to use</param>
/// <param name="options">The options used</param>
/// <param name="database">The database to compare with</param>
2013-05-25 16:40:15 +02:00
public static RemoteAnalysisResult RemoteListAnalysis ( BackendManager backend , Options options , LocalDatabase database , IBackendWriter log )
2013-05-08 21:29:59 +02:00
{
var rawlist = backend . List ();
var lookup = new Dictionary < string , Volumes . IParsedVolume >();
2014-08-19 20:20:01 +02:00
var remotelist = ( from n in rawlist
let p = Volumes . VolumeBase . ParseFilename ( n )
2015-01-07 09:38:42 +01:00
where p != null && p . Prefix == options . Prefix
2014-08-19 20:20:01 +02:00
select p ). ToList ();
2015-01-07 09:38:42 +01:00
var otherlist = ( from n in rawlist
let p = Volumes . VolumeBase . ParseFilename ( n )
where p != null && p . Prefix != options . Prefix
select p ). ToList ();
2014-08-19 20:20:01 +02:00
var unknownlist = ( from n in rawlist
let p = Volumes . VolumeBase . ParseFilename ( n )
where p == null
select n ). ToList ();
2015-01-07 09:38:42 +01:00
2014-08-19 20:20:01 +02:00
var filesets = ( from n in remotelist
where n . FileType == RemoteVolumeType . Files orderby n . Time descending
select n ). ToList ();
2015-01-07 09:38:42 +01:00
2013-05-25 16:40:15 +02:00
log . KnownFileCount = remotelist . Count ();
log . KnownFileSize = remotelist . Select ( x => x . File . Size ). Sum ();
log . UnknownFileCount = unknownlist . Count ();
log . UnknownFileSize = unknownlist . Select ( x => x . Size ). Sum ();
log . BackupListCount = filesets . Count ;
log . LastBackupDate = filesets . Count == 0 ? new DateTime ( 0 ) : filesets [ 0 ]. Time . ToLocalTime ();
if ( backend is Library . Interface . IQuotaEnabledBackend )
{
log . TotalQuotaSpace = (( Library . Interface . IQuotaEnabledBackend ) backend ). TotalQuotaSpace ;
log . FreeQuotaSpace = (( Library . Interface . IQuotaEnabledBackend ) backend ). FreeQuotaSpace ;
}
log . AssignedQuotaSpace = options . QuotaSize ;
2014-08-19 20:20:01 +02:00
foreach ( var s in remotelist )
2015-01-07 09:38:42 +01:00
lookup [ s . File . Name ] = s ;
2013-05-25 16:40:15 +02:00
2013-05-08 21:29:59 +02:00
var missing = new List < RemoteVolumeEntry >();
2014-08-26 12:51:09 +02:00
var missingHash = new List < Tuple < long , RemoteVolumeEntry >>();
2013-05-08 21:29:59 +02:00
var locallist = database . GetRemoteVolumes ();
2014-08-19 20:20:01 +02:00
foreach ( var i in locallist )
2013-05-08 21:29:59 +02:00
{
2014-08-19 20:20:01 +02:00
Volumes . IParsedVolume r ;
var remoteFound = lookup . TryGetValue ( i . Name , out r );
var correctSize = remoteFound && i . Size >= 0 && ( i . Size == r . File . Size || r . File . Size < 0 );
lookup . Remove ( i . Name );
switch ( i . State )
2013-05-08 21:29:59 +02:00
{
2014-08-19 20:20:01 +02:00
case RemoteVolumeState . Deleted :
if ( remoteFound )
log . AddMessage ( string . Format ( "ignoring remote file listed as {0}: {1}" , i . State , i . Name ));
break ;
case RemoteVolumeState . Temporary :
case RemoteVolumeState . Deleting :
if ( remoteFound )
{
log . AddMessage ( string . Format ( "removing remote file listed as {0}: {1}" , i . State , i . Name ));
backend . Delete ( i . Name , i . Size , true );
}
else
2013-05-08 21:29:59 +02:00
{
2015-04-05 14:33:13 +02:00
if ( i . deleteGracePeriod > DateTime . UtcNow )
{
log . AddMessage ( string . Format ( "keeping delete request for {0} until {1}" , i . Name , i . deleteGracePeriod . ToLocalTime ()));
}
else
{
log . AddMessage ( string . Format ( "removing file listed as {0}: {1}" , i . State , i . Name ));
database . RemoveRemoteVolume ( i . Name , null );
}
2013-05-08 21:29:59 +02:00
}
2014-08-19 20:20:01 +02:00
break ;
case RemoteVolumeState . Uploading :
if ( remoteFound && correctSize && r . File . Size >= 0 )
{
log . AddMessage ( string . Format ( "promoting uploaded complete file from {0} to {2}: {1}" , i . State , i . Name , RemoteVolumeState . Uploaded ));
database . UpdateRemoteVolume ( i . Name , RemoteVolumeState . Uploaded , i . Size , i . Hash );
}
2014-12-30 18:26:54 +01:00
else if (! remoteFound )
{
2015-01-24 23:27:42 +01:00
log . AddMessage ( string . Format ( "scheduling missing file for deletion, currently listed as {0}: {1}" , i . State , i . Name ));
2014-12-30 18:26:54 +01:00
database . RemoveRemoteVolume ( i . Name , null );
2015-04-05 14:33:13 +02:00
database . RegisterRemoteVolume ( i . Name , i . Type , RemoteVolumeState . Deleting , TimeSpan . FromHours ( 2 ), null );
database . UpdateRemoteVolume ( i . Name , RemoteVolumeState . Deleting , i . Size , i . Hash , null );
2014-12-30 18:26:54 +01:00
}
2014-08-19 20:20:01 +02:00
else
{
log . AddMessage ( string . Format ( "removing incomplete remote file listed as {0}: {1}" , i . State , i . Name ));
backend . Delete ( i . Name , i . Size , true );
}
break ;
2014-08-26 12:51:09 +02:00
case RemoteVolumeState . Uploaded :
2014-08-19 20:20:01 +02:00
if (! remoteFound )
missing . Add ( i );
else if ( correctSize )
database . UpdateRemoteVolume ( i . Name , RemoteVolumeState . Verified , i . Size , i . Hash );
2013-05-08 21:29:59 +02:00
else
2014-08-26 12:51:09 +02:00
missingHash . Add ( new Tuple < long , RemoteVolumeEntry >( r . File . Size , i ));
2014-08-19 20:20:01 +02:00
break ;
case RemoteVolumeState . Verified :
if (! remoteFound )
2013-05-08 21:29:59 +02:00
missing . Add ( i );
2014-08-19 20:20:01 +02:00
else if (! correctSize )
2014-08-26 12:51:09 +02:00
missingHash . Add ( new Tuple < long , RemoteVolumeEntry >( r . File . Size , i ));
2013-05-08 21:29:59 +02:00
2014-08-19 20:20:01 +02:00
break ;
default :
log . AddWarning ( string . Format ( "unknown state for remote file listed as {0}: {1}" , i . State , i . Name ), null );
break ;
2013-05-08 21:29:59 +02:00
}
2014-08-19 20:20:01 +02:00
2014-12-30 16:15:21 +01:00
backend . FlushDbMessages ();
2013-05-08 21:29:59 +02:00
}
2014-08-26 12:51:09 +02:00
2014-12-30 16:15:21 +01:00
2014-08-26 12:51:09 +02:00
foreach ( var i in missingHash )
log . AddWarning ( string . Format ( "remote file {1} is listed as {0} with size {2} but should be {3}, please verify the sha256 hash \"{4}\"" , i . Item2 . State , i . Item2 . Name , i . Item1 , i . Item2 . Size , i . Item2 . Hash ), null );
2013-05-08 21:29:59 +02:00
2015-01-07 09:38:42 +01:00
return new RemoteAnalysisResult () {
ParsedVolumes = remotelist ,
OtherVolumes = otherlist ,
ExtraVolumes = lookup . Values ,
MissingVolumes = missing ,
VerificationRequiredVolumes = missingHash . Select ( x => x . Item2 )
};
2013-05-08 21:29:59 +02:00
}
}
}