2024-03-04 12:21:53 +01:00
// Copyright (C) 2024, The Duplicati Team
2024-02-28 15:45:30 +01:00
// 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.
using Duplicati.Library.Interface ;
2016-06-05 23:37:10 +02:00
using Duplicati.Library.Main ;
using Duplicati.Library.Main.Volumes ;
2016-07-14 00:05:35 +02:00
using Duplicati.Library.Utility ;
2016-08-21 12:53:44 +02:00
using Newtonsoft.Json.Linq ;
2019-02-22 21:58:40 -06:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
2024-02-28 15:45:30 +01:00
using System.Threading ;
2019-04-29 00:57:02 +02:00
using System.Security.Cryptography ;
2024-02-28 15:45:30 +01:00
2016-06-05 23:37:10 +02:00
namespace Duplicati.CommandLine.RecoveryTool
{
public static class Recompress
{
public static int Run ( List < string > args , Dictionary < string , string > options , Library . Utility . IFilter filter )
{
if ( args . Count != 4 )
{
Console . WriteLine ( "Invalid argument count ({0} expected 4): {1}{2}" , args . Count , Environment . NewLine , string . Join ( Environment . NewLine , args ));
return 100 ;
}
string target_compr_module = args [ 1 ];
if (! Library . DynamicLoader . CompressionLoader . Keys . Contains ( target_compr_module ))
{
Console . WriteLine ( "Target compression module not found: {0}{1}Modules supported: {2}" , args [ 1 ], Environment . NewLine , string . Join ( ", " , Library . DynamicLoader . CompressionLoader . Keys ));
return 100 ;
}
var m_Options = new Options ( options );
using ( var backend = Library . DynamicLoader . BackendLoader . GetBackend ( args [ 2 ], options ))
{
if ( backend == null )
{
Console . WriteLine ( "Backend not found: {0}{1}Backends supported: {2}" , args [ 2 ], Environment . NewLine , string . Join ( ", " , Library . DynamicLoader . BackendLoader . Keys ));
return 100 ;
}
var targetfolder = Path . GetFullPath ( args [ 3 ]);
if (! Directory . Exists ( args [ 3 ]))
{
Console . WriteLine ( "Creating target folder: {0}" , targetfolder );
Directory . CreateDirectory ( targetfolder );
}
Console . WriteLine ( "Listing files on backend: {0} ..." , backend . ProtocolKey );
2017-09-25 23:17:45 -06:00
var rawlist = backend . List (). ToList ();
2016-06-05 23:37:10 +02:00
2016-08-21 21:12:48 +02:00
Console . WriteLine ( "Found {0} files at remote storage" , rawlist . Count );
2016-06-05 23:37:10 +02:00
var i = 0 ;
var downloaded = 0 ;
var errors = 0 ;
var needspass = 0 ;
var remotefiles =
( from x in rawlist
let n = VolumeBase . ParseFilename ( x )
where n != null && n . Prefix == m_Options . Prefix
select n ). ToArray (); //ToArray() ensures that we do not remote-request it multiple times
if ( remotefiles . Length == 0 )
{
if ( rawlist . Count == 0 )
Console . WriteLine ( "No files were found at the remote location, perhaps the target url is incorrect?" );
else
{
var tmp =
( from x in rawlist
let n = VolumeBase . ParseFilename ( x )
where
n != null
select n . Prefix ). ToArray ();
var types = tmp . Distinct (). ToArray ();
if ( tmp . Length == 0 )
Console . WriteLine ( "Found {0} files at the remote storage, but none that could be parsed" , rawlist . Count );
else if ( types . Length == 1 )
2017-06-19 22:06:35 +02:00
Console . WriteLine ( "Found {0} parse-able files with the prefix {1}, did you forget to set the backup prefix?" , tmp . Length , types [ 0 ]);
2016-06-05 23:37:10 +02:00
else
2017-06-19 22:06:35 +02:00
Console . WriteLine ( "Found {0} parse-able files (of {1} files) with different prefixes: {2}, did you forget to set the backup prefix?" , tmp . Length , rawlist . Count , string . Join ( ", " , types ));
2016-06-05 23:37:10 +02:00
}
return 100 ;
}
2016-07-14 12:13:54 +02:00
bool reencrypt = Library . Utility . Utility . ParseBoolOption ( options , "reencrypt" );
bool reupload = Library . Utility . Utility . ParseBoolOption ( options , "reupload" );
2016-08-21 12:53:44 +02:00
// Needs order (Files or Blocks) and Indexes as last because indexes content will be adjusted based on recompressed blocks
var files = remotefiles . Where ( a => a . FileType == RemoteVolumeType . Files ). ToArray ();
var blocks = remotefiles . Where ( a => a . FileType == RemoteVolumeType . Blocks ). ToArray ();
var indexes = remotefiles . Where ( a => a . FileType == RemoteVolumeType . Index ). ToArray ();
remotefiles = files . Concat ( blocks ). ToArray (). Concat ( indexes ). ToArray ();
2016-08-21 21:12:48 +02:00
Console . WriteLine ( "Found {0} files which belongs to backup with prefix {1}" , remotefiles . Count (), m_Options . Prefix );
2016-08-21 12:53:44 +02:00
foreach ( var remoteFile in remotefiles )
2016-06-05 23:37:10 +02:00
{
try
{
2016-08-21 21:12:48 +02:00
Console . Write ( "{0}/{1}: {2}" , ++ i , remotefiles . Count (), remoteFile . File . Name );
2016-06-05 23:37:10 +02:00
2016-08-21 12:53:44 +02:00
var localFileSource = Path . Combine ( targetfolder , remoteFile . File . Name );
string localFileTarget ;
string localFileSourceEncryption = "" ;
if ( remoteFile . EncryptionModule != null )
2016-06-05 23:37:10 +02:00
{
if ( string . IsNullOrWhiteSpace ( m_Options . Passphrase ))
{
needspass ++;
Console . WriteLine ( " - No passphrase supplied, skipping" );
continue ;
}
2016-08-21 12:53:44 +02:00
using ( var m = Library . DynamicLoader . EncryptionLoader . GetModule ( remoteFile . EncryptionModule , m_Options . Passphrase , options ))
localFileSourceEncryption = m . FilenameExtension ;
2016-06-05 23:37:10 +02:00
2016-08-21 12:53:44 +02:00
localFileSource = localFileSource . Substring ( 0 , localFileSource . Length - localFileSourceEncryption . Length - 1 );
2016-06-05 23:37:10 +02:00
}
2016-08-21 12:53:44 +02:00
if ( remoteFile . CompressionModule != null )
localFileTarget = localFileSource . Substring ( 0 , localFileSource . Length - remoteFile . CompressionModule . Length - 1 ) + "." + target_compr_module ;
2016-06-05 23:37:10 +02:00
else
{
Console . WriteLine ( " - cannot detect compression type" );
continue ;
2017-12-25 04:12:19 +07:00
}
2016-08-21 12:53:44 +02:00
if ((! reencrypt && File . Exists ( localFileTarget )) || ( reencrypt && File . Exists ( localFileTarget + "." + localFileSourceEncryption )))
{
Console . WriteLine ( " - target file already exist" );
continue ;
2016-06-05 23:37:10 +02:00
}
2016-08-21 12:53:44 +02:00
if ( File . Exists ( localFileSource ))
File . Delete ( localFileSource );
2016-06-05 23:37:10 +02:00
2016-08-21 12:53:44 +02:00
Console . Write ( " - downloading ({0})..." , Library . Utility . Utility . FormatSizeString ( remoteFile . File . Size ));
2016-06-05 23:37:10 +02:00
2016-07-14 21:06:43 +02:00
DateTime originLastWriteTime ;
2016-07-14 16:03:29 +02:00
FileInfo destinationFileInfo ;
2016-08-21 12:53:44 +02:00
using ( var tf = new TempFile ())
2016-06-05 23:37:10 +02:00
{
2016-08-21 12:53:44 +02:00
backend . Get ( remoteFile . File . Name , tf );
2016-07-14 21:06:43 +02:00
originLastWriteTime = new FileInfo ( tf ). LastWriteTime ;
2016-06-05 23:37:10 +02:00
downloaded ++;
2016-08-21 12:53:44 +02:00
if ( remoteFile . EncryptionModule != null )
2016-06-05 23:37:10 +02:00
{
2016-07-14 00:05:35 +02:00
Console . Write ( " decrypting ..." );
2016-08-21 12:53:44 +02:00
using ( var m = Library . DynamicLoader . EncryptionLoader . GetModule ( remoteFile . EncryptionModule , m_Options . Passphrase , options ))
using ( var tf2 = new TempFile ())
2016-06-05 23:37:10 +02:00
{
m . Decrypt ( tf , tf2 );
2016-08-21 12:53:44 +02:00
File . Copy ( tf2 , localFileSource );
2016-06-05 23:37:10 +02:00
File . Delete ( tf2 );
}
}
else
2016-08-21 12:53:44 +02:00
File . Copy ( tf , localFileSource );
2016-06-05 23:37:10 +02:00
File . Delete ( tf );
2016-08-21 12:53:44 +02:00
destinationFileInfo = new FileInfo ( localFileSource );
2016-07-14 21:06:43 +02:00
destinationFileInfo . LastWriteTime = originLastWriteTime ;
2016-06-05 23:37:10 +02:00
}
2016-08-21 12:53:44 +02:00
if ( remoteFile . CompressionModule != null )
2016-06-05 23:37:10 +02:00
{
2016-07-14 00:05:35 +02:00
Console . Write ( " recompressing ..." );
2016-06-05 23:37:10 +02:00
2016-08-21 12:53:44 +02:00
//Recompressing from eg. zip to zip
if ( localFileSource == localFileTarget )
{
File . Move ( localFileSource , localFileSource + ".same" );
localFileSource = localFileSource + ".same" ;
}
2017-12-25 04:12:19 +07:00
using ( var localFileSourceStream = new System . IO . FileStream ( localFileSource , FileMode . Open , FileAccess . Read , FileShare . Read ))
using ( var cmOld = Library . DynamicLoader . CompressionLoader . GetModule ( remoteFile . CompressionModule , localFileSourceStream , ArchiveMode . Read , options ))
using ( var localFileTargetStream = new FileStream ( localFileTarget , FileMode . Create , FileAccess . Write , FileShare . Delete ))
using ( var cmNew = Library . DynamicLoader . CompressionLoader . GetModule ( target_compr_module , localFileTargetStream , ArchiveMode . Write , options ))
2016-07-14 21:06:43 +02:00
foreach ( var cmfile in cmOld . ListFiles ( "" ))
2016-06-05 23:37:10 +02:00
{
2016-07-14 21:06:43 +02:00
string cmfileNew = cmfile ;
2016-08-21 12:53:44 +02:00
var cmFileVolume = VolumeBase . ParseFilename ( cmfileNew );
2017-12-25 04:12:19 +07:00
2016-08-21 12:53:44 +02:00
if ( remoteFile . FileType == RemoteVolumeType . Index && cmFileVolume != null && cmFileVolume . FileType == RemoteVolumeType . Blocks )
2016-07-14 21:06:43 +02:00
{
2016-08-21 12:53:44 +02:00
// Correct inner filename extension to target compression type
cmfileNew = cmfileNew . Replace ( "." + cmFileVolume . CompressionModule , "." + target_compr_module );
if (! reencrypt )
cmfileNew = cmfileNew . Replace ( "." + cmFileVolume . EncryptionModule , "" );
//Because compression changes blocks file sizes - needs to be updated
string textJSON ;
using ( var sourceStream = cmOld . OpenRead ( cmfile ))
using ( var sourceStreamReader = new StreamReader ( sourceStream ))
2016-07-14 21:06:43 +02:00
{
2016-08-21 12:53:44 +02:00
textJSON = sourceStreamReader . ReadToEnd ();
JToken token = JObject . Parse ( textJSON );
var fileInfoBlocks = new FileInfo ( Path . Combine ( targetfolder , cmfileNew . Replace ( "vol/" , "" )));
2021-04-04 11:17:13 -07:00
using ( var filehasher = HashFactory . CreateHasher ( m_Options . FileHashAlgorithm ))
2016-08-21 12:53:44 +02:00
using ( var fileStream = fileInfoBlocks . Open ( FileMode . Open ))
{
fileStream . Position = 0 ;
token [ "volumehash" ] = Convert . ToBase64String ( filehasher . ComputeHash ( fileStream ));
fileStream . Close ();
}
token [ "volumesize" ] = fileInfoBlocks . Length ;
textJSON = token . ToString ();
2016-07-14 21:06:43 +02:00
}
2017-12-25 04:12:19 +07:00
2016-08-21 12:53:44 +02:00
using ( var sourceStream = new MemoryStream ( System . Text . Encoding . UTF8 . GetBytes ( textJSON )))
using ( var cs = cmNew . CreateFile ( cmfileNew , Library . Interface . CompressionHint . Compressible , cmOld . GetLastWriteTime ( cmfile )))
Library . Utility . Utility . CopyStream ( sourceStream , cs );
2016-07-14 21:06:43 +02:00
}
2016-08-21 12:53:44 +02:00
else
{
using ( var sourceStream = cmOld . OpenRead ( cmfile ))
using ( var cs = cmNew . CreateFile ( cmfileNew , Library . Interface . CompressionHint . Compressible , cmOld . GetLastWriteTime ( cmfile )))
2016-06-05 23:37:10 +02:00
Library . Utility . Utility . CopyStream ( sourceStream , cs );
2016-08-21 12:53:44 +02:00
}
2016-06-05 23:37:10 +02:00
}
2017-12-25 04:12:19 +07:00
2016-08-21 12:53:44 +02:00
File . Delete ( localFileSource );
destinationFileInfo = new FileInfo ( localFileTarget );
2016-07-14 21:06:43 +02:00
destinationFileInfo . LastWriteTime = originLastWriteTime ;
2016-06-05 23:37:10 +02:00
}
2017-12-25 04:12:19 +07:00
2016-08-21 12:53:44 +02:00
if ( reencrypt && remoteFile . EncryptionModule != null )
2016-06-05 23:37:10 +02:00
{
2016-07-14 00:05:35 +02:00
Console . Write ( " reencrypting ..." );
2016-08-21 12:53:44 +02:00
using ( var m = Library . DynamicLoader . EncryptionLoader . GetModule ( remoteFile . EncryptionModule , m_Options . Passphrase , options ))
2016-06-05 23:37:10 +02:00
{
2016-08-21 12:53:44 +02:00
m . Encrypt ( localFileTarget , localFileTarget + "." + localFileSourceEncryption );
File . Delete ( localFileTarget );
localFileTarget = localFileTarget + "." + localFileSourceEncryption ;
2016-06-05 23:37:10 +02:00
}
2016-07-14 16:03:29 +02:00
2016-08-21 12:53:44 +02:00
destinationFileInfo = new FileInfo ( localFileTarget );
2016-07-14 21:06:43 +02:00
destinationFileInfo . LastWriteTime = originLastWriteTime ;
2016-06-05 23:37:10 +02:00
}
2017-12-25 04:12:19 +07:00
2016-07-14 00:05:35 +02:00
if ( reupload )
2016-06-05 23:37:10 +02:00
{
2016-08-21 12:53:44 +02:00
Console . Write ( " reuploading ..." );
2019-03-17 18:20:14 -05:00
backend . PutAsync (( new FileInfo ( localFileTarget )). Name , localFileTarget , CancellationToken . None ). Wait ();
2016-08-21 12:53:44 +02:00
backend . Delete ( remoteFile . File . Name );
File . Delete ( localFileTarget );
2016-06-05 23:37:10 +02:00
}
Console . WriteLine ( " done!" );
}
catch ( Exception ex )
{
2017-11-26 15:24:53 -08:00
Console . WriteLine ( " error: {0}" , ex );
2016-06-05 23:37:10 +02:00
errors ++;
}
}
2016-07-14 12:13:54 +02:00
if ( reupload )
{
var remoteverificationfileexist = rawlist . Any ( x => x . Name == ( m_Options . Prefix + "-verification.json" ));
if ( remoteverificationfileexist )
{
Console . WriteLine ( "Found verification file {0} - deleting" , m_Options . Prefix + "-verification.json" );
backend . Delete ( m_Options . Prefix + "-verification.json" );
}
}
2016-06-05 23:37:10 +02:00
if ( needspass > 0 && downloaded == 0 )
{
Console . WriteLine ( "No files downloaded, try adding --passphrase to decrypt files" );
return 100 ;
}
Console . WriteLine ( "Download complete, of {0} remote files, {1} were downloaded with {2} errors" , remotefiles . Count (), downloaded , errors );
if ( needspass > 0 )
2018-05-12 17:06:12 -07:00
Console . WriteLine ( "Additonally {0} remote files were skipped because of encryption, supply --passphrase to download those" , needspass );
2016-06-05 23:37:10 +02:00
if ( errors > 0 )
2016-07-14 00:05:35 +02:00
{
Console . WriteLine ( "There were errors during recompress of remote backend files!" );
2016-06-05 23:37:10 +02:00
return 200 ;
2016-07-14 00:05:35 +02:00
}
2016-06-05 23:37:10 +02:00
2016-07-14 00:05:35 +02:00
return 0 ;
2016-06-05 23:37:10 +02:00
}
}
}
}