Updated CIFS
This commit is contained in:
@@ -19,6 +19,8 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using Duplicati.Library.Interface;
|
||||
using System.Collections.Generic;
|
||||
@@ -32,7 +34,7 @@ using SMBLibrary;
|
||||
using Duplicati.Library.SourceProvider;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Duplicati.Library.Common.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Duplicati.Library.Utility.Options;
|
||||
|
||||
namespace Duplicati.Library.Backend;
|
||||
|
||||
@@ -69,12 +71,17 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend
|
||||
/// <summary>
|
||||
/// Cache of parsed connection parameters
|
||||
/// </summary>
|
||||
private SMBConnectionParameters _connectionParameters;
|
||||
private readonly SMBConnectionParameters _connectionParameters;
|
||||
|
||||
/// <summary>
|
||||
/// Timeout options to be used in the backend
|
||||
/// </summary>
|
||||
private readonly TimeoutOptionsHelper.Timeouts _timeouts;
|
||||
|
||||
/// <summary>
|
||||
/// Shared connection between all methods to avoid re-authentication
|
||||
/// </summary>
|
||||
private SMBShareConnection _sharedConnection;
|
||||
private SMBShareConnection? _sharedConnection;
|
||||
|
||||
/// <summary>
|
||||
/// Read buffer size for SMB operations (will be capped automatically by SMB negotiated values)
|
||||
@@ -96,16 +103,6 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend
|
||||
/// </summary>
|
||||
private const string AUTH_DOMAIN_OPTION = "auth-domain";
|
||||
|
||||
/// <summary>
|
||||
/// Username for authentication
|
||||
/// </summary>
|
||||
private const string AUTH_USERNAME_OPTION = "auth-username";
|
||||
|
||||
/// <summary>
|
||||
/// Password for authentication
|
||||
/// </summary>
|
||||
private const string AUTH_PASSWORD_OPTION = "auth-password";
|
||||
|
||||
/// <summary>
|
||||
/// Defines the default transport to be used in CIFS connection
|
||||
/// </summary>
|
||||
@@ -125,6 +122,9 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend
|
||||
/// </summary>
|
||||
public CIFSBackend()
|
||||
{
|
||||
_DnsName = null!;
|
||||
_connectionParameters = null!;
|
||||
_timeouts = null!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -132,7 +132,7 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend
|
||||
/// </summary>
|
||||
/// <param name="url">URL in Duplicati Uri format</param>
|
||||
/// <param name="options">options to be used in the backend</param>
|
||||
public CIFSBackend(string url, Dictionary<string, string> options)
|
||||
public CIFSBackend(string url, Dictionary<string, string?> options)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url))
|
||||
throw new ArgumentNullException(nameof(url));
|
||||
@@ -146,37 +146,39 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend
|
||||
var input = uri.Path.TrimEnd('/');
|
||||
var slashIndex = input.IndexOf('/'); // Find first slash to separate server and share if present.
|
||||
|
||||
options.TryGetValue(AUTH_USERNAME_OPTION, out string authUsername);
|
||||
options.TryGetValue(AUTH_PASSWORD_OPTION, out string authPassword);
|
||||
options.TryGetValue(AUTH_DOMAIN_OPTION, out string authDomain);
|
||||
options.TryGetValue(TRANSPORT_OPTION, out string transport);
|
||||
var auth = AuthOptionsHelper.Parse(options, uri);
|
||||
var authDomain = options.GetValueOrDefault(AUTH_DOMAIN_OPTION);
|
||||
var transport = options.GetValueOrDefault(TRANSPORT_OPTION);
|
||||
|
||||
int? readBufferSize = null, writeBufferSize = null;
|
||||
|
||||
options.TryGetValue(READ_BUFFER_SIZE_OPTION, out string readBufferSizeConfig);
|
||||
if (!string.IsNullOrWhiteSpace(readBufferSizeConfig)) readBufferSize = Int32.TryParse(readBufferSizeConfig, out int value) ? value : null;
|
||||
var readBufferSizeConfig = options.GetValueOrDefault(READ_BUFFER_SIZE_OPTION);
|
||||
if (!string.IsNullOrWhiteSpace(readBufferSizeConfig))
|
||||
readBufferSize = int.TryParse(readBufferSizeConfig, out int value) ? value : null;
|
||||
|
||||
options.TryGetValue(WRITE_BUFFER_SIZE_OPTION, out string writeBufferSizeConfig);
|
||||
if (!string.IsNullOrWhiteSpace(writeBufferSizeConfig)) writeBufferSize = Int32.TryParse(readBufferSizeConfig, out int value) ? value : null;
|
||||
var writeBufferSizeConfig = options.GetValueOrDefault(WRITE_BUFFER_SIZE_OPTION);
|
||||
if (!string.IsNullOrWhiteSpace(writeBufferSizeConfig))
|
||||
writeBufferSize = int.TryParse(readBufferSizeConfig, out int value) ? value : null;
|
||||
|
||||
// Normalize to 10KB minimum buffers size
|
||||
readBufferSize = readBufferSize < 1024 * 10 ? null : readBufferSize;
|
||||
writeBufferSize = writeBufferSize < 1024 * 10 ? null : writeBufferSize;
|
||||
|
||||
SMBTransportType transportType = _transportMap.TryGetValue(
|
||||
var transportType = _transportMap.TryGetValue(
|
||||
string.IsNullOrEmpty(transport) ? DEFAULT_TRANSPORT : transport.ToLower(),
|
||||
out SMBTransportType type)
|
||||
? type
|
||||
: throw new UserInformationException($"Transport must be one of: {string.Join(", ", _transportMap.Keys)}", "CIFSConfig");
|
||||
|
||||
_timeouts = TimeoutOptionsHelper.Parse(options);
|
||||
_connectionParameters = new SMBConnectionParameters(
|
||||
uri.Host,
|
||||
transportType,
|
||||
slashIndex >= 0 ? input[..slashIndex] : input,
|
||||
slashIndex >= 0 ? input[(slashIndex + 1)..] : "",
|
||||
authDomain,
|
||||
authUsername,
|
||||
authPassword,
|
||||
auth.Username,
|
||||
auth.Password,
|
||||
readBufferSize,
|
||||
writeBufferSize
|
||||
);
|
||||
@@ -186,14 +188,14 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend
|
||||
/// Implementation of interface property to return supported command parameters
|
||||
/// </summary>
|
||||
public IList<ICommandLineArgument> SupportedCommands =>
|
||||
new List<ICommandLineArgument>([
|
||||
new CommandLineArgument(AUTH_PASSWORD_OPTION, CommandLineArgument.ArgumentType.Password, Strings.CIFSBackend.DescriptionAuthPasswordShort, Strings.CIFSBackend.DescriptionAuthPasswordLong),
|
||||
new CommandLineArgument(AUTH_USERNAME_OPTION, CommandLineArgument.ArgumentType.String, Strings.CIFSBackend.DescriptionAuthUsernameShort, Strings.CIFSBackend.DescriptionAuthUsernameLong),
|
||||
[
|
||||
.. AuthOptionsHelper.GetOptions(),
|
||||
new CommandLineArgument(AUTH_DOMAIN_OPTION, CommandLineArgument.ArgumentType.String, Strings.CIFSBackend.DescriptionAuthDomainShort, Strings.CIFSBackend.DescriptionAuthDomainLong),
|
||||
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),
|
||||
new CommandLineArgument(WRITE_BUFFER_SIZE_OPTION, CommandLineArgument.ArgumentType.String, Strings.Options.DescriptionWriteBufferSizeShort, Strings.Options.DescriptionWriteBufferSizeLong)
|
||||
]);
|
||||
new CommandLineArgument(WRITE_BUFFER_SIZE_OPTION, CommandLineArgument.ArgumentType.String, Strings.Options.DescriptionWriteBufferSizeShort, Strings.Options.DescriptionWriteBufferSizeLong),
|
||||
.. TimeoutOptionsHelper.GetOptions()
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of interface method for listing remote folder contents
|
||||
@@ -201,7 +203,8 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend
|
||||
/// <returns>List of IFileEntry with directory listing result</returns>
|
||||
public async IAsyncEnumerable<IFileEntry> ListAsync([EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var v in await GetConnection().ListAsync(_connectionParameters.Path, cancellationToken).ConfigureAwait(false))
|
||||
var con = await GetConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
foreach (var v in await con.ListAsync(_connectionParameters.Path, cancellationToken).ConfigureAwait(false))
|
||||
yield return v;
|
||||
}
|
||||
|
||||
@@ -230,7 +233,8 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend
|
||||
/// <exception cref="Exception">Exceptions arising from either code execution</exception>
|
||||
public async Task PutAsync(string remotename, Stream input, CancellationToken cancellationToken)
|
||||
{
|
||||
await GetConnection().PutAsync(remotename, input, cancellationToken).ConfigureAwait(false);
|
||||
var con = await GetConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await con.PutAsync(remotename, input, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -259,7 +263,8 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend
|
||||
/// <exception cref="Exception">Exceptions arising from either code execution or FileMissingException</exception>
|
||||
public async Task GetAsync(string remotename, Stream output, CancellationToken cancellationToken)
|
||||
{
|
||||
await GetConnection().GetAsync(remotename, output, cancellationToken).ConfigureAwait(false);
|
||||
var con = await GetConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await con.GetAsync(remotename, output, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -272,7 +277,8 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend
|
||||
/// <exception cref="Exception">Exceptions arising from either code execution or business logic errors</exception>
|
||||
public async Task DeleteAsync(string remotename, CancellationToken cancellationToken)
|
||||
{
|
||||
await GetConnection().DeleteAsync(remotename, cancellationToken).ConfigureAwait(false);
|
||||
var con = await GetConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await con.DeleteAsync(remotename, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -291,7 +297,8 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend
|
||||
public async Task TestAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// This will throw an exception if the folder is missing
|
||||
await GetConnection().ListAsync(_connectionParameters.Path, cancellationToken).ConfigureAwait(false);
|
||||
var con = await GetConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await con.ListAsync(_connectionParameters.Path, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -308,7 +315,8 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend
|
||||
if (pathParts == null || pathParts.Length == 0)
|
||||
return;
|
||||
|
||||
await GetConnection().CreateFolderAsync(_connectionParameters.Path, cancellationToken)
|
||||
var con = await GetConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await con.CreateFolderAsync(_connectionParameters.Path ?? "", cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -316,7 +324,8 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend
|
||||
/// Gets or creates a shared SMB connection
|
||||
/// </summary>
|
||||
/// <returns>An SMB connection that can be used for file operations</returns>
|
||||
private SMBShareConnection GetConnection() => _sharedConnection ??= new SMBShareConnection(_connectionParameters);
|
||||
private async Task<SMBShareConnection> GetConnectionAsync(CancellationToken cancellationToken)
|
||||
=> _sharedConnection ??= await SMBShareConnection.CreateAsync(_connectionParameters, _timeouts, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
||||
|
||||
@@ -337,18 +346,19 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<IFileEntry> ListAsync(string path, [EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
public async IAsyncEnumerable<IFileEntry> ListAsync(string? path, [EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
var sourcePath = _connectionParameters.Path;
|
||||
if (!string.IsNullOrWhiteSpace(sourcePath))
|
||||
sourcePath = Util.AppendDirSeparator(sourcePath, "/");
|
||||
|
||||
foreach (var v in await GetConnection().ListAsync(sourcePath + BackendSourceFileEntry.NormalizePathTo(path, '/'), cancellationToken).ConfigureAwait(false))
|
||||
var con = await GetConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
foreach (var v in await con.ListAsync(sourcePath + BackendSourceFileEntry.NormalizePathTo(path, '/'), cancellationToken).ConfigureAwait(false))
|
||||
if (v.Name != "." && v.Name != "..")
|
||||
yield return v;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<IFileEntry> GetEntryAsync(string path, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<IFileEntry>(null);
|
||||
public Task<IFileEntry?> GetEntryAsync(string path, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<IFileEntry?>(null);
|
||||
}
|
||||
@@ -26,84 +26,22 @@ namespace Duplicati.Library.Backend.CIFS.Model;
|
||||
/// <summary>
|
||||
/// Connection parameters for establishing an SMB connection.
|
||||
/// </summary>
|
||||
public record SMBConnectionParameters
|
||||
{
|
||||
/// <summary>
|
||||
/// The name or IP address of the SMB server
|
||||
/// </summary>
|
||||
public string ServerName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The transport protocol type used for SMB communication
|
||||
/// </summary>
|
||||
public SMBTransportType TransportType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the network share to connect to
|
||||
/// </summary>
|
||||
public string ShareName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The path within the share to access
|
||||
/// </summary>
|
||||
public string Path { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The authentication domain name
|
||||
/// </summary>
|
||||
public string AuthDomain { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The username for authentication
|
||||
/// </summary>
|
||||
public string AuthUser { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The password for authentication
|
||||
/// </summary>
|
||||
public string AuthPassword { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Write buffer size for SMB operations (will be capped automatically by SMB negotiated values)
|
||||
/// </summary>
|
||||
public int? WriteBufferSize { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Read buffer size for SMB operations (will be capped automatically by SMB negotiated values)
|
||||
/// </summary>
|
||||
public int? ReadBufferSize { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of SMB connection parameters
|
||||
/// </summary>
|
||||
/// <param name="serverName">The name or IP address of the SMB server</param>
|
||||
/// <param name="transportType">The transport protocol type used for SMB communication</param>
|
||||
/// <param name="shareName">The name of the network share to connect to</param>
|
||||
/// <param name="path">The path within the share to access</param>
|
||||
/// <param name="authDomain">The authentication domain name</param>
|
||||
/// <param name="authUser">The username for authentication</param>
|
||||
/// <param name="authPassword">The password for authentication</param>
|
||||
/// <param name="readBufferSize">Read buffer size for SMB operations (will be capped automatically by SMB negotiated values)</param>
|
||||
/// <param name="writeBufferSize">Write buffer size for SMB operations (will be capped automatically by SMB negotiated values)</param>
|
||||
public SMBConnectionParameters(
|
||||
string serverName,
|
||||
SMBTransportType transportType,
|
||||
string shareName,
|
||||
string path,
|
||||
string authDomain,
|
||||
string authUser,
|
||||
string authPassword,
|
||||
int? readBufferSize,
|
||||
int? writeBufferSize)
|
||||
{
|
||||
ServerName = serverName;
|
||||
TransportType = transportType;
|
||||
ShareName = shareName;
|
||||
Path = path;
|
||||
AuthDomain = authDomain;
|
||||
AuthUser = authUser;
|
||||
AuthPassword = authPassword;
|
||||
ReadBufferSize = readBufferSize;
|
||||
WriteBufferSize = writeBufferSize;
|
||||
}
|
||||
}
|
||||
/// <param name="ServerName">The name or IP address of the SMB server</param>
|
||||
/// <param name="TransportType">The transport protocol type used for SMB communication</param>
|
||||
/// <param name="ShareName">The name of the network share to connect to</param>
|
||||
/// <param name="Path">The path within the share to access</param>
|
||||
/// <param name="AuthDomain">The authentication domain name</param>
|
||||
/// <param name="AuthUser">The username for authentication</param>
|
||||
/// <param name="AuthPassword">The password for authentication</param>
|
||||
/// <param name="ReadBufferSize">Read buffer size for SMB operations (will be capped automatically by SMB negotiated values)</param>
|
||||
/// <param name="WriteBufferSize">Write buffer size for SMB operations (will be capped automatically by SMB negotiated values)</param>
|
||||
public sealed record SMBConnectionParameters(
|
||||
string ServerName,
|
||||
SMBTransportType TransportType,
|
||||
string ShareName,
|
||||
string Path,
|
||||
string AuthDomain,
|
||||
string AuthUser,
|
||||
string AuthPassword,
|
||||
int? ReadBufferSize,
|
||||
int? WriteBufferSize);
|
||||
@@ -31,6 +31,8 @@ using Duplicati.Library.Backend.CIFS.Model;
|
||||
using Duplicati.Library.Common.IO;
|
||||
using Duplicati.Library.Interface;
|
||||
using Duplicati.Library.Localization.Short;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Library.Utility.Options;
|
||||
using SMBLibrary;
|
||||
using SMBLibrary.Client;
|
||||
using FileAttributes = SMBLibrary.FileAttributes;
|
||||
@@ -46,7 +48,7 @@ public class SMBShareConnection : IDisposable, IAsyncDisposable
|
||||
/// <summary>
|
||||
/// SMBConnection client
|
||||
/// </summary>
|
||||
private readonly SMB2Client _smb2Client = new();
|
||||
private readonly SMB2Client _smb2Client;
|
||||
|
||||
/// <summary>
|
||||
/// Shared fileStore object.
|
||||
@@ -58,11 +60,19 @@ public class SMBShareConnection : IDisposable, IAsyncDisposable
|
||||
/// </summary>
|
||||
private readonly SMBConnectionParameters _connectionParameters;
|
||||
|
||||
/// <summary>
|
||||
/// The timeouts to use for operations.
|
||||
/// </summary>
|
||||
private readonly TimeoutOptionsHelper.Timeouts _timeouts;
|
||||
|
||||
/// <summary>
|
||||
/// The semaphore to ensure that only one operation is performed at a time.
|
||||
/// </summary>
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Flag to indicate if the object has been disposed.
|
||||
/// </summary>
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
@@ -71,23 +81,51 @@ public class SMBShareConnection : IDisposable, IAsyncDisposable
|
||||
/// It throws specific exceptions for connection and authentication failures.
|
||||
/// </summary>
|
||||
/// <param name="connectionParameters">Connection Parameters</param>
|
||||
/// <exception cref="UserInformationException">Exception to be displayed to user</exception>
|
||||
public SMBShareConnection(SMBConnectionParameters connectionParameters)
|
||||
/// <param name="timeouts">Timeouts to use for operations</param>
|
||||
private SMBShareConnection(SMB2Client client, ISMBFileStore fileStore, SMBConnectionParameters connectionParameters, TimeoutOptionsHelper.Timeouts timeouts)
|
||||
{
|
||||
_connectionParameters = connectionParameters;
|
||||
_timeouts = timeouts;
|
||||
_smbFileStore = fileStore;
|
||||
_smb2Client = client;
|
||||
}
|
||||
|
||||
if (!_smb2Client.Connect(connectionParameters.ServerName, connectionParameters.TransportType))
|
||||
/// <summary>
|
||||
/// Creates a new SMBShareConnection object asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="connectionParameters">The connection parameters</param>
|
||||
/// <param name="timeouts">The timeouts to use for operations</param>
|
||||
/// <param name="cancellationToken">The cancellation token</param>
|
||||
/// <returns>The SMBShareConnection object</returns>
|
||||
/// <exception cref="UserInformationException">Exception to be displayed to user</exception>
|
||||
public static async Task<SMBShareConnection> CreateAsync(SMBConnectionParameters connectionParameters, TimeoutOptionsHelper.Timeouts timeouts, CancellationToken cancellationToken)
|
||||
{
|
||||
var client = new SMB2Client();
|
||||
var connected = await Utility.Utility.WithTimeout(timeouts.ShortTimeout, cancellationToken, _ =>
|
||||
client.Connect(connectionParameters.ServerName, connectionParameters.TransportType)
|
||||
).ConfigureAwait(false);
|
||||
|
||||
if (!connected)
|
||||
throw new UserInformationException($"{LC.L("Failed to connect to server")} {connectionParameters.ServerName}", "ConnectionError");
|
||||
|
||||
var status = _smb2Client.Login(connectionParameters.AuthDomain ?? "", connectionParameters.AuthUser ?? "", connectionParameters.AuthPassword ?? "");
|
||||
var status = await Utility.Utility.WithTimeout(timeouts.ShortTimeout, cancellationToken, _ =>
|
||||
client.Login(connectionParameters.AuthDomain ?? "", connectionParameters.AuthUser ?? "", connectionParameters.AuthPassword ?? "")
|
||||
).ConfigureAwait(false);
|
||||
|
||||
if (status != NTStatus.STATUS_SUCCESS)
|
||||
throw new UserInformationException($"{LC.L("Failed to authenticate to server")} {connectionParameters.ServerName} with status {status}", "ConnectionError");
|
||||
|
||||
_smbFileStore = _smb2Client.TreeConnect(connectionParameters.ShareName, out status);
|
||||
(var res, status) = await Utility.Utility.WithTimeout(timeouts.ShortTimeout, cancellationToken, _ =>
|
||||
{
|
||||
var res = client.TreeConnect(connectionParameters.ShareName, out status);
|
||||
return (res, status);
|
||||
}
|
||||
).ConfigureAwait(false);
|
||||
|
||||
if (status != NTStatus.STATUS_SUCCESS)
|
||||
if (status != NTStatus.STATUS_SUCCESS || res == null)
|
||||
throw new UserInformationException($"{LC.L("Failed to connect to share")} {connectionParameters.ShareName} with status {status}", "ConnectionError");
|
||||
|
||||
return new SMBShareConnection(client, res, connectionParameters, timeouts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -101,33 +139,33 @@ public class SMBShareConnection : IDisposable, IAsyncDisposable
|
||||
await _semaphore.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
NTStatus status;
|
||||
object fileHandle;
|
||||
FileStatus fileStatus;
|
||||
status = _smbFileStore.CreateFile(out fileHandle, out fileStatus, NormalizeSlashes(Path.Combine(_connectionParameters.Path, fileName)),
|
||||
AccessMask.GENERIC_WRITE | AccessMask.DELETE | AccessMask.SYNCHRONIZE,
|
||||
FileAttributes.Normal,
|
||||
ShareAccess.None,
|
||||
CreateDisposition.FILE_OPEN,
|
||||
CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_ALERT,
|
||||
null);
|
||||
|
||||
if (status == NTStatus.STATUS_OBJECT_NAME_NOT_FOUND)
|
||||
throw new FileMissingException();
|
||||
|
||||
if (status == NTStatus.STATUS_SUCCESS)
|
||||
await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, cancellationToken, _ =>
|
||||
{
|
||||
var fileDispositionInformation = new FileDispositionInformation
|
||||
var status = _smbFileStore.CreateFile(out var fileHandle, out var fileStatus, NormalizeSlashes(Path.Combine(_connectionParameters.Path, fileName)),
|
||||
AccessMask.GENERIC_WRITE | AccessMask.DELETE | AccessMask.SYNCHRONIZE,
|
||||
FileAttributes.Normal,
|
||||
ShareAccess.None,
|
||||
CreateDisposition.FILE_OPEN,
|
||||
CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_ALERT,
|
||||
null);
|
||||
|
||||
if (status == NTStatus.STATUS_OBJECT_NAME_NOT_FOUND)
|
||||
throw new FileMissingException();
|
||||
|
||||
if (status == NTStatus.STATUS_SUCCESS)
|
||||
{
|
||||
DeletePending = true
|
||||
};
|
||||
status = _smbFileStore.SetFileInformation(fileHandle, fileDispositionInformation);
|
||||
if (status != NTStatus.STATUS_SUCCESS)
|
||||
throw new UserInformationException($"{LC.L("Failed to delete file on DeleteAsync")} with status {status}", "DeleteFileError");
|
||||
status = _smbFileStore.CloseFile(fileHandle);
|
||||
if (status != NTStatus.STATUS_SUCCESS)
|
||||
throw new UserInformationException($"{LC.L("Failed to close file on DeleteAsync")} with status {status}", "CloseFileError");
|
||||
}
|
||||
var fileDispositionInformation = new FileDispositionInformation
|
||||
{
|
||||
DeletePending = true
|
||||
};
|
||||
status = _smbFileStore.SetFileInformation(fileHandle, fileDispositionInformation);
|
||||
if (status != NTStatus.STATUS_SUCCESS)
|
||||
throw new UserInformationException($"{LC.L("Failed to delete file on DeleteAsync")} with status {status}", "DeleteFileError");
|
||||
status = _smbFileStore.CloseFile(fileHandle);
|
||||
if (status != NTStatus.STATUS_SUCCESS)
|
||||
throw new UserInformationException($"{LC.L("Failed to close file on DeleteAsync")} with status {status}", "CloseFileError");
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -147,10 +185,10 @@ public class SMBShareConnection : IDisposable, IAsyncDisposable
|
||||
try
|
||||
{
|
||||
// Normalize path separators to forward slashes and trim any trailing separators
|
||||
string linuxNormalizedPath = path.Replace('/', '\\').TrimEnd('\\');
|
||||
string currentPath = "";
|
||||
var linuxNormalizedPath = path.Replace('/', '\\').TrimEnd('\\');
|
||||
var currentPath = "";
|
||||
|
||||
foreach (string part in linuxNormalizedPath.Split('\\', StringSplitOptions.RemoveEmptyEntries))
|
||||
foreach (var part in linuxNormalizedPath.Split('\\', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(part) || part == ".")
|
||||
continue;
|
||||
@@ -159,20 +197,23 @@ public class SMBShareConnection : IDisposable, IAsyncDisposable
|
||||
object? fileHandle = null;
|
||||
try
|
||||
{
|
||||
NTStatus status = _smbFileStore.CreateFile(
|
||||
out fileHandle,
|
||||
out FileStatus fileStatus,
|
||||
currentPath,
|
||||
AccessMask.GENERIC_WRITE | AccessMask.SYNCHRONIZE,
|
||||
FileAttributes.Normal,
|
||||
ShareAccess.None,
|
||||
CreateDisposition.FILE_CREATE,
|
||||
CreateOptions.FILE_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_ALERT,
|
||||
null
|
||||
);
|
||||
if (status != NTStatus.STATUS_SUCCESS &&
|
||||
status != NTStatus.STATUS_OBJECT_NAME_COLLISION) // Ignore if directory already exists
|
||||
throw new UserInformationException($"{LC.L("Failed to create directory")} {currentPath} with status{status}", "CreateDirectoryError");
|
||||
await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, cancellationToken, _ =>
|
||||
{
|
||||
var status = _smbFileStore.CreateFile(
|
||||
out fileHandle,
|
||||
out var fileStatus,
|
||||
currentPath,
|
||||
AccessMask.GENERIC_WRITE | AccessMask.SYNCHRONIZE,
|
||||
FileAttributes.Normal,
|
||||
ShareAccess.None,
|
||||
CreateDisposition.FILE_CREATE,
|
||||
CreateOptions.FILE_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_ALERT,
|
||||
null
|
||||
);
|
||||
if (status != NTStatus.STATUS_SUCCESS &&
|
||||
status != NTStatus.STATUS_OBJECT_NAME_COLLISION) // Ignore if directory already exists
|
||||
throw new UserInformationException($"{LC.L("Failed to create directory")} {currentPath} with status{status}", "CreateDirectoryError");
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -207,32 +248,37 @@ public class SMBShareConnection : IDisposable, IAsyncDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
var status = _smbFileStore.CreateFile(
|
||||
out directoryHandle,
|
||||
out fileStatus,
|
||||
NormalizeSlashes(path),
|
||||
AccessMask.GENERIC_READ,
|
||||
FileAttributes.Directory,
|
||||
ShareAccess.Read | ShareAccess.Write,
|
||||
CreateDisposition.FILE_OPEN,
|
||||
CreateOptions.FILE_DIRECTORY_FILE,
|
||||
null);
|
||||
var fileList = await Utility.Utility.WithTimeout(_timeouts.ListTimeout, cancellationToken, _ =>
|
||||
{
|
||||
var status = _smbFileStore.CreateFile(
|
||||
out directoryHandle,
|
||||
out fileStatus,
|
||||
NormalizeSlashes(path),
|
||||
AccessMask.GENERIC_READ,
|
||||
FileAttributes.Directory,
|
||||
ShareAccess.Read | ShareAccess.Write,
|
||||
CreateDisposition.FILE_OPEN,
|
||||
CreateOptions.FILE_DIRECTORY_FILE,
|
||||
null);
|
||||
|
||||
if (status != NTStatus.STATUS_SUCCESS && fileStatus != FileStatus.FILE_OPENED)
|
||||
if (status == NTStatus.STATUS_OBJECT_PATH_NOT_FOUND || status == NTStatus.STATUS_OBJECT_NAME_NOT_FOUND)
|
||||
throw new FolderMissingException();
|
||||
else
|
||||
throw new UserInformationException($"{LC.L("Failed to open directory")} {NormalizeSlashes(path)} with status {status}", "DirectoryOpenError");
|
||||
if (status != NTStatus.STATUS_SUCCESS && fileStatus != FileStatus.FILE_OPENED)
|
||||
if (status == NTStatus.STATUS_OBJECT_PATH_NOT_FOUND || status == NTStatus.STATUS_OBJECT_NAME_NOT_FOUND)
|
||||
throw new FolderMissingException();
|
||||
else
|
||||
throw new UserInformationException($"{LC.L("Failed to open directory")} {NormalizeSlashes(path)} with status {status}", "DirectoryOpenError");
|
||||
|
||||
List<QueryDirectoryFileInformation> fileList;
|
||||
status = _smbFileStore.QueryDirectory(
|
||||
out fileList,
|
||||
directoryHandle,
|
||||
"*",
|
||||
FileInformationClass.FileDirectoryInformation);
|
||||
List<QueryDirectoryFileInformation> fileList;
|
||||
status = _smbFileStore.QueryDirectory(
|
||||
out fileList,
|
||||
directoryHandle,
|
||||
"*",
|
||||
FileInformationClass.FileDirectoryInformation);
|
||||
|
||||
if (status != NTStatus.STATUS_NO_MORE_FILES)
|
||||
throw new UserInformationException($"{LC.L("Failed to query directory contents")} with status {status}", "DirectoryQueryError");
|
||||
if (status != NTStatus.STATUS_NO_MORE_FILES)
|
||||
throw new UserInformationException($"{LC.L("Failed to query directory contents")} with status {status}", "DirectoryQueryError");
|
||||
|
||||
return fileList;
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
return
|
||||
[
|
||||
@@ -279,18 +325,22 @@ public class SMBShareConnection : IDisposable, IAsyncDisposable
|
||||
|
||||
try
|
||||
{
|
||||
object? fileHandle;
|
||||
FileStatus fileStatus;
|
||||
NTStatus status = _smbFileStore.CreateFile(out fileHandle, out fileStatus,
|
||||
(var status, var fileStatus, var fileHandle) = await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, cancellationToken, _ =>
|
||||
{
|
||||
var status = _smbFileStore.CreateFile(out var fileHandle, out var fileStatus,
|
||||
NormalizeSlashes(Path.Combine(_connectionParameters.Path, filename)),
|
||||
AccessMask.GENERIC_READ | AccessMask.SYNCHRONIZE, FileAttributes.Normal, ShareAccess.Read,
|
||||
CreateDisposition.FILE_OPEN,
|
||||
CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_ALERT, null);
|
||||
return (status, fileStatus, fileHandle);
|
||||
}
|
||||
).ConfigureAwait(false);
|
||||
|
||||
if (status == NTStatus.STATUS_SUCCESS || fileStatus != FileStatus.FILE_DOES_NOT_EXIST)
|
||||
{
|
||||
byte[] data;
|
||||
long bytesRead = 0;
|
||||
using var timeoutStream = destinationStream.ObserveWriteTimeout(_timeouts.ReadWriteTimeout, false);
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Use the provided read buffer size if set, otherwise use the protocol negotiated maximum. Never exceed the negotiated maximum.
|
||||
@@ -303,10 +353,10 @@ public class SMBShareConnection : IDisposable, IAsyncDisposable
|
||||
break;
|
||||
|
||||
bytesRead += data.Length;
|
||||
await destinationStream.WriteAsync(data, 0, data.Length, cancellationToken);
|
||||
await timeoutStream.WriteAsync(data, 0, data.Length, cancellationToken);
|
||||
}
|
||||
|
||||
await destinationStream.FlushAsync(cancellationToken);
|
||||
await timeoutStream.FlushAsync(cancellationToken);
|
||||
|
||||
if (fileHandle != null)
|
||||
{
|
||||
@@ -321,7 +371,7 @@ public class SMBShareConnection : IDisposable, IAsyncDisposable
|
||||
throw new FolderMissingException();
|
||||
else
|
||||
throw new UserInformationException(
|
||||
$"{LC.L("Failed to open file with error")} {filename} with status {status.ToString()}",
|
||||
$"{LC.L("Failed to open file with error")} {filename} with status {status}",
|
||||
"FileOpenError");
|
||||
}
|
||||
finally
|
||||
@@ -343,25 +393,29 @@ public class SMBShareConnection : IDisposable, IAsyncDisposable
|
||||
await _semaphore.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
object fileHandle;
|
||||
FileStatus fileStatus;
|
||||
NTStatus status = _smbFileStore.CreateFile(out fileHandle, out fileStatus,
|
||||
NormalizeSlashes(Path.Combine(_connectionParameters.Path, filename)),
|
||||
AccessMask.GENERIC_WRITE | AccessMask.SYNCHRONIZE,
|
||||
FileAttributes.Normal, ShareAccess.None,
|
||||
CreateDisposition.FILE_SUPERSEDE,
|
||||
CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_ALERT,
|
||||
null);
|
||||
(var status, var fileStatus, var fileHandle) = await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, cancellationToken, _ =>
|
||||
{
|
||||
var status = _smbFileStore.CreateFile(out var fileHandle, out var fileStatus,
|
||||
NormalizeSlashes(Path.Combine(_connectionParameters.Path, filename)),
|
||||
AccessMask.GENERIC_WRITE | AccessMask.SYNCHRONIZE,
|
||||
FileAttributes.Normal, ShareAccess.None,
|
||||
CreateDisposition.FILE_SUPERSEDE,
|
||||
CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_ALERT,
|
||||
null);
|
||||
return (status, fileStatus, fileHandle);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (status == NTStatus.STATUS_SUCCESS)
|
||||
{
|
||||
// Use the provided write buffer size if set, otherwise use the protocol negotiated maximum. Never exceed the negotiated maximum.
|
||||
byte[] buffer = new byte[Math.Min(_connectionParameters.WriteBufferSize ?? (int)_smb2Client.MaxWriteSize, _smb2Client.MaxWriteSize)];
|
||||
var buffer = new byte[Math.Min(_connectionParameters.WriteBufferSize ?? (int)_smb2Client.MaxWriteSize, _smb2Client.MaxWriteSize)];
|
||||
int bytesRead;
|
||||
int numberOfBytesWritten;
|
||||
int offset = 0;
|
||||
while (!cancellationToken.IsCancellationRequested && sourceStream.Position < sourceStream.Length)
|
||||
using var timeoutStream = sourceStream.ObserveReadTimeout(_timeouts.ReadWriteTimeout, false);
|
||||
while (!cancellationToken.IsCancellationRequested && timeoutStream.Position < timeoutStream.Length)
|
||||
{
|
||||
bytesRead = await sourceStream.ReadAsync(buffer, cancellationToken);
|
||||
bytesRead = await timeoutStream.ReadAsync(buffer, cancellationToken);
|
||||
if (bytesRead == 0)
|
||||
break;
|
||||
status = _smbFileStore.WriteFile(out numberOfBytesWritten, fileHandle, offset, buffer.Take(bytesRead).ToArray());
|
||||
|
||||
@@ -26,10 +26,6 @@ namespace Duplicati.Library.Backend.Strings
|
||||
internal static class CIFSBackend
|
||||
{
|
||||
|
||||
public static string DescriptionAuthPasswordLong => LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD"".");
|
||||
public static string DescriptionAuthPasswordShort => LC.L(@"Supply the password used to connect to the server");
|
||||
public static string DescriptionAuthUsernameLong => LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME"".");
|
||||
public static string DescriptionAuthUsernameShort => LC.L(@"Supply the username used to connect to the server");
|
||||
public static string DescriptionAuthDomainLong => LC.L(@"The domain used to connect to the server. This may also be supplied as the environment variable ""AUTH_DOMAIN"".");
|
||||
public static string DescriptionAuthDomainShort => LC.L(@"Supply the domain used to connect to the server");
|
||||
public static string Description => LC.L(@"This backend can read and write data to CIFS/SMB destinations. Allowed format is ""cifs://server/share"".");
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO.Pipelines;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Duplicati.Library.Common.IO;
|
||||
@@ -322,7 +323,8 @@ public class BackendSourceFileEntry(BackendSourceProvider parent, string path, b
|
||||
/// </summary>
|
||||
/// <param name="path">The path to normalize</param>
|
||||
/// <returns>The normalized path</returns>
|
||||
public static string NormalizePathTo(string path, char separator)
|
||||
[return: NotNullIfNotNull("path")]
|
||||
public static string? NormalizePathTo(string? path, char separator)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return path;
|
||||
|
||||
Reference in New Issue
Block a user