2025-01-21 15:22:04 +01: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
// DEALINGS IN THE SOFTWARE.
2025-01-17 08:57:02 +01:00
using Duplicati.Library.Common.IO ;
using Duplicati.Library.Interface ;
2025-01-21 15:22:04 +01:00
using System ;
using System.Collections.Generic ;
2025-01-22 09:37:28 +01:00
using System.CommandLine ;
2025-01-17 08:57:02 +01:00
using System.IO ;
2025-01-21 15:22:04 +01:00
using System.Linq ;
2025-01-17 08:57:02 +01:00
using System.Threading ;
2025-01-22 09:37:28 +01:00
using System.Threading.Tasks ;
2025-01-17 08:57:02 +01:00
namespace RemoteSynchronization
{
2025-01-22 06:10:43 +01:00
public class Program
2025-01-17 08:57:02 +01:00
{
2025-01-22 09:37:28 +01:00
public static async Task < int > Main ( string [] args )
2025-01-17 08:57:02 +01:00
{
2025-01-22 09:37:28 +01:00
var src_arg = new Argument < string >( name : "backend_src" , description : "The source backend string" );
var dst_arg = new Argument < string >( name : "backend_dst" , description : "The destination backend string" );
2025-01-17 08:57:02 +01:00
2025-01-22 09:37:28 +01:00
var root_cmd = new RootCommand ( "Remote Synchronization Tool" );
root_cmd . AddArgument ( src_arg );
root_cmd . AddArgument ( dst_arg );
2025-01-17 08:57:02 +01:00
2025-01-22 09:37:28 +01:00
root_cmd . SetHandler ( Run , src_arg , dst_arg );
return await root_cmd . InvokeAsync ( args );
}
private static async Task < int > Run ( string src , string dst )
{
var options = new Dictionary < string , string >();
using ( var b1 = Duplicati . Library . DynamicLoader . BackendLoader . GetBackend ( src , options ))
2025-01-17 08:57:02 +01:00
{
2025-01-22 09:37:28 +01:00
var b1s = b1 as IStreamingBackend ;
System . Diagnostics . Debug . Assert ( b1s != null );
using ( var b2 = Duplicati . Library . DynamicLoader . BackendLoader . GetBackend ( dst , options ))
{
var b2s = b2 as IStreamingBackend ;
System . Diagnostics . Debug . Assert ( b2s != null );
var ( to_copy , to_delete ) = PrepareFileLists ( b1s , b2s );
var deleted = Delete ( b2s , to_delete );
Console . WriteLine ( $"Deleted {deleted} files from {dst}" );
var copied = Copy ( b1s , b2s , to_copy );
Console . WriteLine ( $"Copied {copied} files from {src} to {dst}" );
}
2025-01-17 08:57:02 +01:00
}
2025-01-22 09:37:28 +01:00
return 42 ;
2025-01-17 08:57:02 +01:00
}
2025-01-20 07:58:21 +01:00
2025-01-20 07:58:39 +01:00
// TODO maximum retention?: don't delete files, only rename old ones.
2025-01-21 15:18:32 +01:00
// TODO have concurrency parameters: uploaders, downloaders
// TODO low memory mode, where things aren't kept in memory. Maybe utilize SQLite?
// TODO Force parameter
// TODO Progress reporting
// TODO Dry run
// TODO Logging
2025-01-20 07:58:39 +01:00
// TODO check database consistency. I.e. have both databases, check that the block, volume, files, etc match up.
// TODO introduce these checks as a post processing step? Especially the database consistency check, as that is often recreated from the index files.
2025-01-21 15:18:32 +01:00
// TODO This tool shouldn't handle it, but for convenience, it should support making the seperate call to the regular Duplicati on the destination backend, which alread carries this functionality.
2025-01-20 07:58:21 +01:00
// Forcefully synchronize the remote backends
2025-01-21 15:14:50 +01:00
private static long Copy ( IStreamingBackend b_src , IStreamingBackend b_dst , IEnumerable < IFileEntry > files )
2025-01-20 07:58:21 +01:00
{
long successful_copies = 0 ;
foreach ( var f in files )
{
try
{
var s = new MemoryStream ();
2025-01-21 15:14:50 +01:00
var s1 = b_src . GetAsync ( f . Name , s , CancellationToken . None );
2025-01-20 07:58:21 +01:00
s1 . Wait ();
2025-01-21 15:14:50 +01:00
var s2 = b_dst . PutAsync ( f . Name , s , CancellationToken . None );
2025-01-20 07:58:21 +01:00
s2 . Wait ();
successful_copies ++;
}
catch ( Exception e )
{
Console . WriteLine ( $"Error copying {f.Name}: {e.Message}" );
}
}
return successful_copies ;
}
2025-01-21 15:14:50 +01:00
2025-01-21 15:16:42 +01:00
private static long Delete ( IStreamingBackend b , IEnumerable < IFileEntry > files )
{
long successful_deletes = 0 ;
foreach ( var f in files )
{
try
{
b . DeleteAsync ( f . Name , CancellationToken . None ). Wait ();
successful_deletes ++;
}
catch ( Exception e )
{
Console . WriteLine ( $"Error deleting {f.Name}: {e.Message}" );
}
}
return successful_deletes ;
}
2025-01-21 15:14:50 +01:00
private static ( IEnumerable < IFileEntry >, IEnumerable < IFileEntry >) PrepareFileLists ( IStreamingBackend b_src , IStreamingBackend b_dst )
{
var files_src = b_src . List ();
var files_dst = b_dst . List ();
// Shortcut for empty destination
if (! files_dst . Any ())
{
return ( files_src , []);
}
var lookup_src = files_src . ToDictionary ( x => x . Name );
var lookup_dst = files_dst . ToDictionary ( x => x . Name );
var to_copy = new List < IFileEntry >();
var to_delete = new HashSet < string >();
// Find all of the files in src that are not in dst, have a different size or have a more recent modification date
foreach ( var f_src in files_src )
{
if ( lookup_dst . TryGetValue ( f_src . Name , out var f_dst ))
{
if ( f_src . Size != f_dst . Size || f_src . LastModification > f_dst . LastModification )
{
to_copy . Add ( f_src );
to_delete . Add ( f_dst . Name );
}
}
else
{
to_copy . Add ( f_src );
}
}
// Find all of the files in dst that are not in src
foreach ( var f_dst in files_dst )
{
if ( to_delete . Contains ( f_dst . Name ))
continue ;
if (! lookup_src . ContainsKey ( f_dst . Name ))
{
to_delete . Add ( f_dst . Name );
}
}
return ( to_copy , to_delete . Select ( x => lookup_dst [ x ]));
}
2025-01-21 15:16:59 +01:00
private static long Rename ( IStreamingBackend b , IEnumerable < FileEntry > files )
{
long successful_renames = 0 ;
string suffix = $"{System.DateTime.Now:yyyyMMddHHmmss}.old" ;
foreach ( var f in files )
{
try
{
var downloaded = new MemoryStream ();
b . GetAsync ( f . Name , downloaded , CancellationToken . None ). Wait ();
b . PutAsync ( $"{f.Name}.{suffix}" , downloaded , CancellationToken . None ). Wait ();
successful_renames ++;
}
catch ( Exception e )
{
Console . WriteLine ( $"Error renaming {f.Name}: {e.Message}" );
}
}
return successful_renames ;
}
2025-01-17 08:57:02 +01:00
}
}