// Copyright (C) 2024, 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 Duplicati.Library.Interface; using Duplicati.Library.Utility; namespace Duplicati.Library.SourceProvider; /// /// A source provider that wraps a backend /// /// The backend to wrap public class BackendSourceProvider : ISourceProvider { /// /// The wrapped backend /// public IBackend WrappedBackend { get; init; } /// /// The path key unique for the destination /// public readonly string PathKey; /// /// Creates a new backend source provider /// /// The backend to wrap /// The URL of the backend public BackendSourceProvider(IBackend backend, string url) { WrappedBackend = backend; var uri = new Utility.Uri(url); // TODO: The username may be provided in other ways to the backend... PathKey = $"{uri.Scheme}://{uri.Host}:{uri.Port}/~{uri.Username}/{uri.Path}/"; } /// /// Creates a new backend source entry /// /// The parent backend /// The path of the entry /// True if the entry is a folder /// True if the entry is a meta entry /// The creation time of the entry /// The last modification time of the entry /// The size of the entry private class BackendSourceFileEntry(BackendSourceProvider parent, string path, bool isFolder, bool isMetaEntry, DateTime createdUtc, DateTime lastModificationUtc, long size) : ISourceFileEntry { /// public bool IsFolder => isFolder; /// public bool IsMetaEntry => isMetaEntry; /// public bool IsRootEntry => isMetaEntry; /// public DateTime CreatedUtc => createdUtc; /// public DateTime LastModificationUtc => lastModificationUtc; /// public string Path => $"{parent.PathKey}{path}"; /// public long Size => size; /// public bool IsSymlink => false; /// public string? SymlinkTarget => null; /// public FileAttributes Attributes => IsFolder ? FileAttributes.Directory : FileAttributes.Normal; /// public Dictionary MinorMetadata => new Dictionary(); /// public bool IsBlockDevice => false; /// public bool IsCharacterDevice => false; /// public bool IsAlternateStream => false; /// public string? HardlinkTargetId => null; /// public IAsyncEnumerable Enumerate(CancellationToken cancellationToken) { if (!isFolder) throw new InvalidOperationException("Enumerate can only be called on folders"); if (parent.WrappedBackend is IFolderEnabledBackend folderEnabledBackend) { return folderEnabledBackend.ListAsync(path, cancellationToken) .Select(x => new BackendSourceFileEntry(parent, x.Path, x.IsFolder, false, new DateTime(0), x.LastModificationUtc, x.Size)); } else { // If we do not support folders, we can only list the root if (this.IsMetaEntry) return parent.WrappedBackend.List() .Select(x => new BackendSourceFileEntry(parent, x.Name, x.IsFolder, false, new DateTime(0), x.LastModification, x.Size)) .ToAsyncEnumerable(); return Array.Empty().ToAsyncEnumerable(); } } /// public async Task FileExists(string path, CancellationToken cancellationToken) { if (!isFolder) throw new InvalidOperationException("FileExists can only be called on folders"); if (parent.WrappedBackend is IFolderEnabledBackend folderEnabledBackend) { try { var entry = await folderEnabledBackend.GetEntryAsync(path, cancellationToken); return entry != null && !entry.IsFolder; } catch (FileNotFoundException) { return false; } } return false; } /// public Task OpenMetadataRead(CancellationToken cancellationToken) => Task.FromResult(null); /// public async Task OpenRead(CancellationToken cancellationToken) { if (isFolder) throw new InvalidOperationException("OpenRead can only be called on files"); if (parent.WrappedBackend is IStreamingBackend streamingBackend) { var mapStream = new MapStream(); mapStream.CopyTask = streamingBackend.GetAsync(this.Path, mapStream, cancellationToken); return mapStream; } else { TempFileStream? file = null; try { file = TempFileStream.Create(); await parent.WrappedBackend.GetAsync(this.Path, file.Path, cancellationToken); file.Position = 0; return file; } catch { file?.Dispose(); throw; } } } } /// public IAsyncEnumerable Enumerate(CancellationToken cancellationToken) => new[] { new BackendSourceFileEntry(this, "", true, true, new DateTime(0), new DateTime(0), 0) }.ToAsyncEnumerable(); /// public Task GetEntry(string path, bool isFolder, CancellationToken cancellationToken) // TODO: If we cache the enumerate result, we could check if the path exists in the cache and return it => Task.FromResult(new BackendSourceFileEntry(this, path, isFolder, false, new DateTime(0), new DateTime(0), -1)); /// public void Dispose() { WrappedBackend.Dispose(); GC.SuppressFinalize(this); } }