2024-02-28 15:45:30 +01:00
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
2016-10-18 13:27:36 +02:00
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 ;
2018-09-09 14:42:40 +02:00
using System.Security.Cryptography ;
2016-10-18 13:27:36 +02:00
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 ;
var emptymetadata = Utility . WrapMetadata ( new Dictionary < string , string >(), options );
2020-11-21 13:58:16 -08:00
var maxmetadatasize = ( options . Blocksize / ( long ) options . BlockhashSize ) * options . Blocksize ;
2017-05-30 00:35:31 +02:00
2021-04-04 11:17:13 -07:00
using ( var filehasher = HashFactory . CreateHasher ( options . FileHashAlgorithm ))
using ( var blockhasher = HashFactory . CreateHasher ( options . BlockHashAlgorithm ))
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
{
2020-11-21 13:58:16 -08:00
while ( await taskreader . ProgressAsync )
2016-10-18 13:27:36 +02:00
{
2020-11-21 13:58:16 -08:00
var send_close = false ;
var filesize = 0L ;
2016-10-18 13:27:36 +02:00
2020-11-21 13:58:16 -08:00
var e = await self . Input . ReadAsync ();
var cur = e . Result ;
2016-10-18 13:27:36 +02:00
2020-11-21 13:58:16 -08:00
try
{
var stream = e . Stream ;
2016-10-18 13:27:36 +02:00
2020-11-21 13:58:16 -08:00
using ( var blocklisthashes = new Library . Utility . FileBackedStringList ())
using ( var hashcollector = new Library . Utility . FileBackedStringList ())
2016-10-18 13:27:36 +02:00
{
2020-11-21 13:58:16 -08:00
var blocklistbuffer = new byte [ blocksize ];
var blocklistoffset = 0L ;
2016-10-18 13:27:36 +02:00
2020-11-21 13:58:16 -08:00
long fslen = - 1 ;
try
{
fslen = stream . Length ;
}
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
2020-11-21 13:58:16 -08:00
if ( e . IsMetadata && fslen > maxmetadatasize )
{
//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
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 );
empty_metadata_stream . Position = 0 ;
stream = empty_metadata_stream ;
fslen = stream . Length ;
}
2016-10-18 13:27:36 +02:00
2020-11-21 13:58:16 -08:00
// Don't send progress reports for metadata
if (! e . IsMetadata )
{
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
2020-11-21 13:58:16 -08:00
filehasher . Initialize ();
var lastread = 0 ;
var buf = new byte [ blocksize ];
var lastupdate = DateTime . Now ;
2016-10-18 15:16:56 +02:00
2020-11-21 13:58:16 -08:00
// Core processing loop, read blocks of data and hash individually
while ((( lastread = await stream . ForceStreamReadAsync ( buf , blocksize )) != 0 ))
2016-10-18 13:27:36 +02:00
{
2020-11-21 13:58:16 -08: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 ));
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 )
{
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 );
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 ( send_close && ( DateTime . Now - lastupdate ). TotalSeconds > 5 )
{
await self . ProgressChannel . WriteAsync ( new ProgressEvent () { Filepath = e . Path , Length = filesize , Type = EventType . FileProgressUpdate });
lastupdate = DateTime . Now ;
}
// Make sure the filehasher is done with the buf instance before we pass it on
await pftask . ConfigureAwait ( false );
await DataBlock . AddBlockToOutputAsync ( self . BlockOutput , hashkey , buf , 0 , lastread , e . Hint , false );
buf = new byte [ blocksize ];
2016-10-18 15:16:56 +02:00
}
2020-11-21 13:58:16 -08:00
// If we have more than a single block of data, output the (trailing) blocklist
if ( hashcollector . Count > 1 )
2016-10-18 15:16:56 +02:00
{
2020-11-21 13:58:16 -08: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
}
2020-11-21 13:58:16 -08: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
}
2020-11-21 13:58:16 -08:00
}
catch ( Exception ex )
{
try
{
if ( cur != null )
cur . TrySetException ( ex );
}
catch
2016-10-18 13:27:36 +02:00
{
}
2016-10-18 15:16:56 +02:00
2020-11-21 13:58:16 -08:00
// Rethrow
if ( ex . IsRetiredException ())
throw ;
2016-10-18 13:27:36 +02:00
}
2020-11-21 13:58:16 -08:00
finally
2016-10-18 13:27:36 +02:00
{
if ( cur != null )
2020-11-21 13:58:16 -08:00
{
try
{
cur . TrySetCanceled ();
}
catch
{
}
2016-10-18 13:27:36 +02:00
2020-11-21 13:58:16 -08:00
cur = null ;
}
2016-10-18 15:16:56 +02:00
2020-11-21 13:58:16 -08: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
});
}
}
}