Updated CIFS

This commit is contained in:
Kenneth Skovhede
2025-03-20 12:02:25 +01:00
parent be8de849fb
commit 1465d7c368
5 changed files with 217 additions and 217 deletions
+51 -41
View File
@@ -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;