2026-04-16 15:21:24 +02:00
// Copyright (C) 2026, The Duplicati Team
2025-01-06 10:22:23 -03: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 ;
2025-03-21 10:26:01 +01:00
using Duplicati.Library.Backend.SMB ;
using Duplicati.Library.Backend.SMB.Model ;
2025-01-06 10:22:23 -03:00
using SMBLibrary ;
2025-02-17 14:01:08 +01:00
using Duplicati.Library.SourceProvider ;
using System.Runtime.CompilerServices ;
2025-02-21 13:20:52 +01:00
using Duplicati.Library.Common.IO ;
2025-03-20 12:02:25 +01:00
using Duplicati.Library.Utility.Options ;
2025-05-21 17:19:27 +02:00
using Duplicati.Library.Utility ;
2025-01-06 10:22:23 -03:00
namespace Duplicati.Library.Backend ;
/// <summary>
/// Native CIFS/SMB Backend implementation
/// </summary>
2026-01-27 13:08:20 +01:00
public class SMBBackend : IStreamingBackend , IFolderEnabledBackend , IRenameEnabledBackend
2025-01-06 10:22:23 -03:00
{
2025-11-03 12:48:37 +01:00
/// <inheritdoc/>
2025-03-21 10:26:01 +01:00
public virtual string ProtocolKey => "smb" ;
2025-01-06 10:22:23 -03:00
2025-11-03 12:48:37 +01:00
/// <inheritdoc/>
2025-03-21 10:26:01 +01:00
public virtual string DisplayName => Strings . SMBBackend . DisplayName ;
2025-01-06 10:22:23 -03:00
2025-11-03 12:48:37 +01:00
/// <inheritdoc/>
2025-03-21 10:26:01 +01:00
public virtual string Description => Strings . SMBBackend . Description ;
2025-01-06 10:22:23 -03:00
2025-11-03 12:48:37 +01:00
/// <inheritdoc/>
public virtual bool SupportsStreaming => true ;
2025-01-06 10:22:23 -03:00
/// <summary>
/// Hostname only (no ports or paths) to be used on DNS resolutions.
/// </summary>
private string _DnsName ;
/// <summary>
/// Path separators (both Windows \ and unix /) to be used in path manipulation
/// </summary>
private static readonly char [] PATH_SEPARATORS = [ '/' , '\\' ];
/// <summary>
/// Cache of parsed connection parameters
/// </summary>
2025-03-20 12:02:25 +01:00
private readonly SMBConnectionParameters _connectionParameters ;
/// <summary>
/// Timeout options to be used in the backend
/// </summary>
private readonly TimeoutOptionsHelper . Timeouts _timeouts ;
2025-01-06 10:22:23 -03:00
/// <summary>
/// Shared connection between all methods to avoid re-authentication
/// </summary>
2025-03-20 12:02:25 +01:00
private SMBShareConnection ? _sharedConnection ;
2025-02-17 14:01:08 +01:00
2025-01-10 13:53:51 -03:00
/// <summary>
/// Read buffer size for SMB operations (will be capped automatically by SMB negotiated values)
/// </summary>
private const string READ_BUFFER_SIZE_OPTION = "read-buffer-size" ;
2025-02-17 14:01:08 +01:00
2025-01-10 13:53:51 -03:00
/// <summary>
/// Write buffer size for SMB operations (will be capped automatically by SMB negotiated values)
/// </summary>
private const string WRITE_BUFFER_SIZE_OPTION = "write-buffer-size" ;
2025-01-06 10:22:23 -03:00
/// <summary>
/// Backend option for controlling the transport (directtcp or netbios)
/// </summary>
private const string TRANSPORT_OPTION = "transport" ;
/// <summary>
/// Domain (complementary part of authentication) option
/// </summary>
private const string AUTH_DOMAIN_OPTION = "auth-domain" ;
/// <summary>
/// Defines the default transport to be used in CIFS connection
/// </summary>
private const string DEFAULT_TRANSPORT = "directtcp" ;
/// <summary>
/// Mapping of transport string to SMBTransportType enum to be used in parsing the option string
/// </summary>
private readonly Dictionary < string , SMBTransportType > _transportMap = new ()
{
["directtcp"] = SMBTransportType . DirectTCPTransport ,
["netbios"] = SMBTransportType . NetBiosOverTCP
};
/// <summary>
/// Empty constructor is required for the backend to be loaded by the backend factory
/// </summary>
2025-03-21 10:26:01 +01:00
public SMBBackend ()
2025-01-06 10:22:23 -03:00
{
2025-03-20 12:02:25 +01:00
_DnsName = null !;
_connectionParameters = null !;
_timeouts = null !;
2025-01-06 10:22:23 -03:00
}
/// <summary>
/// Actual constructor for the backend that accepts the url and options
/// </summary>
/// <param name="url">URL in Duplicati Uri format</param>
/// <param name="options">options to be used in the backend</param>
2025-03-21 10:26:01 +01:00
public SMBBackend ( string url , Dictionary < string , string? > options )
2025-01-06 10:22:23 -03:00
{
if ( string . IsNullOrEmpty ( url ))
throw new ArgumentNullException ( nameof ( url ));
if ( options == null )
throw new ArgumentNullException ( nameof ( options ));
var uri = new Utility . Uri ( url );
uri . RequireHost ();
2025-08-07 08:06:24 +02:00
_DnsName = uri . Host ?? "" ;
2025-02-17 14:01:08 +01:00
2025-01-06 10:22:23 -03:00
var input = uri . Path . TrimEnd ( '/' );
var slashIndex = input . IndexOf ( '/' ); // Find first slash to separate server and share if present.
2025-02-17 14:01:08 +01:00
2025-03-20 12:02:25 +01:00
var auth = AuthOptionsHelper . Parse ( options , uri );
var authDomain = options . GetValueOrDefault ( AUTH_DOMAIN_OPTION );
var transport = options . GetValueOrDefault ( TRANSPORT_OPTION );
2025-02-17 14:01:08 +01:00
2025-01-10 13:53:51 -03:00
int? readBufferSize = null , writeBufferSize = null ;
2025-02-17 14:01:08 +01:00
2025-03-20 12:02:25 +01:00
var readBufferSizeConfig = options . GetValueOrDefault ( READ_BUFFER_SIZE_OPTION );
if (! string . IsNullOrWhiteSpace ( readBufferSizeConfig ))
readBufferSize = int . TryParse ( readBufferSizeConfig , out int value ) ? value : null ;
2025-02-17 14:01:08 +01:00
2025-03-20 12:02:25 +01:00
var writeBufferSizeConfig = options . GetValueOrDefault ( WRITE_BUFFER_SIZE_OPTION );
if (! string . IsNullOrWhiteSpace ( writeBufferSizeConfig ))
2025-05-20 15:24:15 +02:00
writeBufferSize = int . TryParse ( writeBufferSizeConfig , out int value ) ? value : null ;
2025-02-17 14:01:08 +01:00
2025-01-10 13:53:51 -03:00
// Normalize to 10KB minimum buffers size
readBufferSize = readBufferSize < 1024 * 10 ? null : readBufferSize ;
writeBufferSize = writeBufferSize < 1024 * 10 ? null : writeBufferSize ;
2025-01-08 22:25:34 -03:00
2025-03-20 12:02:25 +01:00
var transportType = _transportMap . TryGetValue (
2025-02-17 14:01:08 +01:00
string . IsNullOrEmpty ( transport ) ? DEFAULT_TRANSPORT : transport . ToLower (),
2025-01-06 10:22:23 -03:00
out SMBTransportType type )
? type
2025-02-17 14:01:08 +01:00
: throw new UserInformationException ( $"Transport must be one of: {string.Join(" , ", _transportMap.Keys)}" , "CIFSConfig" );
2025-03-20 12:02:25 +01:00
_timeouts = TimeoutOptionsHelper . Parse ( options );
2025-01-06 10:22:23 -03:00
_connectionParameters = new SMBConnectionParameters (
2025-08-07 08:06:24 +02:00
_DnsName ,
2025-01-06 10:22:23 -03:00
transportType ,
slashIndex >= 0 ? input [.. slashIndex ] : input ,
slashIndex >= 0 ? input [( slashIndex + 1 )..] : "" ,
authDomain ,
2025-03-20 12:02:25 +01:00
auth . Username ,
auth . Password ,
2025-01-10 13:53:51 -03:00
readBufferSize ,
writeBufferSize
2025-01-06 10:22:23 -03:00
);
}
/// <summary>
/// Implementation of interface property to return supported command parameters
/// </summary>
2025-01-08 22:25:34 -03:00
public IList < ICommandLineArgument > SupportedCommands =>
2025-03-20 12:02:25 +01:00
[
.. AuthOptionsHelper.GetOptions(),
2025-03-21 10:26:01 +01:00
new CommandLineArgument(AUTH_DOMAIN_OPTION, CommandLineArgument.ArgumentType.String, Strings.SMBBackend.DescriptionAuthDomainShort, Strings.SMBBackend.DescriptionAuthDomainLong),
2025-01-10 13:53:51 -03:00
new CommandLineArgument(TRANSPORT_OPTION, CommandLineArgument.ArgumentType.Enumeration, Strings.Options.TransportShort, Strings.Options.TransportLong, DEFAULT_TRANSPORT, null, _transportMap.Keys.ToArray()),
new CommandLineArgument(READ_BUFFER_SIZE_OPTION, CommandLineArgument.ArgumentType.String, Strings.Options.DescriptionReadBufferSizeShort, Strings.Options.DescriptionReadBufferSizeLong),
2025-03-20 12:02:25 +01:00
new CommandLineArgument(WRITE_BUFFER_SIZE_OPTION, CommandLineArgument.ArgumentType.String, Strings.Options.DescriptionWriteBufferSizeShort, Strings.Options.DescriptionWriteBufferSizeLong),
.. TimeoutOptionsHelper.GetOptions()
] ;
2025-01-08 22:25:34 -03:00
2025-01-06 10:22:23 -03:00
/// <summary>
/// Implementation of interface method for listing remote folder contents
/// </summary>
/// <returns>List of IFileEntry with directory listing result</returns>
2025-02-17 16:45:51 +01:00
public async IAsyncEnumerable < IFileEntry > ListAsync ([ EnumeratorCancellation ] CancellationToken cancellationToken )
2025-01-06 10:22:23 -03:00
{
2025-03-20 12:02:25 +01:00
var con = await GetConnectionAsync ( cancellationToken ). ConfigureAwait ( false );
foreach ( var v in await con . ListAsync ( _connectionParameters . Path , cancellationToken ). ConfigureAwait ( false ))
2025-02-17 16:45:51 +01:00
yield return v ;
2025-01-06 10:22:23 -03:00
}
/// <summary>
/// Upload files to remote location
/// </summary>
/// <param name="remotename">Filename at remote location</param>
/// <param name="localname">Filename to read from</param>
/// <param name="cancellationToken">CancellationToken that is combined with internal timeout token</param>
/// <exception cref="FileMissingException">FileMissingException when file is not found</exception>
/// <exception cref="Exception">Exceptions arising from either code execution</exception>
public async Task PutAsync ( string remotename , string localname , CancellationToken cancellationToken )
{
await using var fs = File . Open ( localname ,
FileMode . Open , FileAccess . Read , FileShare . Read );
await PutAsync ( remotename , fs , cancellationToken ). ConfigureAwait ( false );
}
/// <summary>
/// Upload files to remote location
/// </summary>
/// <param name="remotename">Filename at remote location</param>
/// <param name="input">Stream to read from</param>
/// <param name="cancellationToken">CancellationToken that is combined with internal timeout token</param>
/// <exception cref="FileMissingException">FileMissingException when file is not found</exception>
/// <exception cref="Exception">Exceptions arising from either code execution</exception>
public async Task PutAsync ( string remotename , Stream input , CancellationToken cancellationToken )
{
2025-03-20 12:02:25 +01:00
var con = await GetConnectionAsync ( cancellationToken ). ConfigureAwait ( false );
await con . PutAsync ( remotename , input , cancellationToken ). ConfigureAwait ( false );
2025-01-06 10:22:23 -03:00
}
/// <summary>
/// Download files from remote
/// </summary>
/// <param name="remotename">Filename at remote location</param>
/// <param name="localname">Local filename to write to</param>
/// <param name="cancellationToken">CancellationToken that is combined with internal timeout token</param>
/// <exception cref="FileMissingException">FileMissingException when file is not found</exception>
/// <exception cref="Exception">Exceptions arising from either code execution or FileMissingException</exception>
public async Task GetAsync ( string remotename , string localname , CancellationToken cancellationToken )
{
await using var fs = File . Open ( localname ,
FileMode . Create , FileAccess . Write ,
FileShare . None );
await GetAsync ( remotename , fs , cancellationToken ). ConfigureAwait ( false );
}
/// <summary>
/// Download files from remote
/// </summary>
/// <param name="remotename">Filename at remote location</param>
/// <param name="output">Destination stream to write to</param>
/// <param name="cancellationToken">CancellationToken that is combined with internal timeout token</param>
/// <exception cref="FileMissingException">FileMissingException when file is not found</exception>
/// <exception cref="Exception">Exceptions arising from either code execution or FileMissingException</exception>
public async Task GetAsync ( string remotename , Stream output , CancellationToken cancellationToken )
{
2025-03-20 12:02:25 +01:00
var con = await GetConnectionAsync ( cancellationToken ). ConfigureAwait ( false );
await con . GetAsync ( remotename , output , cancellationToken ). ConfigureAwait ( false );
2025-01-06 10:22:23 -03:00
}
/// <summary>
/// Delete remote file if it exists, if not, throws FileMissingException
/// </summary>
/// <param name="remotename">filename to be deleted on the remote</param>
/// <param name="cancellationToken">CancellationToken that is combined with internal timeout token</param>
/// <returns></returns>
/// <exception cref="FileMissingException">FileMissingException when file is not found</exception>
/// <exception cref="Exception">Exceptions arising from either code execution or business logic errors</exception>
public async Task DeleteAsync ( string remotename , CancellationToken cancellationToken )
{
2025-03-20 12:02:25 +01:00
var con = await GetConnectionAsync ( cancellationToken ). ConfigureAwait ( false );
await con . DeleteAsync ( remotename , cancellationToken ). ConfigureAwait ( false );
2025-01-06 10:22:23 -03:00
}
/// <summary>
/// Implementation of interface function to return hosnames used by the backend
/// </summary>
/// <param name="cancellationToken">CancellationToken, in this call not used.</param>
/// <returns></returns>
2025-02-17 14:01:08 +01:00
public Task < string []> GetDNSNamesAsync ( CancellationToken cancellationToken ) =>
2025-01-06 10:22:23 -03:00
Task . FromResult ( new [] { _DnsName ?? string . Empty });
/// <summary>
/// Tests backend connectivity by verifying the configured path exists
/// </summary>
/// <param name="cancellationToken">The cancellation token (not used)</param>
/// <exception cref="FolderMissingException">Thrown when configured path does not exist</exception>
2025-05-21 17:19:27 +02:00
public Task TestAsync ( CancellationToken cancellationToken )
=> this . TestReadWritePermissionsAsync ( cancellationToken );
2025-01-06 10:22:23 -03:00
/// <summary>
/// Creates the configured remote folder path if it doesn't exist
/// </summary>
/// <param name="cancellationToken">CancellationToken that will be combined with internal timeout token</param>
/// <returns>Task representing the asynchronous operation</returns>
/// <exception cref="Exception">Thrown when folder creation fails</exception>
public async Task CreateFolderAsync ( CancellationToken cancellationToken )
{
var pathParts = _connectionParameters . Path ?
. Split ( PATH_SEPARATORS , StringSplitOptions . RemoveEmptyEntries );
2025-02-17 14:01:08 +01:00
2025-01-06 10:22:23 -03:00
if ( pathParts == null || pathParts . Length == 0 )
return ;
2025-02-17 14:01:08 +01:00
2025-03-20 12:02:25 +01:00
var con = await GetConnectionAsync ( cancellationToken ). ConfigureAwait ( false );
await con . CreateFolderAsync ( _connectionParameters . Path ?? "" , cancellationToken )
2025-01-06 10:22:23 -03:00
. ConfigureAwait ( false );
}
/// <summary>
/// Gets or creates a shared SMB connection
/// </summary>
/// <returns>An SMB connection that can be used for file operations</returns>
2025-03-20 12:02:25 +01:00
private async Task < SMBShareConnection > GetConnectionAsync ( CancellationToken cancellationToken )
=> _sharedConnection ??= await SMBShareConnection . CreateAsync ( _connectionParameters , _timeouts , cancellationToken ). ConfigureAwait ( false );
2025-01-06 10:22:23 -03:00
2025-02-17 14:01:08 +01:00
2025-01-06 10:22:23 -03:00
/// <summary>
/// Implementation of Dispose pattern enforced by interface
/// </summary>
public void Dispose ()
{
try
{
2025-01-10 12:10:03 -03:00
_sharedConnection ?. Dispose ();
2025-01-06 10:22:23 -03:00
}
catch ( Exception ex )
{
// Log the exception but don't rethrow since we're in Dispose
System . Diagnostics . Debug . WriteLine ( $"Error disposing CIFS connection: {ex.Message}" );
}
}
2025-02-17 14:01:08 +01:00
/// <inheritdoc/>
2025-03-20 12:02:25 +01:00
public async IAsyncEnumerable < IFileEntry > ListAsync ( string? path , [ EnumeratorCancellation ] CancellationToken cancellationToken )
2025-02-17 14:01:08 +01:00
{
var sourcePath = _connectionParameters . Path ;
2025-02-21 16:06:55 +01:00
if (! string . IsNullOrWhiteSpace ( sourcePath ))
sourcePath = Util . AppendDirSeparator ( sourcePath , "/" );
2025-02-17 14:01:08 +01:00
2025-03-20 12:02:25 +01:00
var con = await GetConnectionAsync ( cancellationToken ). ConfigureAwait ( false );
foreach ( var v in await con . ListAsync ( sourcePath + BackendSourceFileEntry . NormalizePathTo ( path , '/' ), cancellationToken ). ConfigureAwait ( false ))
2025-02-21 16:06:55 +01:00
if ( v . Name != "." && v . Name != ".." )
yield return v ;
2025-02-17 14:01:08 +01:00
}
/// <inheritdoc/>
2026-01-27 13:08:20 +01:00
public async Task RenameAsync ( string oldname , string newname , CancellationToken cancellationToken )
{
var con = await GetConnectionAsync ( cancellationToken ). ConfigureAwait ( false );
await con . RenameAsync ( oldname , newname , cancellationToken ). ConfigureAwait ( false );
}
2026-01-30 14:40:13 +01:00
2025-02-17 14:01:08 +01:00
/// <inheritdoc/>
2026-01-28 14:37:51 +01:00
public async Task < IFileEntry ?> GetEntryAsync ( string path , CancellationToken cancellationToken )
{
var sourcePath = _connectionParameters . Path ;
if (! string . IsNullOrWhiteSpace ( sourcePath ))
sourcePath = Util . AppendDirSeparator ( sourcePath , "/" );
var con = await GetConnectionAsync ( cancellationToken ). ConfigureAwait ( false );
return await con . GetEntryAsync ( sourcePath + BackendSourceFileEntry . NormalizePathTo ( path , '/' ), cancellationToken ). ConfigureAwait ( false );
}
2025-01-06 10:22:23 -03:00
}