2016-10-18 13:27:36 +02:00
// Copyright (C) 2016, The Duplicati Team
// http://www.duplicati.com, info@duplicati.com
//
// 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 CoCoL ;
using Duplicati.Library.Main.Operation.Common ;
using System.Threading.Tasks ;
using System.Collections.Generic ;
using Duplicati.Library.Utility ;
using System.Linq ;
using Duplicati.Library.Interface ;
using System.IO ;
namespace Duplicati.Library.Main.Operation.Backup
{
internal static class StreamBlockSplitter
{
2018-04-11 23:02:47 +02:00
/// <summary>
/// The tag used for log messages
/// </summary>
private static readonly string LOGTAG = Logging . Log . LogTagFromType ( typeof ( StreamBlockSplitter )) ;
private static readonly string FILELOGTAG = LOGTAG + ".FileEntry" ;
2016-10-18 13:27:36 +02:00
public static Task Run ( Options options , BackupDatabase database , ITaskReader taskreader )
{
return AutomationExtensions . RunTask (
new
{
Input = Channels . StreamBlock . ForRead ,
ProgressChannel = Channels . ProgressEvents . ForWrite ,
BlockOutput = Channels . OutputBlocks . ForWrite
},
async self =>
{
var blocksize = options . Blocksize ;
2017-09-20 13:56:37 +02:00
var filehasher = Duplicati . Library . Utility . HashAlgorithmHelper . Create ( options . FileHashAlgorithm );
var blockhasher = Duplicati . Library . Utility . HashAlgorithmHelper . Create ( options . BlockHashAlgorithm );
2016-10-18 13:27:36 +02:00
var emptymetadata = Utility . WrapMetadata ( new Dictionary < string , string >(), options );
2017-05-30 00:35:31 +02:00
var maxmetadatasize = ( options . Blocksize / ( long ) options . BlockhashSize ) * options . Blocksize ;
if ( blockhasher == null )
2018-04-11 23:02:47 +02:00
throw new UserInformationException ( Strings . Common . InvalidHashAlgorithm ( options . BlockHashAlgorithm ), "BlockHashAlgorithmNotSupported" );
2017-05-30 00:35:31 +02:00
if ( filehasher == null )
2018-04-11 23:02:47 +02:00
throw new UserInformationException ( Strings . Common . InvalidHashAlgorithm ( options . FileHashAlgorithm ), "FileHashAlgorithmNotSupported" );
2017-05-30 00:35:31 +02:00
if (! blockhasher . CanReuseTransform )
2018-04-11 23:02:47 +02:00
throw new UserInformationException ( Strings . Common . InvalidCryptoSystem ( options . BlockHashAlgorithm ), "BlockHashAlgorithmNotSupported" );
2017-05-30 00:35:31 +02:00
if (! filehasher . CanReuseTransform )
2018-04-11 23:02:47 +02:00
throw new UserInformationException ( Strings . Common . InvalidCryptoSystem ( options . FileHashAlgorithm ), "FileHashAlgorithmNotSupported" );
2016-10-18 13:27:36 +02:00
using ( var empty_metadata_stream = new MemoryStream ( emptymetadata . Blob ))
2016-10-18 15:16:56 +02:00
while ( await taskreader . ProgressAsync )
{
var send_close = false ;
var filesize = 0L ;
var filename = string . Empty ;
var e = await self . Input . ReadAsync ();
var cur = e . Result ;
try
2016-10-18 13:27:36 +02:00
{
2016-10-18 15:16:56 +02:00
var stream = e . Stream ;
2016-10-18 13:27:36 +02:00
2016-10-18 15:16:56 +02:00
using ( var blocklisthashes = new Library . Utility . FileBackedStringList ())
using ( var hashcollector = new Library . Utility . FileBackedStringList ())
2016-10-18 13:27:36 +02:00
{
2016-10-18 15:16:56 +02:00
var blocklistbuffer = new byte [ blocksize ];
var blocklistoffset = 0L ;
2016-10-18 13:27:36 +02:00
2016-10-18 15:16:56 +02:00
long fslen = - 1 ;
try { fslen = stream . Length ; }
2018-04-11 23:02:47 +02:00
catch ( Exception ex ) { Logging . Log . WriteWarningMessage ( FILELOGTAG , "FileLengthFailure" , ex , "Failed to read file length for file {0}" , e . Path ); }
2016-10-18 13:27:36 +02:00
2016-10-18 15:16:56 +02:00
if ( e . IsMetadata && fslen > maxmetadatasize )
2016-10-18 13:27:36 +02:00
{
2016-10-18 15:16:56 +02:00
//TODO: To fix this, the "WriteFileset" method in BackupHandler needs to
// be updated such that it can select sets even when there are multiple
// blocklist hashes for the metadata.
// This could be done such that an extra query is made if the metadata
// spans multiple blocklist hashes, as it is not expected to be common
2018-04-11 23:02:47 +02:00
Logging . Log . WriteWarningMessage ( LOGTAG , "TooLargeMetadata" , null , "Metadata size is {0}, but the largest accepted size is {1}, recording empty metadata for {2}" , fslen , maxmetadatasize , e . Path );
2016-10-18 15:16:56 +02:00
empty_metadata_stream . Position = 0 ;
stream = empty_metadata_stream ;
fslen = stream . Length ;
}
2016-10-18 13:27:36 +02:00
2016-10-18 15:16:56 +02:00
await self . ProgressChannel . WriteAsync ( new ProgressEvent () { Filepath = e . Path , Length = fslen , Type = EventType . FileStarted });
send_close = true ;
2016-10-18 13:27:36 +02:00
2016-10-18 15:16:56 +02:00
filehasher . Initialize ();
var lastread = 0 ;
var buf = new byte [ blocksize ];
var lastupdate = DateTime . Now ;
2016-10-18 13:27:36 +02:00
2016-10-18 15:16:56 +02:00
// Core processing loop, read blocks of data and hash individually
2017-08-12 14:09:55 +01:00
while ((( lastread = await stream . ForceStreamReadAsync ( buf , blocksize )) != 0 ))
2016-10-18 15:16:56 +02:00
{
// Run file hashing concurrently to squeeze a little extra concurrency out of it
var pftask = Task . Run (() => filehasher . TransformBlock ( buf , 0 , lastread , buf , 0 ));
2016-10-18 13:27:36 +02:00
2016-10-18 15:16:56 +02:00
var hashdata = blockhasher . ComputeHash ( buf , 0 , lastread );
var hashkey = Convert . ToBase64String ( hashdata );
// If we have too many hashes, flush the blocklist
if ( blocklistbuffer . Length - blocklistoffset < hashdata . Length )
2016-10-18 13:27:36 +02:00
{
var blkey = Convert . ToBase64String ( blockhasher . ComputeHash ( blocklistbuffer , 0 , ( int ) blocklistoffset ));
blocklisthashes . Add ( blkey );
await DataBlock . AddBlockToOutputAsync ( self . BlockOutput , blkey , blocklistbuffer , 0 , blocklistoffset , CompressionHint . Noncompressible , true );
2016-10-18 15:16:56 +02:00
blocklistoffset = 0 ;
blocklistbuffer = new byte [ blocksize ];
}
// Store the current hash in the blocklist
Array . Copy ( hashdata , 0 , blocklistbuffer , blocklistoffset , hashdata . Length );
blocklistoffset += hashdata . Length ;
hashcollector . Add ( hashkey );
filesize += lastread ;
// Don't spam updates
if (( DateTime . Now - lastupdate ). TotalSeconds > 10 )
{
await self . ProgressChannel . WriteAsync ( new ProgressEvent () { Filepath = e . Path , Length = filesize , Type = EventType . FileProgressUpdate });
lastupdate = DateTime . Now ;
2016-10-18 13:27:36 +02:00
}
2016-10-18 15:16:56 +02:00
// Make sure the filehasher is done with the buf instance before we pass it on
await pftask ;
await DataBlock . AddBlockToOutputAsync ( self . BlockOutput , hashkey , buf , 0 , lastread , e . Hint , true );
buf = new byte [ blocksize ];
2016-10-18 13:27:36 +02:00
}
2016-10-18 15:16:56 +02:00
// If we have more than a single block of data, output the (trailing) blocklist
if ( hashcollector . Count > 1 )
2016-10-18 13:27:36 +02:00
{
2016-10-18 15:16:56 +02:00
var blkey = Convert . ToBase64String ( blockhasher . ComputeHash ( blocklistbuffer , 0 , ( int ) blocklistoffset ));
blocklisthashes . Add ( blkey );
await DataBlock . AddBlockToOutputAsync ( self . BlockOutput , blkey , blocklistbuffer , 0 , blocklistoffset , CompressionHint . Noncompressible , true );
2016-10-18 13:27:36 +02:00
}
2016-10-18 15:16:56 +02:00
filehasher . TransformFinalBlock ( new byte [ 0 ], 0 , 0 );
var filehash = Convert . ToBase64String ( filehasher . Hash );
var blocksetid = await database . AddBlocksetAsync ( filehash , filesize , blocksize , hashcollector , blocklisthashes );
cur . SetResult ( new StreamProcessResult () { Streamlength = filesize , Streamhash = filehash , Blocksetid = blocksetid });
cur = null ;
2016-10-18 13:27:36 +02:00
}
2016-10-18 15:16:56 +02:00
}
catch ( Exception ex )
{
try
2016-10-18 13:27:36 +02:00
{
if ( cur != null )
2016-10-18 15:16:56 +02:00
cur . TrySetException ( ex );
}
catch { }
2016-10-18 13:27:36 +02:00
2016-10-18 15:16:56 +02:00
// Rethrow
if ( ex . IsRetiredException ())
throw ;
}
finally
{
if ( cur != null )
{
try { cur . TrySetCanceled (); }
catch { }
cur = null ;
2016-10-18 13:27:36 +02:00
}
2016-10-18 15:16:56 +02:00
if ( send_close )
await self . ProgressChannel . WriteAsync ( new ProgressEvent () { Filepath = e . Path , Length = filesize , Type = EventType . FileClosed });
send_close = false ;
2016-10-18 13:27:36 +02:00
}
2016-10-18 15:16:56 +02:00
}
2016-10-18 13:27:36 +02:00
});
}
}
}