From 1465d7c36817f85e22aad790588ca2b6ef4ece4e Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Thu, 20 Mar 2025 12:02:25 +0100 Subject: [PATCH] Updated CIFS --- Duplicati/Library/Backend/CIFS/CIFS.cs | 92 ++++--- .../CIFS/Model/SMBConnectionParameters.cs | 100 ++------ .../Backend/CIFS/SMBShareConnection.cs | 234 +++++++++++------- Duplicati/Library/Backend/CIFS/Strings.cs | 4 - .../Builtin/BackendSourceFileEntry.cs | 4 +- 5 files changed, 217 insertions(+), 217 deletions(-) diff --git a/Duplicati/Library/Backend/CIFS/CIFS.cs b/Duplicati/Library/Backend/CIFS/CIFS.cs index 53ec3264e..04603ec74 100644 --- a/Duplicati/Library/Backend/CIFS/CIFS.cs +++ b/Duplicati/Library/Backend/CIFS/CIFS.cs @@ -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 /// /// Cache of parsed connection parameters /// - private SMBConnectionParameters _connectionParameters; + private readonly SMBConnectionParameters _connectionParameters; + + /// + /// Timeout options to be used in the backend + /// + private readonly TimeoutOptionsHelper.Timeouts _timeouts; /// /// Shared connection between all methods to avoid re-authentication /// - private SMBShareConnection _sharedConnection; + private SMBShareConnection? _sharedConnection; /// /// Read buffer size for SMB operations (will be capped automatically by SMB negotiated values) @@ -96,16 +103,6 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend /// private const string AUTH_DOMAIN_OPTION = "auth-domain"; - /// - /// Username for authentication - /// - private const string AUTH_USERNAME_OPTION = "auth-username"; - - /// - /// Password for authentication - /// - private const string AUTH_PASSWORD_OPTION = "auth-password"; - /// /// Defines the default transport to be used in CIFS connection /// @@ -125,6 +122,9 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend /// public CIFSBackend() { + _DnsName = null!; + _connectionParameters = null!; + _timeouts = null!; } /// @@ -132,7 +132,7 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend /// /// URL in Duplicati Uri format /// options to be used in the backend - public CIFSBackend(string url, Dictionary options) + public CIFSBackend(string url, Dictionary 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 /// public IList SupportedCommands => - new List([ - 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() + ]; /// /// Implementation of interface method for listing remote folder contents @@ -201,7 +203,8 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend /// List of IFileEntry with directory listing result public async IAsyncEnumerable 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 /// Exceptions arising from either code execution 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); } /// @@ -259,7 +263,8 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend /// Exceptions arising from either code execution or FileMissingException 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); } /// @@ -272,7 +277,8 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend /// Exceptions arising from either code execution or business logic errors 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); } /// @@ -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); } /// @@ -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 /// /// An SMB connection that can be used for file operations - private SMBShareConnection GetConnection() => _sharedConnection ??= new SMBShareConnection(_connectionParameters); + private async Task GetConnectionAsync(CancellationToken cancellationToken) + => _sharedConnection ??= await SMBShareConnection.CreateAsync(_connectionParameters, _timeouts, cancellationToken).ConfigureAwait(false); @@ -337,18 +346,19 @@ public class CIFSBackend : IStreamingBackend, IFolderEnabledBackend } /// - public async IAsyncEnumerable ListAsync(string path, [EnumeratorCancellation] CancellationToken cancellationToken) + public async IAsyncEnumerable 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; } /// - public Task GetEntryAsync(string path, CancellationToken cancellationToken) - => Task.FromResult(null); + public Task GetEntryAsync(string path, CancellationToken cancellationToken) + => Task.FromResult(null); } \ No newline at end of file diff --git a/Duplicati/Library/Backend/CIFS/Model/SMBConnectionParameters.cs b/Duplicati/Library/Backend/CIFS/Model/SMBConnectionParameters.cs index e2f687625..8fe1770b9 100644 --- a/Duplicati/Library/Backend/CIFS/Model/SMBConnectionParameters.cs +++ b/Duplicati/Library/Backend/CIFS/Model/SMBConnectionParameters.cs @@ -26,84 +26,22 @@ namespace Duplicati.Library.Backend.CIFS.Model; /// /// Connection parameters for establishing an SMB connection. /// -public record SMBConnectionParameters -{ - /// - /// The name or IP address of the SMB server - /// - public string ServerName { get; init; } - - /// - /// The transport protocol type used for SMB communication - /// - public SMBTransportType TransportType { get; init; } - - /// - /// The name of the network share to connect to - /// - public string ShareName { get; init; } - - /// - /// The path within the share to access - /// - public string Path { get; init; } - - /// - /// The authentication domain name - /// - public string AuthDomain { get; init; } - - /// - /// The username for authentication - /// - public string AuthUser { get; init; } - - /// - /// The password for authentication - /// - public string AuthPassword { get; init; } - - /// - /// Write buffer size for SMB operations (will be capped automatically by SMB negotiated values) - /// - public int? WriteBufferSize { get; init; } - - /// - /// Read buffer size for SMB operations (will be capped automatically by SMB negotiated values) - /// - public int? ReadBufferSize { get; init; } - - /// - /// Creates a new instance of SMB connection parameters - /// - /// The name or IP address of the SMB server - /// The transport protocol type used for SMB communication - /// The name of the network share to connect to - /// The path within the share to access - /// The authentication domain name - /// The username for authentication - /// The password for authentication - /// Read buffer size for SMB operations (will be capped automatically by SMB negotiated values) - /// Write buffer size for SMB operations (will be capped automatically by SMB negotiated values) - 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; - } -} \ No newline at end of file +/// The name or IP address of the SMB server +/// The transport protocol type used for SMB communication +/// The name of the network share to connect to +/// The path within the share to access +/// The authentication domain name +/// The username for authentication +/// The password for authentication +/// Read buffer size for SMB operations (will be capped automatically by SMB negotiated values) +/// Write buffer size for SMB operations (will be capped automatically by SMB negotiated values) +public sealed record SMBConnectionParameters( + string ServerName, + SMBTransportType TransportType, + string ShareName, + string Path, + string AuthDomain, + string AuthUser, + string AuthPassword, + int? ReadBufferSize, + int? WriteBufferSize); \ No newline at end of file diff --git a/Duplicati/Library/Backend/CIFS/SMBShareConnection.cs b/Duplicati/Library/Backend/CIFS/SMBShareConnection.cs index 47e23b742..0226811cd 100644 --- a/Duplicati/Library/Backend/CIFS/SMBShareConnection.cs +++ b/Duplicati/Library/Backend/CIFS/SMBShareConnection.cs @@ -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 /// /// SMBConnection client /// - private readonly SMB2Client _smb2Client = new(); + private readonly SMB2Client _smb2Client; /// /// Shared fileStore object. @@ -58,11 +60,19 @@ public class SMBShareConnection : IDisposable, IAsyncDisposable /// private readonly SMBConnectionParameters _connectionParameters; + /// + /// The timeouts to use for operations. + /// + private readonly TimeoutOptionsHelper.Timeouts _timeouts; + /// /// The semaphore to ensure that only one operation is performed at a time. /// private readonly SemaphoreSlim _semaphore = new(1, 1); + /// + /// Flag to indicate if the object has been disposed. + /// private bool _disposed; /// @@ -71,23 +81,51 @@ public class SMBShareConnection : IDisposable, IAsyncDisposable /// It throws specific exceptions for connection and authentication failures. /// /// Connection Parameters - /// Exception to be displayed to user - public SMBShareConnection(SMBConnectionParameters connectionParameters) + /// Timeouts to use for operations + 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)) + /// + /// Creates a new SMBShareConnection object asynchronously. + /// + /// The connection parameters + /// The timeouts to use for operations + /// The cancellation token + /// The SMBShareConnection object + /// Exception to be displayed to user + public static async Task 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); } /// @@ -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 fileList; - status = _smbFileStore.QueryDirectory( - out fileList, - directoryHandle, - "*", - FileInformationClass.FileDirectoryInformation); + List 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()); diff --git a/Duplicati/Library/Backend/CIFS/Strings.cs b/Duplicati/Library/Backend/CIFS/Strings.cs index 53699aa9a..2457dcf9d 100644 --- a/Duplicati/Library/Backend/CIFS/Strings.cs +++ b/Duplicati/Library/Backend/CIFS/Strings.cs @@ -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""."); diff --git a/Duplicati/Library/SourceProvider/Builtin/BackendSourceFileEntry.cs b/Duplicati/Library/SourceProvider/Builtin/BackendSourceFileEntry.cs index b9e134014..da1a31ef0 100644 --- a/Duplicati/Library/SourceProvider/Builtin/BackendSourceFileEntry.cs +++ b/Duplicati/Library/SourceProvider/Builtin/BackendSourceFileEntry.cs @@ -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 /// /// The path to normalize /// The normalized path - 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;