2025-01-17 09:45:49 -03:00
// Copyright (C) 2025, 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-03-15 14:18:56 +01:00
// DEALINGS IN THE SOFTWARE.
2024-02-28 15:45:30 +01:00
2013-02-12 21:43:14 +00:00
using System ;
using System.Collections.Generic ;
2025-01-17 09:45:49 -03:00
using System.Diagnostics ;
2013-02-12 21:43:14 +00:00
using System.Text ;
using Duplicati.Library.Interface ;
2015-07-01 09:39:26 +02:00
using System.Linq ;
2018-12-11 21:07:30 -08:00
using System.Globalization ;
2019-02-22 21:58:40 -06:00
using System.Threading ;
2024-09-29 22:00:57 +02:00
using Duplicati.Library.Utility ;
2024-12-19 09:31:06 +01:00
using Duplicati.StreamUtil ;
2025-02-17 16:45:51 +01:00
using System.Threading.Tasks ;
2013-02-12 21:43:14 +00:00
namespace Duplicati.CommandLine.BackendTester
{
2021-04-03 13:57:02 +02:00
public class Program
2025-02-17 16:45:51 +01:00
{
2013-02-12 21:43:14 +00:00
class TempFile
{
public readonly string remotefilename ;
public readonly string localfilename ;
public readonly byte [] hash ;
public readonly long length ;
public bool found = false ;
public TempFile ( string remotefilename , string localfilename , byte [] hash , long length )
{
this . remotefilename = remotefilename ;
this . localfilename = localfilename ;
this . hash = hash ;
this . length = length ;
}
}
private const string ValidFilenameChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789" ;
private const string ExtendedChars = "-_',=)(&%$#@! +" ;
2014-06-26 22:37:58 +02:00
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
2024-03-15 14:18:56 +01:00
public static int Main ( string [] _args )
2013-02-12 21:43:14 +00:00
{
try
{
2024-08-28 00:18:55 +02:00
Library . AutoUpdater . PreloadSettingsLoader . ConfigurePreloadSettings ( ref _args , Library . AutoUpdater . PackageHelper . NamedExecutable . BackendTester );
2015-07-01 09:39:26 +02:00
if ( _args . Length == 1 )
{
try
{
2018-09-22 19:51:30 -07:00
var p = Environment . ExpandEnvironmentVariables ( _args [ 0 ]);
2015-07-01 09:39:26 +02:00
if ( System . IO . File . Exists ( p ))
_args = ( from x in System . IO . File . ReadLines ( p )
2024-08-28 00:18:55 +02:00
where ! string . IsNullOrWhiteSpace ( x ) && ! x . Trim (). StartsWith ( "#" , StringComparison . Ordinal )
select x . Trim ()
2015-07-01 09:39:26 +02:00
). ToArray ();
}
catch
{
}
}
2013-02-12 21:43:14 +00:00
List < string > args = new List < string >( _args );
Dictionary < string , string > options = Library . Utility . CommandLineParser . ExtractOptions ( args );
2025-01-21 12:45:53 -03:00
if ( args . Count != 1 || HelpOptionExtensions . IsArgumentAnyHelpString ( args ))
2013-02-12 21:43:14 +00:00
{
Console . WriteLine ( "Usage: <protocol>://<username>:<password>@<path>" );
Console . WriteLine ( "Example: ftp://user:pass@server/folder" );
Console . WriteLine ();
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"Supported backends: {string.Join(" , ", Duplicati.Library.DynamicLoader.BackendLoader.Keys)}" );
2013-02-12 21:43:14 +00:00
Console . WriteLine ();
List < string > lines = new List < string >();
foreach ( Library . Interface . ICommandLineArgument arg in SupportedCommands )
Library . Interface . CommandLineArgument . PrintArgument ( lines , arg );
foreach ( string s in lines )
Console . WriteLine ( s );
2024-03-15 14:18:56 +01:00
return 0 ;
2013-02-12 21:43:14 +00:00
}
2018-05-23 19:37:49 +02:00
if ( options . ContainsKey ( "tempdir" ) && ! string . IsNullOrEmpty ( options [ "tempdir" ]))
Library . Utility . SystemContextSettings . DefaultTempPath = options [ "tempdir" ];
2024-08-28 00:18:55 +02:00
2024-12-20 16:10:08 +01:00
using var SystemSettings = Duplicati . Library . Utility . SystemContextSettings . StartSession ();
2018-09-29 13:20:16 -07:00
2018-05-23 19:37:49 +02:00
if (! options . ContainsKey ( "auth_password" ) && ! string . IsNullOrEmpty ( System . Environment . GetEnvironmentVariable ( "AUTH_PASSWORD" )))
options [ "auth_password" ] = System . Environment . GetEnvironmentVariable ( "AUTH_PASSWORD" );
if (! options . ContainsKey ( "auth_username" ) && ! string . IsNullOrEmpty ( System . Environment . GetEnvironmentVariable ( "AUTH_USERNAME" )))
options [ "auth_username" ] = System . Environment . GetEnvironmentVariable ( "AUTH_USERNAME" );
2013-02-12 21:43:14 +00:00
int reruns = 5 ;
if ( options . ContainsKey ( "reruns" ))
reruns = int . Parse ( options [ "reruns" ]);
for ( int i = 0 ; i < reruns ; i ++)
{
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{DateTime.Now.ToShortDateString()} {DateTime.Now.ToLongTimeString()}" );
Console . WriteLine ( $"{LogTimeStamp}Starting run no {i}" );
2013-02-12 21:43:14 +00:00
if (! Run ( args , options , i == 0 ))
2024-03-15 14:18:56 +01:00
return 1 ;
2013-02-12 21:43:14 +00:00
}
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Unittest complete!" );
2024-03-15 14:18:56 +01:00
return 0 ;
2013-02-12 21:43:14 +00:00
}
catch ( Exception ex )
{
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Unittest failed: {ex}" );
2013-02-12 21:43:14 +00:00
}
2024-03-15 14:18:56 +01:00
return 1 ;
2013-02-12 21:43:14 +00:00
}
static bool Run ( List < string > args , Dictionary < string , string > options , bool first )
{
2025-01-17 09:45:49 -03:00
bool failAfterFinished = false ;
2024-12-19 09:31:06 +01:00
var backend = Library . DynamicLoader . BackendLoader . GetBackend ( args [ 0 ], options );
2013-02-12 21:43:14 +00:00
if ( backend == null )
{
Console . WriteLine ( "Unsupported backend" );
Console . WriteLine ();
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"Supported backends: {string.Join(" , ", Duplicati.Library.DynamicLoader.BackendLoader.Keys)}" );
2013-02-12 21:43:14 +00:00
return false ;
}
2024-12-19 09:31:06 +01:00
var allowedChars = ValidFilenameChars ;
2020-01-25 17:07:02 -08:00
if ( options . ContainsKey ( "extended-chars" ))
{
allowedChars += String . IsNullOrEmpty ( options [ "extended-chars" ]) ? ExtendedChars : options [ "extended-chars" ];
}
2024-12-19 09:31:06 +01:00
var autoCreateFolders = Library . Utility . Utility . ParseBoolOption ( options , "auto-create-folder" );
2025-09-10 14:47:23 +02:00
var retries = 0 ;
if ( options . ContainsKey ( "failure-retries" ))
retries = int . Parse ( options [ "failure-retries" ]);
2018-06-29 19:46:33 +02:00
2024-12-19 09:31:06 +01:00
options . TryGetValue ( "enable-module" , out var enabledModulesValue );
options . TryGetValue ( "disable-module" , out var disabledModulesValue );
var enabledModules = enabledModulesValue == null ? new string [ 0 ] : enabledModulesValue . Trim (). ToLower ( CultureInfo . InvariantCulture ). Split ( ',' );
var disabledModules = disabledModulesValue == null ? new string [ 0 ] : disabledModulesValue . Trim (). ToLower ( CultureInfo . InvariantCulture ). Split ( ',' );
2013-02-12 21:43:14 +00:00
2024-12-19 09:31:06 +01:00
var loadedModules = new List < IGenericModule >();
foreach ( var m in Library . DynamicLoader . GenericLoader . Modules )
2018-09-19 14:29:40 -07:00
if (! disabledModules . Contains ( m . Key , StringComparer . OrdinalIgnoreCase ) && ( m . LoadAsDefault || enabledModules . Contains ( m . Key , StringComparer . OrdinalIgnoreCase )))
2013-02-12 21:43:14 +00:00
{
m . Configure ( options );
loadedModules . Add ( m );
}
try
{
2017-09-25 23:17:45 -06:00
IEnumerable < Library . Interface . IFileEntry > curlist = null ;
2013-02-12 21:43:14 +00:00
try
{
2025-09-10 14:47:23 +02:00
Retry (() => backend . TestAsync ( CancellationToken . None ), retries ). Await ();
curlist = Retry (() => backend . ListAsync ( CancellationToken . None ). ToBlockingEnumerable (). ToList (), retries );
2013-02-12 21:43:14 +00:00
}
2018-12-31 19:31:03 -08:00
catch ( FolderMissingException )
2013-02-12 21:43:14 +00:00
{
if ( autoCreateFolders )
{
try
{
2025-09-10 14:47:23 +02:00
Retry (() => backend . CreateFolderAsync ( CancellationToken . None ), retries ). Await ();
curlist = Retry (() => backend . ListAsync ( CancellationToken . None ). ToBlockingEnumerable (). ToList (), retries );
2013-02-12 21:43:14 +00:00
}
catch ( Exception ex )
{
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Autocreate folder failed with message: {ex.Message}" );
2013-02-12 21:43:14 +00:00
}
}
if ( curlist == null )
2018-12-31 14:52:11 -08:00
throw ;
2013-02-12 21:43:14 +00:00
}
foreach ( Library . Interface . IFileEntry fe in curlist )
if (! fe . IsFolder )
{
if ( Library . Utility . Utility . ParseBoolOption ( options , "auto-clean" ) && first )
if ( Library . Utility . Utility . ParseBoolOption ( options , "force" ))
{
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Auto clean, removing file: {fe.Name}" );
2025-09-10 14:47:23 +02:00
Retry (() => backend . DeleteAsync ( fe . Name , CancellationToken . None ), retries ). Await ();
2013-02-12 21:43:14 +00:00
continue ;
}
else
Console . WriteLine ( "Specify the --force flag to actually delete files" );
2024-12-20 16:10:08 +01:00
var fileCount = curlist . Where ( x => ! x . IsFolder ). Count ();
var filenames = curlist . Where ( x => ! x . IsFolder ). Select ( x => x . Name ). Take ( 10 ). ToList ();
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}*** Remote folder contains {fileCount} file(s), aborting" );
Console . WriteLine ( $"{LogTimeStamp}*** First {filenames.Count} file(s): {Environment.NewLine}{string.Join(Environment.NewLine, filenames)}" );
2024-12-20 16:10:08 +01:00
if ( fileCount > filenames . Count )
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}*** ... and {fileCount - filenames.Count} more file(s)" );
2013-02-12 21:43:14 +00:00
return false ;
}
2024-12-19 09:31:06 +01:00
var number_of_files = 10 ;
var min_file_size = 1024 ;
var max_file_size = 1024 * 1024 * 50 ;
var min_filename_size = 5 ;
var max_filename_size = 80 ;
var disableStreaming = Library . Utility . Utility . ParseBoolOption ( options , "disable-streaming-transfers" );
var skipOverwriteTest = Library . Utility . Utility . ParseBoolOption ( options , "skip-overwrite-test" );
var trimFilenameSpaces = Library . Utility . Utility . ParseBoolOption ( options , "trim-filename-spaces" );
2024-12-20 16:10:08 +01:00
var waitAfterUpload = TimeSpan . Zero ;
var waitAfterDelete = TimeSpan . Zero ;
2025-11-03 12:48:37 +01:00
var useStreaming = backend is IStreamingBackend streamingBackend && streamingBackend . SupportsStreaming && ! disableStreaming ;
2013-02-12 21:43:14 +00:00
2024-12-19 09:31:06 +01:00
var throttleUpload = 0L ;
if ( options . TryGetValue ( "throttle-upload" , out var throttleUploadString ))
2021-03-22 09:57:56 -06:00
{
2025-11-03 12:48:37 +01:00
if (! useStreaming )
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Warning: Throttling is only supported in this tool on streaming backends" );
2021-03-22 09:57:56 -06:00
2021-03-22 10:47:44 -06:00
throttleUpload = Duplicati . Library . Utility . Sizeparser . ParseSize ( throttleUploadString , "kb" );
2021-03-22 09:57:56 -06:00
}
2024-12-19 09:31:06 +01:00
var throttleDownload = 0L ;
if ( options . TryGetValue ( "throttle-download" , out var throttleDownloadString ))
2021-03-22 09:57:56 -06:00
{
2025-11-03 12:48:37 +01:00
if (! useStreaming )
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Warning: Throttling is only supported in this tool on streaming backends" );
2021-03-22 09:57:56 -06:00
2021-03-22 10:47:44 -06:00
throttleDownload = Duplicati . Library . Utility . Sizeparser . ParseSize ( throttleDownloadString , "kb" );
2021-03-22 09:57:56 -06:00
}
2025-03-28 16:58:54 +01:00
int readWriteTimeout = ( Environment . GetEnvironmentVariable ( "READ_WRITE_TIMEOUT_SECONDS" ) is { } timeout
2025-02-17 16:45:51 +01:00
&& int . TryParse ( timeout , out var seconds )
2025-01-28 10:35:32 -03:00
&& seconds == - 1 )
? Timeout . Infinite
2025-02-17 16:45:51 +01:00
: Environment . GetEnvironmentVariable ( "READ_WRITE_TIMEOUT_SECONDS" ) is { } timeoutRetry
2025-01-28 10:35:32 -03:00
&& int . TryParse ( timeoutRetry , out var secondsRetry )
? ( int ) TimeSpan . FromSeconds ( secondsRetry ). TotalMilliseconds
2025-01-29 20:37:40 -03:00
: ( int ) TimeSpan . FromSeconds ( 60 ). TotalMilliseconds ;
2025-01-28 10:35:32 -03:00
2024-12-19 09:31:06 +01:00
// Allow overriding the timeout for the backend here, even if timeouts are disabled
if ( options . TryGetValue ( "read-write-timeout" , out var readWriteTimeoutString ))
readWriteTimeout = ( int ) Timeparser . ParseTimeSpan ( readWriteTimeoutString ). TotalMilliseconds ;
if ( readWriteTimeout <= 0 )
readWriteTimeout = Timeout . Infinite ;
2025-07-29 08:48:31 -03:00
Console . WriteLine ( $"{LogTimeStamp}Read Write Timeout set to {(readWriteTimeout == Timeout.Infinite ? " infinite " : readWriteTimeout + " ms ")}" );
2013-02-12 21:43:14 +00:00
if ( options . ContainsKey ( "number-of-files" ))
number_of_files = int . Parse ( options [ "number-of-files" ]);
if ( options . ContainsKey ( "min-file-size" ))
min_file_size = ( int ) Duplicati . Library . Utility . Sizeparser . ParseSize ( options [ "min-file-size" ], "mb" );
if ( options . ContainsKey ( "max-file-size" ))
2013-08-17 13:30:55 +02:00
max_file_size = ( int ) Duplicati . Library . Utility . Sizeparser . ParseSize ( options [ "max-file-size" ], "mb" );
2013-02-12 21:43:14 +00:00
if ( options . ContainsKey ( "min-filename-length" ))
min_filename_size = int . Parse ( options [ "min-filename-length" ]);
if ( options . ContainsKey ( "max-filename-length" ))
max_filename_size = int . Parse ( options [ "max-filename-length" ]);
2024-12-20 16:10:08 +01:00
if ( options . ContainsKey ( "wait-after-upload" ))
waitAfterUpload = Timeparser . ParseTimeSpan ( options [ "wait-after-upload" ]);
if ( options . ContainsKey ( "wait-after-delete" ))
waitAfterDelete = Timeparser . ParseTimeSpan ( options [ "wait-after-delete" ]);
2024-12-19 09:31:06 +01:00
var rnd = new Random ();
var sha = System . Security . Cryptography . SHA256 . Create ();
2013-02-12 21:43:14 +00:00
//Create random files
2024-12-19 09:31:06 +01:00
using ( var tf = new Duplicati . Library . Utility . TempFolder ())
2013-02-12 21:43:14 +00:00
{
2024-12-19 09:31:06 +01:00
var files = new List < TempFile >();
2013-02-12 21:43:14 +00:00
for ( int i = 0 ; i < number_of_files ; i ++)
{
2024-12-19 09:31:06 +01:00
var filename = CreateRandomRemoteFileName ( min_filename_size , max_filename_size , allowedChars , trimFilenameSpaces , rnd );
2013-02-12 21:43:14 +00:00
2024-12-19 09:31:06 +01:00
var localfilename = CreateRandomFile ( tf , i , min_file_size , max_file_size , rnd );
2013-02-12 21:43:14 +00:00
//Calculate local hash and length
2024-12-19 09:31:06 +01:00
using ( var fs = new System . IO . FileStream ( localfilename , System . IO . FileMode . Open , System . IO . FileAccess . Read ))
2018-03-20 13:48:21 -06:00
files . Add ( new TempFile ( filename , localfilename , sha . ComputeHash ( fs ), fs . Length ));
2013-02-12 21:43:14 +00:00
}
byte [] dummyFileHash = null ;
if (! skipOverwriteTest )
{
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Uploading wrong files ..." );
2024-12-19 09:31:06 +01:00
using ( var dummy = Library . Utility . TempFile . WrapExistingFile ( CreateRandomFile ( tf , files . Count , 1024 , 2048 , rnd )))
2013-02-12 21:43:14 +00:00
{
2024-12-19 09:31:06 +01:00
using ( var fs = new System . IO . FileStream ( dummy , System . IO . FileMode . Open , System . IO . FileAccess . Read ))
2013-02-12 21:43:14 +00:00
dummyFileHash = sha . ComputeHash ( fs );
//Upload a dummy file for entry 0 and the last one, they will be replaced by the real files afterwards
//We upload entry 0 twice just to try to freak any internal cache list
2025-09-10 14:47:23 +02:00
Retry (() => Uploadfile ( dummy , 0 , files [ 0 ]. remotefilename , backend , disableStreaming , throttleUpload , readWriteTimeout ), retries );
Retry (() => Uploadfile ( dummy , 0 , files [ 0 ]. remotefilename , backend , disableStreaming , throttleUpload , readWriteTimeout ), retries );
Retry (() => Uploadfile ( dummy , files . Count - 1 , files [ files . Count - 1 ]. remotefilename , backend , disableStreaming , throttleUpload , readWriteTimeout ), retries );
2013-02-12 21:43:14 +00:00
}
}
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Uploading files ..." );
2013-02-12 21:43:14 +00:00
for ( int i = 0 ; i < files . Count ; i ++)
2025-09-10 14:47:23 +02:00
Retry (() => Uploadfile ( files [ i ]. localfilename , i , files [ i ]. remotefilename , backend , disableStreaming , throttleUpload , readWriteTimeout ), retries );
2013-02-12 21:43:14 +00:00
2018-03-20 13:50:06 -06:00
TempFile originalRenamedFile = null ;
string renamedFileNewName = null ;
2024-09-29 23:21:50 +02:00
if ( backend is IRenameEnabledBackend renameEnabledBackend )
2018-03-20 13:50:06 -06:00
{
// Rename the second file in the list, if there are more than one. If not, just do the first one.
int renameIndex = files . Count > 1 ? 1 : 0 ;
originalRenamedFile = files [ renameIndex ];
renamedFileNewName = CreateRandomRemoteFileName ( min_filename_size , max_filename_size , allowedChars , trimFilenameSpaces , rnd );
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Renaming file {renameIndex} from {originalRenamedFile.remotefilename} to {renamedFileNewName}" );
2018-03-20 13:50:06 -06:00
2025-09-10 14:47:23 +02:00
Retry (() => renameEnabledBackend . RenameAsync ( originalRenamedFile . remotefilename , renamedFileNewName , CancellationToken . None ), retries ). Await ();
2018-03-20 13:50:06 -06:00
files [ renameIndex ] = new TempFile ( renamedFileNewName , originalRenamedFile . localfilename , originalRenamedFile . hash , originalRenamedFile . length );
}
2024-12-20 16:10:08 +01:00
if ( waitAfterUpload > TimeSpan . Zero )
{
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Waiting {waitAfterUpload} after upload" );
2024-12-20 16:10:08 +01:00
Thread . Sleep ( waitAfterUpload );
}
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Verifying file list ..." );
2013-02-12 21:43:14 +00:00
2025-09-10 14:47:23 +02:00
curlist = Retry (() => backend . ListAsync ( CancellationToken . None ). ToBlockingEnumerable (). ToList (), retries );
2024-12-19 09:31:06 +01:00
foreach ( var fe in curlist )
2013-02-12 21:43:14 +00:00
if (! fe . IsFolder )
{
bool found = false ;
2025-03-31 11:27:23 +02:00
for ( var i = 0 ; i < files . Count ; i ++)
{
var tx = files [ i ];
2013-02-12 21:43:14 +00:00
if ( tx . remotefilename == fe . Name )
{
if ( tx . found )
2025-03-31 11:27:23 +02:00
Console . WriteLine ( $"{LogTimeStamp}*** File {i}) with name {tx.remotefilename} was found more than once" );
2013-02-12 21:43:14 +00:00
found = true ;
tx . found = true ;
if ( fe . Size > 0 && tx . length != fe . Size )
2025-03-31 11:27:23 +02:00
Console . WriteLine ( $"{LogTimeStamp}*** File {i} with name {tx.remotefilename} has size {tx.length} but the size was reported as {fe.Size}" );
2013-02-12 21:43:14 +00:00
break ;
}
2025-03-31 11:27:23 +02:00
}
2013-02-12 21:43:14 +00:00
if (! found )
2018-03-20 13:50:06 -06:00
if ( originalRenamedFile != null && renamedFileNewName != null && originalRenamedFile . remotefilename == fe . Name )
{
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}*** File with name {fe.Name} was found on server but was supposed to have been renamed to {renamedFileNewName}!" );
2018-03-20 13:50:06 -06:00
}
else
{
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}*** File with name {fe.Name} was found on server but not uploaded!" );
2018-03-20 13:50:06 -06:00
}
2013-02-12 21:43:14 +00:00
}
2025-03-31 11:27:23 +02:00
for ( var i = 0 ; i < files . Count ; i ++)
if (! files [ i ]. found )
Console . WriteLine ( $"{LogTimeStamp}*** File {i} with name {files[i].remotefilename} was uploaded but not found afterwards" );
2013-02-12 21:43:14 +00:00
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Downloading files" );
2013-02-12 21:43:14 +00:00
for ( int i = 0 ; i < files . Count ; i ++)
{
2024-12-19 09:31:06 +01:00
using ( var cf = new Duplicati . Library . Utility . TempFile ())
2013-02-12 21:43:14 +00:00
{
Exception e = null ;
2025-03-31 11:23:29 +02:00
Console . Write ( $"{LogTimeStamp}Downloading file {i} ... " );
2013-02-12 21:43:14 +00:00
2025-01-17 09:45:49 -03:00
var sw = Stopwatch . StartNew ();
2025-04-22 22:54:48 +02:00
var throttleManager = new ThrottleManager () { Limit = throttleDownload };
2025-01-17 09:45:49 -03:00
2013-02-12 21:43:14 +00:00
try
{
2025-09-10 14:47:23 +02:00
Retry ( async () =>
2013-02-12 21:43:14 +00:00
{
2025-11-03 12:48:37 +01:00
if ( backend is IStreamingBackend streamingBackend && streamingBackend . SupportsStreaming && ! disableStreaming )
2025-09-10 14:47:23 +02:00
{
using ( var fs = new System . IO . FileStream ( cf , System . IO . FileMode . Create , System . IO . FileAccess . Write , System . IO . FileShare . None ))
using ( var timeoutStream = new TimeoutObservingStream ( fs ) { WriteTimeout = readWriteTimeout })
using ( var ts = new ThrottleEnabledStream ( timeoutStream , throttleManager ))
using ( var nss = new NonSeekableStream ( ts ))
await streamingBackend . GetAsync ( files [ i ]. remotefilename , nss , timeoutStream . TimeoutToken );
}
else
await backend . GetAsync ( files [ i ]. remotefilename , cf , CancellationToken . None );
}, retries ). Await ();
2013-02-12 21:43:14 +00:00
e = null ;
}
catch ( Exception ex )
{
e = ex ;
}
2025-04-22 22:54:48 +02:00
sw . Stop ();
2013-02-12 21:43:14 +00:00
if ( e != null )
2025-01-17 09:45:49 -03:00
{
failAfterFinished = true ;
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}failed\n*** Error: {e} after {sw.ElapsedMilliseconds} ms" );
2025-01-17 09:45:49 -03:00
}
2013-02-12 21:43:14 +00:00
else
2025-04-22 22:54:48 +02:00
Console . WriteLine ( $" done in {sw.ElapsedMilliseconds} ms (~{Utility.FormatSizeString(files[i].length / sw.Elapsed.TotalSeconds)}/s)" );
2013-02-12 21:43:14 +00:00
2025-03-31 11:23:29 +02:00
Console . Write ( $"{LogTimeStamp}Checking hash ... " );
2013-02-12 21:43:14 +00:00
2024-12-19 09:31:06 +01:00
using ( var fs = new System . IO . FileStream ( cf , System . IO . FileMode . Open , System . IO . FileAccess . Read ))
2013-02-12 21:43:14 +00:00
if ( Convert . ToBase64String ( sha . ComputeHash ( fs )) != Convert . ToBase64String ( files [ i ]. hash ))
{
if ( dummyFileHash != null && Convert . ToBase64String ( sha . ComputeHash ( fs )) == Convert . ToBase64String ( dummyFileHash ))
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}failed\n*** Downloaded file was the dummy file" ); // Should this be failed?
2013-02-12 21:43:14 +00:00
else
2025-01-17 09:45:49 -03:00
{
failAfterFinished = true ;
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}failed\n*** Downloaded file was corrupt" );
2025-01-17 09:45:49 -03:00
}
2013-02-12 21:43:14 +00:00
}
else
Console . WriteLine ( "done" );
}
}
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Deleting files..." );
2013-02-12 21:43:14 +00:00
2025-03-25 14:17:26 +01:00
for ( int i = 0 ; i < files . Count ; i ++)
try
{
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Deleting file {i}" );
2025-09-10 14:47:23 +02:00
Retry (() => backend . DeleteAsync ( files [ i ]. remotefilename , CancellationToken . None ), retries ). Await ();
2025-03-25 14:17:26 +01:00
}
2013-02-12 21:43:14 +00:00
catch ( Exception ex )
{
2025-01-17 09:45:49 -03:00
failAfterFinished = true ;
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}*** Failed to delete file {files[i].remotefilename}, message: {ex}" );
2013-02-12 21:43:14 +00:00
}
2024-12-20 16:10:08 +01:00
if ( waitAfterDelete > TimeSpan . Zero )
{
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Waiting {waitAfterDelete} after delete" );
2024-12-20 16:10:08 +01:00
Thread . Sleep ( waitAfterDelete );
}
2025-09-10 14:47:23 +02:00
curlist = Retry (() => backend . ListAsync ( CancellationToken . None ). ToBlockingEnumerable (). ToList (), retries );
2024-12-19 09:31:06 +01:00
foreach ( var fe in curlist )
2013-02-12 21:43:14 +00:00
if (! fe . IsFolder )
{
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}*** Remote folder contains {fe.Name} after cleanup" );
2013-02-12 21:43:14 +00:00
}
2020-03-23 14:10:40 -06:00
// Test some error cases
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Checking retrieval of non-existent file..." );
2024-12-19 09:31:06 +01:00
var caughtExpectedException = false ;
2020-03-23 14:10:40 -06:00
try
{
2024-12-19 09:31:06 +01:00
using ( var tempFile = new Duplicati . Library . Utility . TempFile ())
2020-03-23 14:10:40 -06:00
{
2025-03-31 11:23:29 +02:00
backend . GetAsync ( $"NonExistentFile-{Guid.NewGuid()}" , tempFile . Name , CancellationToken . None ). Await ();
2020-03-23 14:10:40 -06:00
}
}
2020-03-29 19:17:37 -07:00
catch ( FileMissingException )
2020-03-23 14:10:40 -06:00
{
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Caught expected FileMissingException" );
2020-03-23 14:10:40 -06:00
caughtExpectedException = true ;
}
catch ( Exception ex )
{
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}*** Retrieval of non-existent file failed: {ex}" );
2020-03-23 14:10:40 -06:00
}
if (! caughtExpectedException )
{
2025-01-17 09:45:49 -03:00
failAfterFinished = true ;
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}*** Retrieval of non-existent file should have failed with FileMissingException" );
2020-03-23 14:10:40 -06:00
}
2013-02-12 21:43:14 +00:00
}
2018-03-20 13:53:32 -06:00
// Test quota retrieval
2024-12-19 09:31:06 +01:00
if ( backend is IQuotaEnabledBackend quotaEnabledBackend )
2018-03-20 13:53:32 -06:00
{
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Checking quota..." );
2018-03-20 13:53:32 -06:00
IQuotaInfo quota = null ;
bool noException ;
try
{
2025-09-10 14:47:23 +02:00
quota = Retry (() => quotaEnabledBackend . GetQuotaInfoAsync ( CancellationToken . None ), retries ). Await ();
2018-03-20 13:53:32 -06:00
noException = true ;
}
catch ( Exception ex )
{
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}*** Checking quota information failed: {ex}" );
2018-03-20 13:53:32 -06:00
noException = false ;
}
if ( noException )
{
if ( quota != null )
{
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Free Space: {Library.Utility.Utility.FormatSizeString(quota.FreeQuotaSpace)}" );
Console . WriteLine ( $"{LogTimeStamp}Total Space: {Library.Utility.Utility.FormatSizeString(quota.TotalQuotaSpace)}" );
2018-03-20 13:53:32 -06:00
}
else
{
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Unable to retrieve quota information" );
2018-03-20 13:53:32 -06:00
}
}
}
2018-03-20 15:26:17 -06:00
// Test DNSName lookup
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Checking DNS names used by this backend..." );
2018-03-20 15:26:17 -06:00
try
{
2025-09-10 14:47:23 +02:00
var dnsNames = Retry (() => backend . GetDNSNamesAsync ( CancellationToken . None ), retries ). Await ();
2018-03-20 15:26:17 -06:00
if ( dnsNames != null )
{
foreach ( string dnsName in dnsNames )
{
Console . WriteLine ( dnsName );
}
}
else
{
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}No DNS names reported" );
2018-03-20 15:26:17 -06:00
}
}
catch ( Exception ex )
{
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}*** Checking DNSName failed: {ex}" );
2018-03-20 15:26:17 -06:00
}
2013-02-12 21:43:14 +00:00
}
finally
{
2024-12-19 09:31:06 +01:00
foreach ( var m in loadedModules )
2019-09-29 20:16:28 -07:00
if ( m is IDisposable disposable )
disposable . Dispose ();
2013-02-12 21:43:14 +00:00
}
2025-01-17 09:45:49 -03:00
return ! failAfterFinished ;
2013-02-12 21:43:14 +00:00
}
2024-12-19 09:31:06 +01:00
private static void Uploadfile ( string localfilename , int i , string remotefilename , IBackend backend , bool disableStreaming , long throttle , int readWriteTimeout )
2013-02-12 21:43:14 +00:00
{
2025-04-22 22:54:48 +02:00
var filesize = new System . IO . FileInfo ( localfilename ). Length ;
Console . Write ( $"{LogTimeStamp}Uploading file {i}, {Utility.FormatSizeString(filesize)} ... " );
2013-02-12 21:43:14 +00:00
Exception e = null ;
2025-01-17 09:45:49 -03:00
var sw = Stopwatch . StartNew ();
2025-04-22 22:54:48 +02:00
var throttleManager = new ThrottleManager () { Limit = throttle };
2025-01-17 09:45:49 -03:00
2013-02-12 21:43:14 +00:00
try
{
2025-11-03 12:48:37 +01:00
if ( backend is IStreamingBackend streamingBackend && streamingBackend . SupportsStreaming && ! disableStreaming )
2013-02-12 21:43:14 +00:00
{
2024-12-19 09:31:06 +01:00
using ( var fs = new System . IO . FileStream ( localfilename , System . IO . FileMode . Open , System . IO . FileAccess . Read , System . IO . FileShare . Read ))
using ( var timeoutStream = new TimeoutObservingStream ( fs ) { ReadTimeout = readWriteTimeout })
2025-04-22 22:54:48 +02:00
using ( var ts = new ThrottleEnabledStream ( timeoutStream , throttleManager ))
2024-12-19 09:31:06 +01:00
using ( var nss = new NonSeekableStream ( ts ))
streamingBackend . PutAsync ( remotefilename , nss , timeoutStream . TimeoutToken ). Await ();
2013-02-12 21:43:14 +00:00
}
else
2024-09-29 22:00:57 +02:00
backend . PutAsync ( remotefilename , localfilename , CancellationToken . None ). Await ();
2013-02-12 21:43:14 +00:00
e = null ;
}
catch ( Exception ex )
{
e = ex ;
}
2025-04-22 22:54:48 +02:00
sw . Stop ();
2013-02-12 21:43:14 +00:00
if ( e != null )
{
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp}Failed to upload file {i}, error message: {e}, remote name: {remotefilename} after {sw.ElapsedMilliseconds} ms" );
2013-02-12 21:43:14 +00:00
while ( e . InnerException != null )
{
e = e . InnerException ;
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $"{LogTimeStamp} Inner exception: {e}" );
2013-02-12 21:43:14 +00:00
}
}
else
{
2025-04-22 22:54:48 +02:00
Console . WriteLine ( $" done! in {sw.ElapsedMilliseconds} ms (~{Utility.FormatSizeString(filesize / sw.Elapsed.TotalSeconds)}/s)" );
2013-02-12 21:43:14 +00:00
}
}
2018-03-20 13:48:21 -06:00
private static string CreateRandomRemoteFileName ( int min_filename_size , int max_filename_size , string allowedChars , bool trimFilenameSpaces , Random rnd )
{
StringBuilder filenameBuilder = new StringBuilder ();
int filenamelen = rnd . Next ( min_filename_size , max_filename_size );
for ( int j = 0 ; j < filenamelen ; j ++)
filenameBuilder . Append ( allowedChars [ rnd . Next ( 0 , allowedChars . Length )]);
string filename = filenameBuilder . ToString ();
if ( trimFilenameSpaces )
filename = filename . Trim ();
return filename ;
}
2013-02-12 21:43:14 +00:00
private static string CreateRandomFile ( Library . Utility . TempFolder tf , int i , int min_file_size , int max_file_size , Random rnd )
{
2025-03-31 11:23:29 +02:00
Console . Write ( $"{LogTimeStamp}Generating file {i}" );
2013-02-12 21:43:14 +00:00
string filename = System . IO . Path . Combine ( tf , i . ToString ());
using ( System . IO . FileStream fs = new System . IO . FileStream ( filename , System . IO . FileMode . CreateNew , System . IO . FileAccess . Write ))
{
//Random size
byte [] buf = new byte [ 1024 ];
int size = rnd . Next ( min_file_size , max_file_size );
2025-03-31 11:23:29 +02:00
Console . WriteLine ( $" ({Duplicati.Library.Utility.Utility.FormatSizeString(size)})" );
2013-02-12 21:43:14 +00:00
while ( size > 0 )
{
rnd . NextBytes ( buf );
fs . Write ( buf , 0 , Math . Min ( buf . Length , size ));
size -= buf . Length ;
}
}
return filename ;
}
2025-09-10 14:47:23 +02:00
private static async Task < T > Retry < T >( Func < Task < T >> action , int retries )
2013-02-12 21:43:14 +00:00
{
2025-09-10 14:47:23 +02:00
var total = retries ;
while ( true )
2013-02-12 21:43:14 +00:00
{
2025-09-10 14:47:23 +02:00
retries --;
try
{
return await action ();
}
catch
{
if ( retries <= 0 )
throw ;
var delay = Utility . GetRetryDelay ( TimeSpan . FromSeconds ( 1 ), total - retries , true );
Console . WriteLine ( $"{LogTimeStamp}Operation failed with exception, {retries} retries left, delaying retry for {delay.TotalMilliseconds} ms" );
await Task . Delay ( delay );
Console . WriteLine ( $"{LogTimeStamp}Retrying after error, {retries} retries left" );
}
2013-02-12 21:43:14 +00:00
}
}
2025-09-10 14:47:23 +02:00
private static async Task Retry ( Func < Task > action , int retries )
=> await Retry ( async () => { await action (); return true ; }, retries );
private static void Retry ( Action action , int retries )
=> Retry (() => { action (); return true ; }, retries );
private static T Retry < T >( Func < T > action , int retries )
=> Retry (() => Task . FromResult ( action ()), retries ). Result ;
private static string LogTimeStamp => $"[{DateTime.Now:HH:mm:ss fff}] " ;
public static IList < ICommandLineArgument > SupportedCommands => [
new CommandLineArgument ( "reruns" , CommandLineArgument . ArgumentType . Integer , "The number of test runs to perform" , "A number that describes how many times the test is performed" , "5" ),
new CommandLineArgument ( "tempdir" , CommandLineArgument . ArgumentType . Path , "The path used to store temporary files" , "The backend tester will use the system default temp path. You can set this option to choose another path." ),
new CommandLineArgument ( "extended-chars" , CommandLineArgument . ArgumentType . String , "A list of allowed extended filename chars" , "A list of characters besides {a-z, A-Z, 0-9} to use when generating filenames" , ExtendedChars ),
new CommandLineArgument ( "number-of-files" , CommandLineArgument . ArgumentType . Integer , "The number of files to test with" , "An integer describing how many files to upload during a test run" , "10" ),
new CommandLineArgument ( "min-file-size" , CommandLineArgument . ArgumentType . Size , "The minimum allowed file size" , "File sizes are chosen at random, this value is the lower bound" , "1kb" ),
new CommandLineArgument ( "max-file-size" , CommandLineArgument . ArgumentType . Size , "The maximum allowed file size" , "File sizes are chosen at random, this value is the upper bound" , "50mb" ),
new CommandLineArgument ( "min-filename-length" , CommandLineArgument . ArgumentType . Integer , "The minimum allowed filename length" , "File name lengths are chosen at random, this value is the lower bound" , "5" ),
new CommandLineArgument ( "max-filename-length" , CommandLineArgument . ArgumentType . Integer , "The minimum allowed filename length" , "File name lengths are chosen at random, this value is the upper bound" , "80" ),
new CommandLineArgument ( "trim-filename-spaces" , CommandLineArgument . ArgumentType . Boolean , "Trim whitespace from filenames" , "A value that indicates if whitespace should be trimmed from the ends of randomly generated filenames" , "false" ),
new CommandLineArgument ( "auto-create-folder" , CommandLineArgument . ArgumentType . Boolean , "Allow automatic folder creation" , "A value that indicates if missing folders are created automatically" , "false" ),
new CommandLineArgument ( "skip-overwrite-test" , CommandLineArgument . ArgumentType . Boolean , "Bypass the overwrite test" , "A value that indicates if dummy files should be uploaded prior to uploading the real files" , "false" ),
new CommandLineArgument ( "auto-clean" , CommandLineArgument . ArgumentType . Boolean , "Remove any files found in target folder" , "A value that indicates if all files in the target folder should be deleted before starting the first test" , "false" ),
new CommandLineArgument ( "force" , CommandLineArgument . ArgumentType . Boolean , "Activate file deletion" , "A value that indicates if existing files should really be deleted when using auto-clean" , "false" ),
new CommandLineArgument ( "wait-after-upload" , CommandLineArgument . ArgumentType . Timespan , "Wait after all uploads" , "A value that indicates how long to wait after all files are uploaded, to account for the backends eventual consistency" , "0s" ),
new CommandLineArgument ( "wait-after-delete" , CommandLineArgument . ArgumentType . Timespan , "Wait after all deletes" , "A value that indicates how long to wait after each delete operation, to account for the backends eventual consistency" , "0s" ),
new CommandLineArgument ( "failure-retries" , CommandLineArgument . ArgumentType . Integer , "The number of retries for each operation" , "An integer that indicates how many times an operation should be retried before giving up" , "0" ),
];
2013-02-12 21:43:14 +00:00
}
}