2026-04-16 15:21:24 +02:00
// Copyright (C) 2026, The Duplicati Team
2025-05-22 15:28:59 +02: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
2024-06-05 11:02:56 +02:00
// DEALINGS IN THE SOFTWARE.
2013-11-23 14:26:36 +01:00
using System ;
2026-06-25 17:16:38 +02:00
using Duplicati.Server.Serialization ;
2013-11-23 14:26:36 +01:00
using Duplicati.Server.Serialization.Interface ;
using System.Collections.Generic ;
2020-08-23 13:58:34 -07:00
using System.Collections.Specialized ;
2018-11-08 20:00:51 +01:00
using System.Linq ;
2013-11-23 14:26:36 +01:00
namespace Duplicati.Server.Database
{
public class Backup : IBackup
2019-11-30 11:35:43 -08:00
{
2013-11-23 14:26:36 +01:00
public Backup ()
{
2014-03-17 15:27:25 +01:00
this . ID = null ;
2013-11-23 14:26:36 +01:00
}
2024-06-07 15:56:43 +02:00
2013-11-23 14:26:36 +01:00
internal void LoadChildren ( Connection con )
{
2014-03-17 15:27:25 +01:00
if ( this . IsTemporary )
2013-11-23 14:26:36 +01:00
{
this . Sources = new string [ 0 ];
this . Settings = new ISetting [ 0 ];
this . Filters = new IFilter [ 0 ];
this . Metadata = new Dictionary < string , string >();
}
else
{
2014-03-17 15:27:25 +01:00
var id = long . Parse ( this . ID );
this . Sources = con . GetSources ( id );
this . Settings = con . GetSettings ( id );
this . Filters = con . GetFilters ( id );
this . Metadata = con . GetMetadata ( id );
2026-02-18 11:06:08 +01:00
this . AdditionalTargetURLs = con . GetBackupTargetUrls ( id );
2013-11-23 14:26:36 +01:00
}
}
2024-06-07 15:56:43 +02:00
2025-09-17 15:07:09 +02:00
public void SetDBPath ( string path ) => this . DBPath = path ;
2024-06-07 15:56:43 +02:00
2013-11-23 14:26:36 +01:00
/// <summary>
/// The backup ID
/// </summary>
2014-03-17 15:27:25 +01:00
public string ID { get ; set ; }
2013-11-23 14:26:36 +01:00
/// <summary>
2025-09-17 15:07:09 +02:00
/// The external ID for tracking the backup, or null if not set
/// </summary>
public string ExternalID { get ; set ; }
/// <summary>
2013-11-23 14:26:36 +01:00
/// The backup name
/// </summary>
public string Name { get ; set ; }
/// <summary>
2018-09-20 13:57:03 +03:00
/// The backup description
/// </summary>
public string Description { get ; set ; }
/// <summary>
2013-11-23 14:26:36 +01:00
/// The backup tags
/// </summary>
public string [] Tags { get ; set ; }
/// <summary>
2018-11-04 22:07:24 +01:00
/// The backup target url
2013-11-23 14:26:36 +01:00
/// </summary>
public string TargetURL { get ; set ; }
/// <summary>
/// The path to the local database
/// </summary>
public string DBPath { get ; internal set ; }
2024-06-07 15:56:43 +02:00
2026-01-30 13:59:47 +01:00
/// <summary>
/// The connection string ID, or -1 if not used
/// </summary>
public long ConnectionStringID { get ; set ; } = - 1 ;
2026-06-25 17:16:38 +02:00
/// <summary>
/// The operation this backup performs when it runs.
/// </summary>
public OperationType OperationType { get ; set ; } = OperationType . Backup ;
2013-11-23 14:26:36 +01:00
/// <summary>
/// The backup source folders and files
/// </summary>
public string [] Sources { get ; set ; }
2024-06-07 15:56:43 +02:00
2013-11-23 14:26:36 +01:00
/// <summary>
/// The backup settings
/// </summary>
public ISetting [] Settings { get ; set ; }
2024-06-07 15:56:43 +02:00
2013-11-23 14:26:36 +01:00
/// <summary>
/// The filters applied to the source files
/// </summary>
public IFilter [] Filters { get ; set ; }
2024-06-07 15:56:43 +02:00
2013-11-23 14:26:36 +01:00
/// <summary>
/// The backup metadata
/// </summary>
2024-06-07 15:56:43 +02:00
public IDictionary < string , string > Metadata { get ; set ; }
2026-02-18 11:06:08 +01:00
/// <summary>
/// Additional target URLs for remote synchronization
/// These are used by RemoteSynchronizationModule and are separate from the primary TargetURL
/// </summary>
public IEnumerable < ITargetUrlEntry > AdditionalTargetURLs { get ; set ; } = new List < ITargetUrlEntry >();
2014-03-17 15:27:25 +01:00
/// <summary>
/// Gets a value indicating if this instance is not persisted to the database
/// </summary>
2018-11-21 09:55:56 +01:00
public bool IsTemporary { get { return ID != null && ID . IndexOf ( "-" , StringComparison . Ordinal ) > 0 ; } }
2014-03-17 15:27:25 +01:00
2018-11-04 22:07:24 +01:00
/// <summary>
2025-09-04 22:03:10 +02:00
/// Sanitizes the backup TargetUrl from any fields in the PasswordFieldNames list.
2018-11-04 22:07:24 +01:00
/// </summary>
2025-09-04 22:03:10 +02:00
private void SanitizeTargetUrl ()
2018-11-04 22:07:24 +01:00
{
2018-11-08 20:00:51 +01:00
var url = new Duplicati . Library . Utility . Uri ( this . TargetURL );
2020-08-23 13:58:34 -07:00
NameValueCollection filteredParameters = new NameValueCollection ();
if ( url . Query != null )
{
// We cannot use url.QueryParameters since it contains decoded parameter values, which
2024-07-26 00:08:45 -04:00
// breaks assumptions made by the decode_uri function in AppUtils.js. Since we are simply
2020-08-23 13:58:34 -07:00
// removing password parameters, we will leave the parameters as they are in the target URL.
filteredParameters = Library . Utility . Uri . ParseQueryString ( url . Query , false );
2025-09-04 22:03:10 +02:00
foreach ( var field in Connection . PasswordFieldNames )
2020-08-23 13:58:34 -07:00
filteredParameters . Remove ( field );
2018-11-08 20:00:51 +01:00
}
2020-08-23 13:58:34 -07:00
url = url . SetQuery ( Duplicati . Library . Utility . Uri . BuildUriQuery ( filteredParameters ));
2019-11-30 11:35:43 -08:00
this . TargetURL = url . ToString ();
2018-11-08 20:00:51 +01:00
}
/// <summary>
2025-09-04 22:03:10 +02:00
/// Sanitizes the settings from any fields in the PasswordFieldNames list.
2018-11-08 20:00:51 +01:00
/// </summary>
2025-09-04 22:03:10 +02:00
private void SanitizeSettings ()
{
this . Settings = this . Settings . Where (( setting ) => ! Connection . PasswordFieldNames . Contains ( setting . Name )). ToArray ();
}
2026-01-30 14:36:57 +01:00
/// <summary>
/// Sanitizes the sources from any fields in the PasswordFieldNames list.
/// </summary>
private void SanitizeSources ()
{
if ( this . Sources == null )
return ;
for ( int i = 0 ; i < this . Sources . Length ; i ++)
{
if ( SourceMasking . IsSpecialSource ( this . Sources [ i ]))
{
var urlString = SourceMasking . ExtractUrl ( this . Sources [ i ]);
var url = new Library . Utility . Uri ( urlString );
if ( url . Query != null )
{
var filteredParameters = Library . Utility . Uri . ParseQueryString ( url . Query , false );
foreach ( var field in Connection . PasswordFieldNames )
filteredParameters . Remove ( field );
url = url . SetQuery ( Library . Utility . Uri . BuildUriQuery ( filteredParameters ));
this . Sources [ i ] = SourceMasking . ReplaceUrl ( this . Sources [ i ], url . ToString ());
}
}
}
}
2025-09-04 22:03:10 +02:00
/// <inheritdoc/>
public void RemoveSensitiveInformation ()
2018-11-08 20:00:51 +01:00
{
2025-09-04 22:03:10 +02:00
SanitizeTargetUrl ();
SanitizeSettings ();
2026-01-30 14:36:57 +01:00
SanitizeSources ();
2026-02-18 11:06:08 +01:00
SanitizeAdditionalTargetUrls ();
}
/// <summary>
/// Sanitizes the additional target URLs from any fields in the PasswordFieldNames list.
/// </summary>
private void SanitizeAdditionalTargetUrls ()
{
if ( AdditionalTargetURLs == null )
return ;
foreach ( var target in AdditionalTargetURLs )
{
if ( target == null || string . IsNullOrEmpty ( target . TargetUrl ))
continue ;
var url = new Duplicati . Library . Utility . Uri ( target . TargetUrl );
var filteredParameters = url . QueryParameters ;
foreach ( var field in Connection . PasswordFieldNames )
filteredParameters . Remove ( field );
url = url . SetQuery ( Duplicati . Library . Utility . Uri . BuildUriQuery ( filteredParameters ));
target . TargetUrl = url . ToString ();
}
2025-09-04 22:03:10 +02:00
}
/// <inheritdoc/>
public void MaskSensitiveInformation ()
{
var protectedNames = Connection . PasswordFieldNames ;
TargetURL = QuerystringMasking . Mask ( TargetURL , protectedNames );
2026-01-30 14:36:57 +01:00
Sources = SourceMasking . MaskSources ( Sources , protectedNames );
2025-09-04 22:03:10 +02:00
2026-02-18 11:06:08 +01:00
// Mask additional target URLs
if ( AdditionalTargetURLs != null )
foreach ( var target in AdditionalTargetURLs )
if ( target != null )
target . TargetUrl = QuerystringMasking . Mask ( target . TargetUrl , protectedNames );
2025-09-04 22:03:10 +02:00
foreach ( var setting in this . Settings )
if ( protectedNames . Contains ( setting . Name ))
setting . Value = Connection . PASSWORD_PLACEHOLDER ;
}
/// <inheritdoc/>
2026-02-19 16:35:49 +01:00
public void UnmaskSensitiveInformation ( IBackup previous , IReadOnlyDictionary < long , string > connectionStrings )
2025-09-04 22:03:10 +02:00
{
if ( previous == null )
throw new ArgumentNullException ( nameof ( previous ));
2026-02-19 16:35:49 +01:00
// If there is a connection string ID, and it is different from the previous one,
// the user has changed the connection strings source
var constr = ConnectionStringID > 0 && ConnectionStringID != previous . ConnectionStringID
? connectionStrings . GetValueOrDefault ( previous . ConnectionStringID )
: previous . TargetURL ;
TargetURL = QuerystringMasking . Unmask ( TargetURL , [ constr , previous . TargetURL ]);
2026-01-30 14:36:57 +01:00
Sources = SourceMasking . UnmaskSources ( Sources , previous . Sources );
2025-09-04 22:03:10 +02:00
2026-02-18 11:06:08 +01:00
// Unmask additional target URLs
var prevAdditionalTargets = previous . AdditionalTargetURLs ?. ToList ();
if ( AdditionalTargetURLs != null && prevAdditionalTargets != null )
{
var prevTargets = prevAdditionalTargets . ToDictionary (
x => x . TargetUrlKey ,
2026-02-19 16:35:49 +01:00
x => x ,
2026-02-18 11:06:08 +01:00
StringComparer . OrdinalIgnoreCase );
foreach ( var target in AdditionalTargetURLs )
{
if ( target != null && Connection . UrlContainsPasswordPlaceholder ( target . TargetUrl ))
{
if ( prevTargets . TryGetValue ( target . TargetUrlKey , out var prevValue ))
2026-02-19 16:35:49 +01:00
{
var prevTarget = target . ConnectionStringID > 0 && target . ConnectionStringID != prevValue . ConnectionStringID
? connectionStrings . GetValueOrDefault ( prevValue . ConnectionStringID )
: prevValue . TargetUrl ;
target . TargetUrl = QuerystringMasking . Unmask ( target . TargetUrl , [ prevTarget , prevValue . TargetUrl ]);
}
else if ( target . ConnectionStringID > 0 )
{
target . TargetUrl = QuerystringMasking . Unmask ( target . TargetUrl , connectionStrings . GetValueOrDefault ( target . ConnectionStringID ));
}
2026-02-18 11:06:08 +01:00
else
throw new InvalidOperationException ( $"Cannot unmask target URL with key '{target.TargetUrlKey}' because it did not exist in the previous configuration." );
}
}
}
2025-09-04 22:03:10 +02:00
var prevSettings = previous . Settings . ToDictionary ( x => x . Name , x => x . Value , StringComparer . OrdinalIgnoreCase );
foreach ( var setting in this . Settings )
{
2025-09-25 13:11:39 +02:00
if ( Connection . IsPasswordPlaceholder ( setting . Value ))
2025-09-04 22:03:10 +02:00
{
if ( prevSettings . TryGetValue ( setting . Name , out var prevValue ))
setting . Value = prevValue ;
else
throw new InvalidOperationException ( $"Cannot unmask setting '{setting.Name}' because it did not exist in the previous configuration." );
}
}
2018-11-04 22:07:24 +01:00
}
2025-11-03 19:03:16 +01:00
public Backup Clone ()
{
return new Backup ()
{
ID = this . ID ,
ExternalID = this . ExternalID ,
Name = this . Name ,
Description = this . Description ,
2025-11-03 19:24:16 +01:00
Tags = ( string []) this . Tags ?. Clone () ?? [],
2025-11-03 19:03:16 +01:00
TargetURL = this . TargetURL ,
2026-06-25 17:16:38 +02:00
ConnectionStringID = this . ConnectionStringID ,
OperationType = this . OperationType ,
2025-11-03 19:03:16 +01:00
DBPath = this . DBPath ,
2025-11-03 19:24:16 +01:00
Sources = ( string []) this . Sources ?. Clone () ?? [],
Settings = this . Settings ?. Select ( s => new Setting { Name = s . Name , Value = s . Value , Filter = s . Filter }). ToArray () ?? [],
Filters = this . Filters ?. Select ( f => new Filter { Order = f . Order , Include = f . Include , Expression = f . Expression }). ToArray () ?? [],
2026-02-18 11:06:08 +01:00
Metadata = new Dictionary < string , string >( this . Metadata ),
AdditionalTargetURLs = this . AdditionalTargetURLs ?. Select ( t => new TargetUrlEntry
{
ID = t . ID ,
BackupID = t . BackupID ,
TargetUrlKey = t . TargetUrlKey ,
TargetUrl = t . TargetUrl ,
Mode = t . Mode ,
Interval = t . Interval ,
Options = t . Options ?. ToDictionary ( kvp => kvp . Key , kvp => kvp . Value ),
CreatedAt = t . CreatedAt ,
UpdatedAt = t . UpdatedAt
}). Cast < ITargetUrlEntry >(). ToList () ?? new List < ITargetUrlEntry >()
2025-11-03 19:03:16 +01:00
};
}
2013-11-23 14:26:36 +01:00
}
}