2026-05-14 22:25:41 +02:00
// Copyright (C) 2026, 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
2024-02-28 15:45:30 +01:00
// DEALINGS IN THE SOFTWARE.
2015-08-21 21:59:12 +02:00
using System ;
using System.IO ;
using System.Linq ;
using System.Collections.Generic ;
2020-01-18 14:04:48 -08:00
using Duplicati.Library.Common.IO ;
2021-04-04 11:17:13 -07:00
using Duplicati.Library.Utility ;
2024-06-21 15:11:37 +02:00
using System.IO.Compression ;
using Duplicati.Library.Interface ;
2015-08-21 21:59:12 +02:00
namespace Duplicati.CommandLine.RecoveryTool
{
public static class Restore
{
2020-07-19 00:23:24 -07:00
private static readonly ISystemIO systemIO = SystemIO . IO_OS ;
2015-08-21 21:59:12 +02:00
public static int Run ( List < string > args , Dictionary < string , string > options , Library . Utility . IFilter filter )
{
if ( args . Count != 2 && args . Count != 3 )
{
Console . WriteLine ( "Invalid argument count ({0} expected 2 or 3): {1}{2}" , args . Count , Environment . NewLine , string . Join ( Environment . NewLine , args ));
return 100 ;
}
var folder = Path . GetFullPath ( args [ 1 ]);
if (! Directory . Exists ( folder ))
{
Console . WriteLine ( "Folder not found: {0}" , folder );
return 100 ;
}
Directory . SetCurrentDirectory ( folder );
2024-06-21 15:11:37 +02:00
options . TryGetValue ( "targetpath" , out var targetpath );
options . TryGetValue ( "indexfile" , out var ixfile );
2015-08-21 21:59:12 +02:00
if ( string . IsNullOrWhiteSpace ( ixfile ))
ixfile = "index.txt" ;
ixfile = Path . GetFullPath ( ixfile );
if (! File . Exists ( ixfile ))
{
Console . WriteLine ( "Index file not found, perhaps you need to run the index command?" );
return 100 ;
}
string filelist ;
if ( args . Count == 2 )
{
var time = List . ParseListFiles ( folder ). First ();
filelist = time . Value ;
Console . WriteLine ( "Using set 0 with timestamp {0}" , time . Key . ToLocalTime ());
}
else
{
2015-09-05 09:38:52 +02:00
filelist = List . SelectListFile ( args [ 2 ], folder );
2015-08-21 21:59:12 +02:00
}
2026-05-14 22:25:41 +02:00
var parsedoptions = new Library . Main . Options ( options );
Library . Main . Volumes . VolumeReaderBase . UpdateOptionsFromManifest ( Path . GetExtension ( filelist ). Trim ( '.' ), filelist , parsedoptions );
2015-08-21 21:59:12 +02:00
2026-05-14 22:25:41 +02:00
options = parsedoptions . RawOptions ;
2024-06-21 15:11:37 +02:00
options . TryGetValue ( "blocksize" , out var blocksize_str );
options . TryGetValue ( "block-hash-algorithm" , out var blockhash_str );
options . TryGetValue ( "block-hash-algorithm" , out var filehash_str );
2024-06-22 10:27:51 +02:00
var offset = 0L ;
if ( options . TryGetValue ( "offset" , out var offset_str ))
offset = long . Parse ( offset_str );
2015-08-21 21:59:12 +02:00
long blocksize = string . IsNullOrWhiteSpace ( blocksize_str ) ? 0 : Library . Utility . Sizeparser . ParseSize ( blocksize_str );
if ( blocksize <= 0 )
{
2018-05-26 16:13:22 -07:00
Console . WriteLine ( "Invalid blocksize: {0}, try setting --blocksize manually" , blocksize );
2015-08-21 21:59:12 +02:00
return 100 ;
}
2021-04-04 11:17:13 -07:00
if ( string . IsNullOrWhiteSpace ( blockhash_str ))
2018-03-12 14:07:11 +01:00
throw new Duplicati . Library . Interface . UserInformationException ( string . Format ( "Block hash algorithm not valid: {0}" , blockhash_str ), "BlockHashAlgorithmNotSupported" );
2021-04-04 11:17:13 -07:00
if ( string . IsNullOrWhiteSpace ( filehash_str ))
2018-03-12 14:07:11 +01:00
throw new Duplicati . Library . Interface . UserInformationException ( string . Format ( "File hash algorithm not valid: {0}" , filehash_str ), "FileHashAlgorithmNotSupported" );
2015-08-21 21:59:12 +02:00
2024-06-21 15:11:37 +02:00
var useIndexMap = ! Library . Utility . Utility . ParseBoolOption ( options , "reduce-memory-use" );
var disableFileVerify = Library . Utility . Utility . ParseBoolOption ( options , "disable-file-verify" );
2026-06-08 19:17:07 +02:00
var allowPathTraversal = Library . Utility . Utility . ParseBoolOption ( options , "allow-restore-outside-target-directory" );
2015-08-21 21:59:12 +02:00
2024-06-21 15:42:24 +02:00
var startedCount = 0L ;
var restoredCount = 0L ;
var errorCount = 0L ;
var fileErrors = 0L ;
var totalSize = 0L ;
2021-04-04 11:17:13 -07:00
using ( var blockhasher = HashFactory . CreateHasher ( blockhash_str ))
using ( var filehasher = HashFactory . CreateHasher ( filehash_str ))
2017-12-25 04:12:19 +07:00
using ( var mru = new CompressedFileMRUCache ( options ))
2015-08-21 21:59:12 +02:00
{
2021-04-04 11:17:13 -07:00
var hashesprblock = blocksize / ( blockhasher . HashSize / 8 );
2015-08-21 21:59:12 +02:00
Console . WriteLine ( "Building lookup table for file hashes" );
2023-07-11 22:32:35 +02:00
// Source OS can have different directory separator
string sourceDirsep = null ;
2024-06-21 15:11:37 +02:00
using ( HashLookupHelper lookup = new HashLookupHelper ( ixfile , mru , ( int ) blocksize , blockhasher . HashSize / 8 , useIndexMap ))
2020-01-18 14:04:48 -08:00
{
var filecount = 0L ;
2015-08-21 21:59:12 +02:00
2020-01-18 14:04:48 -08:00
if (! string . IsNullOrWhiteSpace ( targetpath ))
Console . WriteLine ( "Computing restore path" );
2017-12-25 04:12:19 +07:00
2023-07-11 22:32:35 +02:00
string largestprefix = GetLargestPrefix ( from f in List . EnumerateFilesInDList ( filelist , filter , options ) select f . Path , out sourceDirsep , out filecount );
2015-08-21 21:59:12 +02:00
2020-01-18 14:04:48 -08:00
Console . WriteLine ( "Restoring {0} files to {1}" , filecount , string . IsNullOrWhiteSpace ( targetpath ) ? "original position" : targetpath );
2015-08-21 21:59:12 +02:00
2020-01-18 14:04:48 -08:00
if (! string . IsNullOrEmpty ( largestprefix ))
Console . WriteLine ( "Removing common prefix {0} from files" , largestprefix );
2015-08-21 21:59:12 +02:00
2020-01-18 14:04:48 -08:00
var i = 0L ;
2024-06-21 15:11:37 +02:00
// Allocate buffers for block and blocklist hashes
var buffer = new byte [ blocksize ];
var blocklistbuffer = new byte [ blocksize ];
2020-01-18 14:04:48 -08:00
foreach ( var f in List . EnumerateFilesInDList ( filelist , filter , options ))
2015-08-21 21:59:12 +02:00
{
2024-06-22 10:27:51 +02:00
if ( i < offset )
{
Console . WriteLine ( $"Skipping {i}: {f.Path} ({Utility.FormatSizeString(f.Size)})" );
i ++;
continue ;
}
2024-06-21 15:42:24 +02:00
startedCount ++;
2020-01-18 14:04:48 -08:00
try
{
2023-07-11 22:32:35 +02:00
var targetfile = MapToRestorePath ( f . Path , largestprefix , targetpath , sourceDirsep );
2026-06-08 19:17:07 +02:00
if (! allowPathTraversal && ! string . IsNullOrWhiteSpace ( targetpath ) && ! Util . IsPathInsideTarget ( targetfile , targetpath ))
2026-06-12 16:37:08 +02:00
{
Console . WriteLine ( $"WARNING: Path traversal detected: {targetfile} resolves outside {targetpath}, skipping entry. Use --allow-restore-outside-target-directory if you are sure this is safe." );
errorCount ++;
continue ;
}
2026-06-08 19:17:07 +02:00
2020-07-19 00:23:24 -07:00
if (! systemIO . DirectoryExists ( systemIO . PathGetDirectoryName ( targetfile )))
systemIO . DirectoryCreate ( systemIO . PathGetDirectoryName ( targetfile ));
2017-12-25 04:12:19 +07:00
2024-06-21 15:42:24 +02:00
Console . Write ( $"{i} ({Utility.FormatSizeString(totalSize)}): {targetfile} ({Utility.FormatSizeString(f.Size)})" );
totalSize += f . Size ;
2015-08-21 21:59:12 +02:00
2020-01-18 14:04:48 -08:00
using ( var tf = new Library . Utility . TempFile ())
2015-08-21 21:59:12 +02:00
{
2020-01-18 14:04:48 -08:00
using ( var sw = File . OpenWrite ( tf ))
2015-08-21 21:59:12 +02:00
{
2020-01-18 14:04:48 -08:00
if ( f . BlocklistHashes == null )
2015-08-21 21:59:12 +02:00
{
2023-07-11 22:32:35 +02:00
if ( f . Size > 0 )
2024-06-21 15:11:37 +02:00
lookup . WriteHash ( sw , f . Hash , buffer );
2020-01-18 14:04:48 -08:00
}
else
{
var blhi = 0L ;
foreach ( var blh in f . BlocklistHashes )
2015-08-21 21:59:12 +02:00
{
2020-01-18 14:04:48 -08:00
Console . Write ( " {0}" , blhi );
var blockhashoffset = blhi * hashesprblock * blocksize ;
try
2015-08-21 21:59:12 +02:00
{
2020-01-18 14:04:48 -08:00
var bi = 0 ;
2024-06-21 15:11:37 +02:00
foreach ( var h in lookup . ReadBlocklistHashes ( blh , blocklistbuffer ))
2015-08-21 21:59:12 +02:00
{
2020-01-18 14:04:48 -08:00
try
{
sw . Position = blockhashoffset + ( bi * blocksize );
2024-06-21 15:11:37 +02:00
lookup . WriteHash ( sw , h , buffer );
2020-01-18 14:04:48 -08:00
}
catch ( Exception ex )
{
2024-06-21 15:42:24 +02:00
errorCount ++;
2020-01-18 14:04:48 -08:00
Console . WriteLine ( "Failed to read hash: {0}{1}{2}" , h , Environment . NewLine , ex );
}
bi ++;
2015-08-21 21:59:12 +02:00
}
}
2020-01-18 14:04:48 -08:00
catch ( Exception ex )
{
2024-06-21 15:42:24 +02:00
errorCount ++;
2020-01-18 14:04:48 -08:00
Console . WriteLine ( "Failed to read Blocklist hash: {0}{1}{2}" , blh , Environment . NewLine , ex );
}
2015-08-21 21:59:12 +02:00
2020-01-18 14:04:48 -08:00
blhi ++;
}
2015-08-21 21:59:12 +02:00
}
}
2017-12-25 04:12:19 +07:00
2024-06-21 15:42:24 +02:00
restoredCount ++;
2024-06-21 15:11:37 +02:00
if ( disableFileVerify )
2020-01-18 14:04:48 -08:00
{
2024-06-21 15:11:37 +02:00
Console . WriteLine ( " done! (not verified)" );
if ( systemIO . FileExists ( targetfile ))
systemIO . FileCopy ( tf , targetfile , true );
else
systemIO . FileMove ( tf , targetfile );
2020-01-18 14:04:48 -08:00
}
2015-08-21 21:59:12 +02:00
else
2020-01-18 14:04:48 -08:00
{
2024-06-21 15:11:37 +02:00
string fh ;
using ( var fs = File . OpenRead ( tf ))
fh = Convert . ToBase64String ( filehasher . ComputeHash ( fs ));
2015-08-21 21:59:12 +02:00
2024-06-21 15:11:37 +02:00
if ( fh == f . Hash )
{
Console . WriteLine ( " done!" );
if ( systemIO . FileExists ( targetfile ))
systemIO . FileCopy ( tf , targetfile , true );
else
systemIO . FileMove ( tf , targetfile );
}
else
{
2024-06-21 15:42:24 +02:00
fileErrors ++;
2024-06-21 15:11:37 +02:00
Console . Write ( " - Restored file hash mismatch" );
if ( systemIO . FileExists ( targetfile ))
Console . WriteLine ( " - not overwriting existing file: {0}" , targetfile );
else
Console . WriteLine ( " - restoring file in damaged condition" );
}
2020-01-18 14:04:48 -08:00
}
2015-08-21 21:59:12 +02:00
}
2020-01-18 14:04:48 -08:00
}
catch ( Exception ex )
{
2024-06-21 15:42:24 +02:00
errorCount ++;
2020-01-18 14:04:48 -08:00
Console . WriteLine ( " error: {0}" , ex );
}
i ++;
2015-08-21 21:59:12 +02:00
}
}
}
2024-06-21 15:42:24 +02:00
if ( fileErrors > 0 || restoredCount != startedCount )
Console . WriteLine ( $"Restored {restoredCount} files of {startedCount} attempted ({Library.Utility.Utility.FormatSizeString(totalSize)}); {fileErrors} files had verification errors" );
else
Console . WriteLine ( $"Restored {restoredCount} files ({Library.Utility.Utility.FormatSizeString(totalSize)})" );
if ( errorCount > 0 )
Console . WriteLine ( "Errors: {0}" , errorCount );
return errorCount != 0 || restoredCount != startedCount || fileErrors > 0
? 1
: 0 ;
2015-08-21 21:59:12 +02:00
}
2023-07-11 22:32:35 +02:00
public static string GetLargestPrefix ( IEnumerable < string > filePaths , out string sourceDirsep , out long filecount )
{
// Get dir separator like in LocalRestoreDatabase.GetLargestPrefix():
string largestprefix = filePaths . OrderByDescending ( p => p . Length ). FirstOrDefault () ?? string . Empty ;
sourceDirsep = Util . GuessDirSeparator ( largestprefix );
string [] dirsepSplit = new string [] { sourceDirsep };
string [] largestprefixparts = largestprefix . Split ( dirsepSplit , StringSplitOptions . None );
// Because only files are in the list, need to remove filename from prefix
// Otherwise, in case of a single file the prefix is not a directory
if ( largestprefixparts . Length > 0 )
Array . Resize ( ref largestprefixparts , largestprefixparts . Length - 1 );
largestprefix = string . Join ( sourceDirsep , largestprefixparts );
filecount = 0 ;
foreach ( var path in filePaths )
{
if ( largestprefix . Length > 1 )
{
// Unix paths starting with / have an empty string in paths[0]
// Completely empty strings should not combine with that, so should have no parts at all
var parts = path . Length == 0 ? new string [ 0 ] : path . Split ( dirsepSplit , StringSplitOptions . None );
var ni = 0 ;
for (; ni < Math . Min ( parts . Length , largestprefixparts . Length ); ni ++)
if (! Library . Utility . Utility . ClientFilenameStringComparer . Equals ( parts [ ni ], largestprefixparts [ ni ]))
break ;
if ( ni != largestprefixparts . Length )
{
if ( ni == 0 )
{
largestprefixparts = new string [ 0 ];
largestprefix = string . Empty ;
}
else
{
// Only the first ni parts match
Array . Resize ( ref largestprefixparts , ni );
largestprefix = string . Join ( sourceDirsep , largestprefixparts );
}
}
}
filecount ++;
}
return largestprefixparts . Length == 0 ? "" : Util . AppendDirSeparator ( largestprefix , sourceDirsep );
}
public static string MapToRestorePath ( string path , string prefixpath , string restorepath , string sourceDirsep )
2015-08-21 21:59:12 +02:00
{
2023-07-11 22:32:35 +02:00
if ( sourceDirsep != null && sourceDirsep != Util . DirectorySeparatorString && sourceDirsep != Util . AltDirectorySeparatorString )
{
// Replace directory separator in source and prefix path
path = path . Replace ( sourceDirsep , Util . DirectorySeparatorString );
if (! string . IsNullOrWhiteSpace ( prefixpath ))
{
prefixpath = prefixpath . Replace ( sourceDirsep , Util . DirectorySeparatorString );
}
}
2016-05-29 00:56:25 +02:00
if ( string . IsNullOrWhiteSpace ( restorepath ))
2015-08-21 21:59:12 +02:00
return path ;
2016-05-29 00:56:25 +02:00
if ( string . IsNullOrWhiteSpace ( prefixpath ))
{
//Special case, restoring to new folder, but files are from different drives
// So we use the format <restore path> / <drive letter> / <source path>
// To avoid generating paths with a colon
if ( path . Substring ( 1 , 1 ) == ":" )
prefixpath = path . Substring ( 0 , 1 ) + path . Substring ( 2 );
2020-07-19 00:23:24 -07:00
return systemIO . PathCombine ( restorepath , prefixpath );
2016-05-29 00:56:25 +02:00
}
2020-07-19 00:23:24 -07:00
return systemIO . PathCombine ( restorepath , path . Substring ( prefixpath . Length ));
2015-08-21 21:59:12 +02:00
}
private class HashLookupHelper : IDisposable
{
private const int LOOKUP_TABLE_SIZE = 2048 ;
2018-05-23 21:18:01 -07:00
private readonly CompressedFileMRUCache m_cache ;
private readonly int m_hashsize ;
private readonly int m_blocksize ;
2015-08-21 21:59:12 +02:00
private Stream m_indexfile ;
2018-05-23 21:18:01 -07:00
private readonly List < string > m_lookup = new List < string >();
private readonly List < long > m_offsets = new List < long >();
2024-06-21 15:11:37 +02:00
private readonly Dictionary < string , string > m_indexMap ;
2018-05-23 21:18:01 -07:00
private readonly byte [] m_linebuf = new byte [ 128 ];
private readonly byte [] m_newline = Environment . NewLine . Select ( x => ( byte ) x ). ToArray ();
2015-08-21 21:59:12 +02:00
2024-06-21 15:11:37 +02:00
public HashLookupHelper ( string indexfile , CompressedFileMRUCache cache , int blocksize , int hashsize , bool useIndexMap )
2015-08-21 21:59:12 +02:00
{
m_cache = cache ;
m_blocksize = blocksize ;
m_hashsize = hashsize ;
m_indexfile = File . OpenRead ( indexfile );
2024-06-21 15:11:37 +02:00
if ( useIndexMap )
{
m_indexMap = new Dictionary < string , string >();
Console . WriteLine ( "Building in-memory index table ... " );
m_indexfile . Position = 0 ;
foreach ( var n in AllFileLines ())
m_indexMap [ n . Key ] = n . Value ;
Console . WriteLine ( "Index table has {0} unique hashes" , m_indexMap . Count );
}
else
2015-08-21 21:59:12 +02:00
{
2024-06-21 15:11:37 +02:00
// Build index ....
var hashes = 0L ;
string prev = null ;
m_indexfile . Position = 0 ;
foreach ( var n in AllFileLines ())
2015-08-21 21:59:12 +02:00
{
2024-06-21 15:11:37 +02:00
if ( n . Key != prev )
{
hashes ++;
prev = n . Key ;
}
2015-08-21 21:59:12 +02:00
}
2024-06-21 15:11:37 +02:00
Console . WriteLine ( "Index file has {0} hashes in total" , hashes );
2015-08-21 21:59:12 +02:00
2024-06-21 15:11:37 +02:00
var lookuptablesize = Math . Max ( 1 , Math . Min ( LOOKUP_TABLE_SIZE , hashes ) - 1 );
var lookupincrements = hashes / lookuptablesize ;
2015-08-21 21:59:12 +02:00
2024-06-21 15:11:37 +02:00
Console . WriteLine ( "Building lookup table with {0} entries, giving increments of {1}" , lookuptablesize , lookupincrements );
2015-08-21 21:59:12 +02:00
2024-06-21 15:11:37 +02:00
m_indexfile . Position = 0 ;
prev = null ;
var prevoff = 0L ;
var hc = 0L ;
foreach ( var n in AllFileLines ())
2015-08-21 21:59:12 +02:00
{
2024-06-21 15:11:37 +02:00
if ( n . Key != prev )
2015-08-21 21:59:12 +02:00
{
2024-06-21 15:11:37 +02:00
if (( hc % lookupincrements ) == 0 )
{
m_lookup . Add ( n . Key );
m_offsets . Add ( prevoff );
}
2015-08-21 21:59:12 +02:00
2024-06-21 15:11:37 +02:00
hc ++;
prev = n . Key ;
prevoff = m_indexfile . Position ;
}
2015-08-21 21:59:12 +02:00
}
}
}
private string ReadNextLine ()
{
var p = m_indexfile . Position ;
var max = m_indexfile . Read ( m_linebuf , 0 , m_linebuf . Length );
if ( max == 0 )
return null ;
2017-12-25 04:12:19 +07:00
2015-08-21 21:59:12 +02:00
var lfi = 0 ;
2017-12-25 04:12:19 +07:00
for ( int i = 0 ; i < max ; i ++)
2015-08-21 21:59:12 +02:00
if ( m_linebuf [ i ] == m_newline [ lfi ])
{
if ( lfi == m_newline . Length - 1 )
{
m_indexfile . Position = p + i + 1 ;
return System . Text . Encoding . Default . GetString ( m_linebuf , 0 , i - m_newline . Length + 1 );
}
else
{
lfi ++;
}
}
else
{
lfi = 0 ;
}
2017-12-25 04:12:19 +07:00
throw new Exception ( string . Format ( "Unexpected long line starting at offset {0}, read {1} bytes without a newline" , p , m_linebuf . Length ));
2015-08-21 21:59:12 +02:00
}
private IEnumerable < KeyValuePair < string , string >> AllFileLines ()
{
2017-12-25 04:12:19 +07:00
while ( true )
2015-08-21 21:59:12 +02:00
{
var str = ReadNextLine ();
if ( str == null )
break ;
if ( str . Length == 0 )
continue ;
2017-11-26 10:53:14 -08:00
var ix = str . IndexOf ( ", " , StringComparison . Ordinal );
2015-08-21 21:59:12 +02:00
if ( ix < 0 )
Console . WriteLine ( "Failed to parse line starting at offset {0} in index file, string: {1}" , m_indexfile . Position - str . Length - m_newline . Length , str );
yield return new KeyValuePair < string , string >( str . Substring ( 0 , ix ), str . Substring ( ix + 2 ));
}
}
2024-06-21 15:11:37 +02:00
public IEnumerable < string > ReadBlocklistHashes ( string hash , byte [] buffer )
2015-08-21 21:59:12 +02:00
{
2024-06-21 15:11:37 +02:00
var bytes = ReadHash ( hash , buffer );
for ( var i = 0 ; i < bytes ; i += m_hashsize )
yield return Convert . ToBase64String ( buffer , i , m_hashsize );
2015-08-21 21:59:12 +02:00
}
2024-06-21 15:11:37 +02:00
public int ReadHash ( string hash , byte [] buffer )
2015-08-21 21:59:12 +02:00
{
2024-06-21 15:11:37 +02:00
// Use in-memory map if available
if ( m_indexMap != null )
2015-08-21 21:59:12 +02:00
{
2024-06-21 15:11:37 +02:00
if (! m_indexMap . TryGetValue ( hash , out var filename ))
throw new Exception ( string . Format ( "Unable to locate block with hash: {0}" , hash ));
using ( var fs = m_cache . ReadBlock ( filename , hash ))
return Duplicati . Library . Utility . Utility . ForceStreamRead ( fs , buffer , buffer . Length );
2015-08-21 21:59:12 +02:00
}
2024-06-21 15:11:37 +02:00
else
{
var ix = m_lookup . BinarySearch ( hash , StringComparer . Ordinal );
if ( ix < 0 )
{
ix = ~ ix ;
if ( ix != 0 )
ix --;
}
2015-08-21 21:59:12 +02:00
2024-06-21 15:11:37 +02:00
m_indexfile . Position = m_offsets [ ix ];
2015-08-21 21:59:12 +02:00
2024-06-21 15:11:37 +02:00
foreach ( var v in AllFileLines ())
{
if ( v . Key == hash )
using ( var fs = m_cache . ReadBlock ( v . Value , hash ))
return Duplicati . Library . Utility . Utility . ForceStreamRead ( fs , buffer , buffer . Length );
2015-08-21 21:59:12 +02:00
2017-12-25 04:12:19 +07:00
2024-06-21 15:11:37 +02:00
if ( StringComparer . Ordinal . Compare ( hash , v . Key ) < 0 )
break ;
}
2017-12-25 04:12:19 +07:00
2024-06-21 15:11:37 +02:00
throw new Exception ( string . Format ( "Unable to locate block with hash: {0}" , hash ));
2015-08-21 21:59:12 +02:00
}
}
2024-06-21 15:11:37 +02:00
public void WriteHash ( Stream sw , string hash , byte [] buffer )
2015-08-21 21:59:12 +02:00
{
2024-06-21 15:11:37 +02:00
var h = ReadHash ( hash , buffer );
sw . Write ( buffer , 0 , h );
2015-08-21 21:59:12 +02:00
}
public void Dispose ()
{
if ( m_indexfile != null )
m_indexfile . Dispose ();
m_indexfile = null ;
}
}
2024-06-21 15:11:37 +02:00
private class WrappedZipReader : IArchiveReader
{
private readonly ZipArchive m_zip ;
public WrappedZipReader ( Stream stream )
{
m_zip = new ZipArchive ( stream , ZipArchiveMode . Read , true );
}
public void Dispose ()
{
m_zip . Dispose ();
}
public bool FileExists ( string file ) => throw new NotImplementedException ();
public DateTime GetLastWriteTime ( string file ) => throw new NotImplementedException ();
public string [] ListFiles ( string prefix ) => throw new NotImplementedException ();
public IEnumerable < KeyValuePair < string , long >> ListFilesWithSize ( string prefix ) => throw new NotImplementedException ();
public Stream OpenRead ( string file ) => m_zip . GetEntry ( file ). Open ();
}
2015-08-21 21:59:12 +02:00
private class CompressedFileMRUCache : IDisposable
{
2024-06-21 15:11:37 +02:00
private Dictionary < string , Library . Interface . IArchiveReader > m_lookup = new Dictionary < string , Library . Interface . IArchiveReader >();
2017-12-25 04:12:19 +07:00
private Dictionary < string , Stream > m_streams = new Dictionary < string , Stream >();
2015-08-21 21:59:12 +02:00
private List < string > m_mru = new List < string >();
2018-05-23 21:18:01 -07:00
private readonly Dictionary < string , string > m_options ;
2024-06-21 15:11:37 +02:00
private readonly bool m_useWrappedZip ;
2015-08-21 21:59:12 +02:00
2024-06-25 09:51:29 +02:00
private readonly int MAX_OPEN_ARCHIVES = 200 ;
2015-08-21 21:59:12 +02:00
public CompressedFileMRUCache ( Dictionary < string , string > options )
{
m_options = options ;
2024-06-21 15:11:37 +02:00
m_useWrappedZip = ! Library . Utility . Utility . ParseBoolOption ( options , "disable-wrapped-zip" );
2024-06-25 09:51:29 +02:00
if ( options . TryGetValue ( "max-open-archives" , out var maxopenarchives ))
MAX_OPEN_ARCHIVES = int . Parse ( maxopenarchives );
2015-08-21 21:59:12 +02:00
}
public Stream ReadBlock ( string filename , string hash )
{
2024-06-21 15:11:37 +02:00
if (! m_lookup . TryGetValue ( filename , out var cf ) || ! m_streams . ContainsKey ( filename ))
2015-08-21 21:59:12 +02:00
{
2024-06-21 15:11:37 +02:00
var stream = new FileStream ( filename , FileMode . Open , FileAccess . Read , FileShare . Read | FileShare . Delete );
var ext = Path . GetExtension ( filename ). Trim ( '.' );
cf = m_useWrappedZip && ext . Equals ( "zip" , StringComparison . OrdinalIgnoreCase )
? new WrappedZipReader ( stream )
: Library . DynamicLoader . CompressionLoader . GetModule ( ext , stream , Library . Interface . ArchiveMode . Read , m_options );
2017-12-25 22:59:17 +07:00
if ( cf == null )
{
stream . Dispose ();
throw new Exception ( string . Format ( "Unable to decompress {0}, no such compression module {1}" , filename , Path . GetExtension ( filename ). Trim ( '.' )));
}
m_lookup [ filename ] = cf ;
2017-12-25 04:12:19 +07:00
m_streams [ filename ] = stream ;
2015-08-21 21:59:12 +02:00
}
2024-06-21 15:11:37 +02:00
if ( m_mru . Count == 0 || m_mru [ 0 ] != filename )
2015-08-21 21:59:12 +02:00
{
m_mru . Remove ( filename );
m_mru . Insert ( 0 , filename );
}
while ( m_mru . Count > MAX_OPEN_ARCHIVES )
{
var f = m_mru . Last ();
2024-06-21 15:11:37 +02:00
( m_lookup [ f ] as IDisposable )?. Dispose ();
2015-08-21 21:59:12 +02:00
m_lookup . Remove ( f );
2017-12-25 04:12:19 +07:00
m_streams [ f ]. Dispose ();
m_streams . Remove ( f );
2015-08-21 21:59:12 +02:00
m_mru . Remove ( f );
}
return cf . OpenRead ( Library . Utility . Utility . Base64PlainToBase64Url ( hash ));
}
#region IDisposable implementation
public void Dispose ()
{
2017-12-25 04:12:19 +07:00
foreach ( var v in m_lookup . Values )
2024-06-21 15:11:37 +02:00
try { ( v as IDisposable )?. Dispose (); }
2017-12-25 04:12:19 +07:00
catch { }
foreach ( var v in m_streams . Values )
2015-08-21 21:59:12 +02:00
try { v . Dispose (); }
catch { }
m_lookup = null ;
2017-12-25 04:12:19 +07:00
m_streams = null ;
2015-08-21 21:59:12 +02:00
m_mru = null ;
}
#endregion
}
}
}