Made the SMBShareConnection slightly more robust. Introduced v2 List API to S3 backend, currently it retains v1 as the default. Changed S3 backends to not recurse, potentially making smaller listings. This option can be disabled if it breaks things. Added support for relative paths in the SSH backend.
284 lines
9.5 KiB
C#
284 lines
9.5 KiB
C#
// Copyright (C) 2025, The Duplicati Team
|
|
// https://duplicati.com, hello@duplicati.com
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
// copy of this software and associated documentation files (the "Software"),
|
|
// to deal in the Software without restriction, including without limitation
|
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
// and/or sell copies of the Software, and to permit persons to whom the
|
|
// Software is furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
// DEALINGS IN THE SOFTWARE.
|
|
|
|
using System.IO.Pipelines;
|
|
using Duplicati.Library.Interface;
|
|
using Duplicati.Library.Utility;
|
|
|
|
namespace Duplicati.Library.SourceProvider;
|
|
|
|
/// <summary>
|
|
/// Creates a new backend source entry
|
|
/// </summary>
|
|
/// <param name="parent">The parent backend</param>
|
|
/// <param name="path">The path of the entry</param>
|
|
/// <param name="isFolder">True if the entry is a folder</param>
|
|
/// <param name="isMetaEntry">True if the entry is a meta entry</param>
|
|
/// <param name="createdUtc">The creation time of the entry</param>
|
|
/// <param name="lastModificationUtc">The last modification time of the entry</param>
|
|
/// <param name="size">The size of the entry</param>
|
|
public class BackendSourceFileEntry(BackendSourceProvider parent, string path, bool isFolder, bool isMetaEntry, DateTime createdUtc, DateTime lastModificationUtc, long size)
|
|
: ISourceFileEntry
|
|
{
|
|
/// <summary>
|
|
/// The log tag for this instance
|
|
/// </summary>
|
|
private static string LOGTAG = Logging.Log.LogTagFromType<BackendSourceFileEntry>();
|
|
|
|
/// <inheritdoc/>
|
|
public bool IsFolder => isFolder;
|
|
|
|
/// <inheritdoc/>
|
|
public bool IsMetaEntry => isMetaEntry;
|
|
|
|
/// <inheritdoc/>
|
|
public bool IsRootEntry => isMetaEntry;
|
|
|
|
/// <inheritdoc/>
|
|
public DateTime CreatedUtc => createdUtc;
|
|
|
|
/// <inheritdoc/>
|
|
public DateTime LastModificationUtc => lastModificationUtc;
|
|
|
|
/// <inheritdoc/>
|
|
public string Path => ConcatPaths(parent.PathKey, NormalizePath(path));
|
|
|
|
/// <inheritdoc/>
|
|
public long Size => size;
|
|
/// <inheritdoc/>
|
|
public bool IsSymlink => false;
|
|
|
|
/// <inheritdoc/>
|
|
public string? SymlinkTarget => null;
|
|
|
|
/// <inheritdoc/>
|
|
public FileAttributes Attributes => IsFolder
|
|
? FileAttributes.Directory
|
|
: FileAttributes.Normal;
|
|
|
|
/// <inheritdoc/>
|
|
public Dictionary<string, string> MinorMetadata => new Dictionary<string, string>();
|
|
|
|
/// <inheritdoc/>
|
|
public bool IsBlockDevice => false;
|
|
|
|
/// <inheritdoc/>
|
|
public bool IsCharacterDevice => false;
|
|
|
|
/// <inheritdoc/>
|
|
public bool IsAlternateStream => false;
|
|
|
|
/// <inheritdoc/>
|
|
public string? HardlinkTargetId => null;
|
|
|
|
/// <inheritdoc/>
|
|
public IAsyncEnumerable<ISourceFileEntry> Enumerate(CancellationToken cancellationToken)
|
|
{
|
|
if (!isFolder)
|
|
throw new InvalidOperationException("Enumerate can only be called on folders");
|
|
|
|
return parent.WrappedBackend.ListAsync(path, cancellationToken)
|
|
.Select(x =>
|
|
{
|
|
if (x is BackendSourceFileEntry)
|
|
return x;
|
|
|
|
return new BackendSourceFileEntry(parent, x.Path, x.IsFolder, false, x.CreatedUtc, x.LastModificationUtc, x.Size);
|
|
});
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<bool> FileExists(string path, CancellationToken cancellationToken)
|
|
{
|
|
if (!isFolder)
|
|
throw new InvalidOperationException("FileExists can only be called on folders");
|
|
|
|
try
|
|
{
|
|
var entry = await parent.WrappedBackend.GetEntryAsync(path, cancellationToken);
|
|
return entry != null && !entry.IsFolder;
|
|
}
|
|
catch (FileNotFoundException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public Task<Stream?> OpenMetadataRead(CancellationToken cancellationToken)
|
|
=> Task.FromResult<Stream?>(null);
|
|
|
|
/// <summary>
|
|
/// Helper class for reporting th length of a stream
|
|
/// </summary>
|
|
/// <param name="stream">The stream to wrap</param>
|
|
/// <param name="reportedLength">The length to report</param>
|
|
private class LengthReportingStream(Stream stream, long reportedLength) : OverrideableStream(stream)
|
|
{
|
|
/// <summary>
|
|
/// Track the position
|
|
/// </summary>
|
|
private long position = 0;
|
|
|
|
/// <inheritdoc/>
|
|
public override long Length => reportedLength;
|
|
|
|
/// <inheritdoc/>
|
|
public override long Position
|
|
{
|
|
get => position;
|
|
set => position = base.Position = value;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override void Write(byte[] buffer, int offset, int count)
|
|
{
|
|
base.Write(buffer, offset, count);
|
|
position += count;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override int Read(byte[] buffer, int offset, int count)
|
|
{
|
|
var read = base.Read(buffer, offset, count);
|
|
position += read;
|
|
return read;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
|
{
|
|
var read = await base.ReadAsync(buffer, offset, count, cancellationToken);
|
|
position += read;
|
|
return read;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
|
{
|
|
await base.WriteAsync(buffer, offset, count, cancellationToken);
|
|
position += count;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<Stream> OpenRead(CancellationToken cancellationToken)
|
|
{
|
|
if (isFolder)
|
|
throw new InvalidOperationException("OpenRead can only be called on files");
|
|
|
|
if (parent.WrappedBackend is IStreamingBackend streamingBackend)
|
|
{
|
|
var pipe = new Pipe();
|
|
|
|
// Start writing data to the pipe asynchronously
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await streamingBackend.GetAsync(path, pipe.Writer.AsStream(), cancellationToken).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await pipe.Writer.CompleteAsync(ex).ConfigureAwait(false);
|
|
}
|
|
finally
|
|
{
|
|
await pipe.Writer.CompleteAsync().ConfigureAwait(false);
|
|
}
|
|
})
|
|
.ContinueWith(t =>
|
|
{
|
|
if (t.IsFaulted)
|
|
Logging.Log.WriteWarningMessage(LOGTAG, "ErrorPipingStream", t.Exception, "Error piping stream for {0}", this.Path);
|
|
});
|
|
|
|
// Return the readable stream so the caller can read data as it's produced
|
|
return new LengthReportingStream(pipe.Reader.AsStream(), size);
|
|
}
|
|
else
|
|
{
|
|
TempFile? tempFile = null;
|
|
TempFileStream? file = null;
|
|
try
|
|
{
|
|
tempFile = new TempFile();
|
|
await parent.WrappedBackend.GetAsync(path, tempFile, cancellationToken);
|
|
file = TempFileStream.Create(tempFile);
|
|
return file;
|
|
}
|
|
catch
|
|
{
|
|
file?.Dispose();
|
|
tempFile?.Dispose();
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Normalizes the path, turning backslashes into forward slashes
|
|
/// </summary>
|
|
/// <param name="path">The path to normalize</param>
|
|
/// <returns>The normalized path</returns>
|
|
public static string NormalizePath(string path)
|
|
{
|
|
if (System.IO.Path.DirectorySeparatorChar == '/')
|
|
return path;
|
|
|
|
if (string.IsNullOrEmpty(path))
|
|
return path;
|
|
|
|
return path.Replace('\\', '/');
|
|
}
|
|
|
|
/// <summary>
|
|
/// Concatenates two paths
|
|
/// </summary>
|
|
/// <param name="path1">The first path</param>
|
|
/// <param name="path2">The second path</param>
|
|
/// <returns>The concatenated path</returns>
|
|
public static string ConcatPaths(string path1, string path2)
|
|
{
|
|
if (string.IsNullOrEmpty(path1))
|
|
return path2;
|
|
|
|
if (string.IsNullOrEmpty(path2))
|
|
return path1;
|
|
|
|
if (path1.EndsWith('/') || path1.EndsWith('\\'))
|
|
return path1 + path2;
|
|
|
|
return path1 + "/" + path2;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new backend source entry from a file entry
|
|
/// </summary>
|
|
/// <param name="parent">The parent backend</param>
|
|
/// <param name="entry">The file entry</param>
|
|
/// <param name="prefix">The prefix to add to the path</param>
|
|
/// <returns>The new backend source entry</returns>
|
|
public static BackendSourceFileEntry FromFileEntry(BackendSourceProvider parent, string prefix, IFileEntry entry)
|
|
=> new BackendSourceFileEntry(parent, ConcatPaths(prefix, entry.Name), entry.IsFolder, false, entry.Created, entry.LastModification, entry.Size);
|
|
}
|
|
|