From 794bcfffb6e999f1203d72ef5dceff1980f9844d Mon Sep 17 00:00:00 2001 From: "Marcelo C." Date: Tue, 6 May 2025 17:16:09 -0300 Subject: [PATCH 1/2] Tahoe LAFS migration to HttpClient with new timeouts & SSL options Migrated to HttpClient. --- .../Library/Backend/TahoeLAFS/TahoeBackend.cs | 464 ++++++++---------- .../Library/Backend/TahoeLAFS/TahoeEl.cs | 7 + .../Backend/TahoeLAFS/TahoeElConverter.cs | 32 ++ .../Backend/TahoeLAFS/TahoeMetadata.cs | 6 + .../Library/Backend/TahoeLAFS/TahoeNode.cs | 12 + .../Library/Backend/TahoeLAFS/TahoeStamps.cs | 7 + 6 files changed, 274 insertions(+), 254 deletions(-) create mode 100644 Duplicati/Library/Backend/TahoeLAFS/TahoeEl.cs create mode 100644 Duplicati/Library/Backend/TahoeLAFS/TahoeElConverter.cs create mode 100644 Duplicati/Library/Backend/TahoeLAFS/TahoeMetadata.cs create mode 100644 Duplicati/Library/Backend/TahoeLAFS/TahoeNode.cs create mode 100644 Duplicati/Library/Backend/TahoeLAFS/TahoeStamps.cs diff --git a/Duplicati/Library/Backend/TahoeLAFS/TahoeBackend.cs b/Duplicati/Library/Backend/TahoeLAFS/TahoeBackend.cs index c3da4ad88..af5b2dab1 100644 --- a/Duplicati/Library/Backend/TahoeLAFS/TahoeBackend.cs +++ b/Duplicati/Library/Backend/TahoeLAFS/TahoeBackend.cs @@ -19,287 +19,243 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using System.Net; +using System.Reflection; +using System.Runtime.CompilerServices; using Duplicati.Library.Common.IO; using Duplicati.Library.Interface; using Duplicati.Library.Utility; using Duplicati.Library.Utility.Options; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System.Net; -using System.Runtime.CompilerServices; +using Uri = Duplicati.Library.Utility.Uri; -namespace Duplicati.Library.Backend +namespace Duplicati.Library.Backend; + +public class TahoeBackend : IStreamingBackend { - public class TahoeBackend : IBackend, IStreamingBackend + /// + /// Base URL for the Tahoe-LAFS backend + /// + private readonly string? _url; + + /// + /// The timeout options for API requests. + /// + private readonly TimeoutOptionsHelper.Timeouts? _timeouts ; + + /// + /// The options for the SSL certificate validation + /// + private readonly SslOptionsHelper.SslCertificateOptions _certificateOptions = null!; + + /// + /// Cached instance of HttpClient to be used + /// + private HttpClient? _httpClient; + + public TahoeBackend() { - private readonly string m_url; - private readonly bool m_useSSL = false; - private readonly TimeoutOptionsHelper.Timeouts m_timeouts; + + } - private class TahoeEl + public TahoeBackend(string url, Dictionary options) + { + //Validate URL + var u = new Uri(url); + u.RequireHost(); + + if (!u.Path.StartsWith("uri/URI:DIR2:", StringComparison.Ordinal) && !u.Path.StartsWith("uri/URI%3ADIR2%3A", StringComparison.Ordinal)) + throw new UserInformationException(Strings.TahoeBackend.UnrecognizedUriError, "TahoeInvalidUri"); + + _certificateOptions = SslOptionsHelper.Parse(options); + + _url = u.SetScheme(_certificateOptions.UseSSL ? "https" : "http").SetQuery(null).SetCredentials(null, null).ToString(); + _url = Util.AppendDirSeparator(_url, "/"); + _timeouts = TimeoutOptionsHelper.Parse(options); + } + + /// + public Task TestAsync(CancellationToken cancelToken) + => this.TestListAsync(cancelToken); + + /// + public async Task CreateFolderAsync(CancellationToken cancelToken) + { + using var resp = await Utility.Utility.WithTimeout(_timeouts!.ListTimeout, cancelToken, + _ => GetHttpClient().SendAsync(CreateRequest(string.Empty, "t=mkdir", HttpMethod.Post), + HttpCompletionOption.ResponseContentRead, cancelToken)).ConfigureAwait(false); + + resp.EnsureSuccessStatusCode(); + } + + /// + public string DisplayName => Strings.TahoeBackend.Displayname; + + /// + public string ProtocolKey => "tahoe"; + + /// + public async IAsyncEnumerable ListAsync([EnumeratorCancellation] CancellationToken cancelToken) + { + TahoeEl? data; + + try { - public string? nodetype { get; set; } - public TahoeNode? node { get; set; } + using var resp = await Utility.Utility.WithTimeout(_timeouts!.ListTimeout, cancelToken, + _ => GetHttpClient().SendAsync(CreateRequest(string.Empty, "t=json", HttpMethod.Get), + HttpCompletionOption.ResponseContentRead, cancelToken)).ConfigureAwait(false); + resp.EnsureSuccessStatusCode(); + + await using var rs = await resp.Content.ReadAsStreamAsync(cancelToken).ConfigureAwait(false); + using var sr = new StreamReader(rs); + await using var jr = new JsonTextReader(sr); + var jsr = new JsonSerializer(); + jsr.Converters.Add(new TahoeElConverter()); + data = jsr.Deserialize(jr) + ?? throw new Exception("Invalid folder listing response"); } - - private class TahoeNode + catch (HttpRequestException wex) + when (wex.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Conflict) { - public string? rw_uri { get; set; } - public string? verify_uri { get; set; } - public string? ro_uri { get; set; } - public Dictionary? children { get; set; } - public bool mutable { get; set; } - public long size { get; set; } - public TahoeMetadata? metadata { get; set; } + throw new FolderMissingException(Strings.TahoeBackend.MissingFolderError(_url, wex.Message), wex); } + + if (data is not { Node: not null } || data.Nodetype != "dirnode") + throw new Exception("Invalid folder listing response"); - private class TahoeMetadata + foreach (var e in data.Node.Children ?? []) { - public TahoeStamps? tahoe { get; set; } - } + if (e.Value.Node == null) + continue; - private class TahoeStamps - { - public double linkmotime { get; set; } - public double linkcrtime { get; set; } - } + var isDir = e.Value.Nodetype == "dirnode"; + var isFile = e.Value.Nodetype == "filenode"; - private class TahoeElConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - => objectType == typeof(TahoeEl); + if (!isDir && !isFile) + continue; - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + var fe = new FileEntry(e.Key) { - var array = JArray.Load(reader); - string? nodetype = null; - TahoeNode? node = null; - foreach (var token in array.Children()) - if (token.Type == JTokenType.String) - nodetype = token.ToString(); - else if (token.Type == JTokenType.Object) - node = token.ToObject(serializer); + IsFolder = isDir + }; - return new TahoeEl() { nodetype = nodetype, node = node }; - } + if (e.Value.Node.Metadata != null && e.Value.Node.Metadata.Tahoe != null) + fe.LastModification = Utility.Utility.EPOCH + TimeSpan.FromSeconds(e.Value.Node.Metadata.Tahoe.Linkmotime); - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - => throw new NotImplementedException(); + if (isFile) + fe.Size = e.Value.Node.Size; + + yield return fe; } + } + /// + public async Task PutAsync(string remotename, string filename, CancellationToken cancelToken) + { + await using var fs = File.OpenRead(filename); + await PutAsync(remotename, fs, cancelToken).ConfigureAwait(false); + } - public TahoeBackend() + /// + public async Task GetAsync(string remotename, string filename, CancellationToken cancelToken) + { + await using var fs = File.Create(filename); + await GetAsync(remotename, fs, cancelToken).ConfigureAwait(false); + } + + /// + public async Task DeleteAsync(string remotename, CancellationToken cancelToken) + { + try { - m_url = null!; - m_timeouts = null!; - } - - public TahoeBackend(string url, Dictionary options) - { - //Validate URL - var u = new Utility.Uri(url); - u.RequireHost(); - - if (!u.Path.StartsWith("uri/URI:DIR2:", StringComparison.Ordinal) && !u.Path.StartsWith("uri/URI%3ADIR2%3A", StringComparison.Ordinal)) - throw new UserInformationException(Strings.TahoeBackend.UnrecognizedUriError, "TahoeInvalidUri"); - - // TODO: When upgrading to HttpClient, also support the certificate options - var certOptions = SslOptionsHelper.Parse(options); - m_useSSL = certOptions.UseSSL; - - m_url = u.SetScheme(m_useSSL ? "https" : "http").SetQuery(null).SetCredentials(null, null).ToString(); - m_url = Util.AppendDirSeparator(m_url, "/"); - m_timeouts = TimeoutOptionsHelper.Parse(options); - } - - private HttpWebRequest CreateRequest(string remotename, string queryparams) - { - var req = (HttpWebRequest)HttpWebRequest.Create($"{m_url}{Utility.Uri.UrlEncode(remotename).Replace("+", "%20")}{(string.IsNullOrEmpty(queryparams) || queryparams.Trim().Length == 0 ? "" : "?" + queryparams)}"); - - req.KeepAlive = false; - req.UserAgent = "Duplicati Tahoe-LAFS Client v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; - - return req; - } - - #region IBackend Members - - public Task TestAsync(CancellationToken cancelToken) - => this.TestListAsync(cancelToken); - - public async Task CreateFolderAsync(CancellationToken cancelToken) - { - var req = CreateRequest("", "t=mkdir"); - req.Method = WebRequestMethods.Http.Post; - var areq = new AsyncHttpRequest(req); - using (await Utility.Utility.WithTimeout(m_timeouts.ShortTimeout, cancelToken, _ => areq.GetResponse()).ConfigureAwait(false)) + using (await Utility.Utility.WithTimeout(_timeouts!.ShortTimeout, cancelToken, + _ => GetHttpClient().SendAsync(CreateRequest(remotename, string.Empty, HttpMethod.Delete), + cancelToken)).ConfigureAwait(false)) { } } - - public string DisplayName => Strings.TahoeBackend.Displayname; - - public string ProtocolKey => "tahoe"; - - /// - public async IAsyncEnumerable ListAsync([EnumeratorCancellation] CancellationToken cancelToken) + catch (WebException wex) + when (wex.Response is HttpWebResponse { StatusCode: HttpStatusCode.NotFound }) { - TahoeEl data; - - try - { - var req = CreateRequest("", "t=json"); - req.Method = WebRequestMethods.Http.Get; - - var areq = new AsyncHttpRequest(req); - using (var resp = await Utility.Utility.WithTimeout(m_timeouts.ListTimeout, cancelToken, _ => (HttpWebResponse)areq.GetResponse()).ConfigureAwait(false)) - { - var code = (int)resp.StatusCode; - if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically - throw new WebException(resp.StatusDescription, null, WebExceptionStatus.ProtocolError, resp); - - using (var rs = areq.GetResponseStream()) - using (var sr = new StreamReader(rs)) - using (var jr = new JsonTextReader(sr)) - { - var jsr = new JsonSerializer(); - jsr.Converters.Add(new TahoeElConverter()); - data = jsr.Deserialize(jr) - ?? throw new Exception("Invalid folder listing response"); - } - } - } - catch (WebException wex) - when (wex.Response is HttpWebResponse response - && (response.StatusCode == HttpStatusCode.Conflict || response.StatusCode == HttpStatusCode.NotFound)) - { - throw new FolderMissingException(Strings.TahoeBackend.MissingFolderError(m_url, wex.Message), wex); - } - - if (data == null || data.node == null || data.nodetype != "dirnode") - throw new Exception("Invalid folder listing response"); - - foreach (var e in data.node.children ?? []) - { - if (e.Value == null || e.Value.node == null) - continue; - - var isDir = e.Value.nodetype == "dirnode"; - var isFile = e.Value.nodetype == "filenode"; - - if (!isDir && !isFile) - continue; - - var fe = new FileEntry(e.Key); - fe.IsFolder = isDir; - - if (e.Value.node.metadata != null && e.Value.node.metadata.tahoe != null) - fe.LastModification = Utility.Utility.EPOCH + TimeSpan.FromSeconds(e.Value.node.metadata.tahoe.linkmotime); - - if (isFile) - fe.Size = e.Value.node.size; - - yield return fe; - } + throw new FileMissingException(wex); } - - public async Task PutAsync(string remotename, string filename, CancellationToken cancelToken) - { - using (var fs = File.OpenRead(filename)) - await PutAsync(remotename, fs, cancelToken).ConfigureAwait(false); - } - - public async Task GetAsync(string remotename, string filename, CancellationToken cancelToken) - { - using (var fs = File.Create(filename)) - await GetAsync(remotename, fs, cancelToken).ConfigureAwait(false); - } - - public async Task DeleteAsync(string remotename, CancellationToken cancelToken) - { - try - { - var req = CreateRequest(remotename, ""); - req.Method = "DELETE"; - var areq = new AsyncHttpRequest(req); - using (await Utility.Utility.WithTimeout(m_timeouts.ShortTimeout, cancelToken, _ => areq.GetResponse()).ConfigureAwait(false)) - { } - } - catch (WebException wex) - when (wex.Response is HttpWebResponse response && response.StatusCode == HttpStatusCode.NotFound) - { - throw new FileMissingException(wex); - } - } - - public IList SupportedCommands => [ - .. SslOptionsHelper.GetSslOnlyOption(), - ]; - - public string Description => Strings.TahoeBackend.Description; - - public Task GetDNSNamesAsync(CancellationToken cancelToken) => Task.FromResult(new[] { new System.Uri(m_url).Host }); - - #endregion - - #region IDisposable Members - - public void Dispose() - { - } - - #endregion - - #region IStreamingBackend Members - - public async Task PutAsync(string remotename, Stream stream, CancellationToken cancelToken) - { - try - { - var req = CreateRequest(remotename, ""); - req.Method = WebRequestMethods.Http.Put; - req.ContentType = "application/binary"; - - try { req.ContentLength = stream.Length; } - catch { } - - var areq = new AsyncHttpRequest(req); - using (var s = areq.GetRequestStream()) - using (var t = s.ObserveWriteTimeout(m_timeouts.ReadWriteTimeout)) - await Utility.Utility.CopyStreamAsync(stream, t, true, cancelToken).ConfigureAwait(false); - - using (var resp = await Utility.Utility.WithTimeout(m_timeouts.ShortTimeout, cancelToken, _ => (HttpWebResponse)areq.GetResponse()).ConfigureAwait(false)) - { - int code = (int)resp.StatusCode; - if (code < 200 || code >= 300) - throw new WebException(resp.StatusDescription, null, WebExceptionStatus.ProtocolError, resp); - } - } - catch (WebException wex) - when (wex.Response is HttpWebResponse response - && (response.StatusCode == HttpStatusCode.Conflict || response.StatusCode == HttpStatusCode.NotFound)) - { - throw new FolderMissingException(Strings.TahoeBackend.MissingFolderError(m_url, wex.Message), wex); - } - } - - public async Task GetAsync(string remotename, System.IO.Stream stream, CancellationToken cancelToken) - { - var req = CreateRequest(remotename, ""); - req.Method = WebRequestMethods.Http.Get; - - var areq = new AsyncHttpRequest(req); - using (var resp = await Utility.Utility.WithTimeout(m_timeouts.ListTimeout, cancelToken, _ => (HttpWebResponse)areq.GetResponse()).ConfigureAwait(false)) - { - int code = (int)resp.StatusCode; - if (code < 200 || code >= 300) - throw new WebException(resp.StatusDescription, null, WebExceptionStatus.ProtocolError, resp); - - using (var s = areq.GetResponseStream()) - using (var t = s.ObserveReadTimeout(m_timeouts.ReadWriteTimeout)) - await Utility.Utility.CopyStreamAsync(t, stream, true, cancelToken).ConfigureAwait(false); - } - } - - #endregion } -} + + /// + public IList SupportedCommands => [ + .. SslOptionsHelper.GetSslOnlyOption(), + ]; + + /// + public string Description => Strings.TahoeBackend.Description; + + /// + public Task GetDNSNamesAsync(CancellationToken cancelToken) => Task.FromResult(new[] { new System.Uri(_url).Host }); + + /// + public void Dispose() + { + } + + /// + public async Task PutAsync(string remotename, Stream stream, CancellationToken cancelToken) + { + try + { + var request = CreateRequest(remotename, string.Empty, HttpMethod.Put); + + await using var timeoutStream = stream.ObserveReadTimeout(_timeouts!.ReadWriteTimeout, false); + request.Content = new StreamContent(timeoutStream); + + request.Content.Headers.Add("Content-Type", "application/binary"); + request.Content.Headers.Add("Content-Length", timeoutStream.Length.ToString()); + + + var response = await GetHttpClient().UploadStream(request, cancelToken).ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + } + catch (HttpRequestException wex) + when (wex.StatusCode is HttpStatusCode.Conflict or HttpStatusCode.NotFound) + { + throw new FolderMissingException(Strings.TahoeBackend.MissingFolderError(_url!, wex.Message), wex); + } + } + + /// + public async Task GetAsync(string remotename, Stream stream, CancellationToken cancelToken) + { + using var resp = await Utility.Utility.WithTimeout(_timeouts!.ListTimeout, cancelToken, _ => GetHttpClient().SendAsync(CreateRequest(remotename, string.Empty, HttpMethod.Get),cancelToken)).ConfigureAwait(false); + resp.EnsureSuccessStatusCode(); + + await using var s = await resp.Content.ReadAsStreamAsync(cancelToken).ConfigureAwait(false); + await using var t = s.ObserveReadTimeout(_timeouts.ReadWriteTimeout); + await Utility.Utility.CopyStreamAsync(t, stream, true, cancelToken).ConfigureAwait(false); + } + + /// + /// Prepares the base request for Tahoe-LAFS + /// + /// Remotename parameter + /// Querystring parameters + /// Http Method + /// + private HttpRequestMessage CreateRequest(string remotename, string queryparams, HttpMethod? method = null) + { + var request = new HttpRequestMessage(method == null ? HttpMethod.Get : method, $"{_url}{Uri.UrlEncode(remotename).Replace("+", "%20")}{(string.IsNullOrEmpty(queryparams) || queryparams.Trim().Length == 0 ? String.Empty : "?" + queryparams)}"); + request.Headers.UserAgent.ParseAdd($"Duplicati Tahoe-LAFS Client {Assembly.GetExecutingAssembly().GetName().Version?.ToString()}"); + return request; + } + + /// + /// Returns the HttpClient instance to use for requests, cached for reuse. + /// + private HttpClient GetHttpClient() + { + if (_httpClient != null) return _httpClient; + + _httpClient = HttpClientHelper.CreateClient(_certificateOptions.CreateHandler()); + _httpClient.Timeout = Timeout.InfiniteTimeSpan; + + return _httpClient; + } +} \ No newline at end of file diff --git a/Duplicati/Library/Backend/TahoeLAFS/TahoeEl.cs b/Duplicati/Library/Backend/TahoeLAFS/TahoeEl.cs new file mode 100644 index 000000000..fb4fee0cc --- /dev/null +++ b/Duplicati/Library/Backend/TahoeLAFS/TahoeEl.cs @@ -0,0 +1,7 @@ +namespace Duplicati.Library.Backend; + +internal record TahoeEl +{ + public string? Nodetype { get; set; } + public TahoeNode? Node { get; set; } +} \ No newline at end of file diff --git a/Duplicati/Library/Backend/TahoeLAFS/TahoeElConverter.cs b/Duplicati/Library/Backend/TahoeLAFS/TahoeElConverter.cs new file mode 100644 index 000000000..94cb26148 --- /dev/null +++ b/Duplicati/Library/Backend/TahoeLAFS/TahoeElConverter.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Duplicati.Library.Backend; + +internal class TahoeElConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + => objectType == typeof(TahoeEl); + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + var array = JArray.Load(reader); + string? nodetype = null; + TahoeNode? node = null; + foreach (var token in array.Children()) + switch (token.Type) + { + case JTokenType.String: + nodetype = token.ToString(); + break; + case JTokenType.Object: + node = token.ToObject(serializer); + break; + } + + return new TahoeEl { Nodetype = nodetype, Node = node }; + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/Duplicati/Library/Backend/TahoeLAFS/TahoeMetadata.cs b/Duplicati/Library/Backend/TahoeLAFS/TahoeMetadata.cs new file mode 100644 index 000000000..34cbde115 --- /dev/null +++ b/Duplicati/Library/Backend/TahoeLAFS/TahoeMetadata.cs @@ -0,0 +1,6 @@ +namespace Duplicati.Library.Backend; + +internal record TahoeMetadata +{ + public TahoeStamps? Tahoe { get; set; } +} \ No newline at end of file diff --git a/Duplicati/Library/Backend/TahoeLAFS/TahoeNode.cs b/Duplicati/Library/Backend/TahoeLAFS/TahoeNode.cs new file mode 100644 index 000000000..4cbf53284 --- /dev/null +++ b/Duplicati/Library/Backend/TahoeLAFS/TahoeNode.cs @@ -0,0 +1,12 @@ +namespace Duplicati.Library.Backend; + +internal record TahoeNode +{ + public string? RwUri { get; set; } + public string? VerifyUri { get; set; } + public string? RoUri { get; set; } + public Dictionary? Children { get; set; } + public bool Mutable { get; set; } + public long Size { get; set; } + public TahoeMetadata? Metadata { get; set; } +} \ No newline at end of file diff --git a/Duplicati/Library/Backend/TahoeLAFS/TahoeStamps.cs b/Duplicati/Library/Backend/TahoeLAFS/TahoeStamps.cs new file mode 100644 index 000000000..d84f2231d --- /dev/null +++ b/Duplicati/Library/Backend/TahoeLAFS/TahoeStamps.cs @@ -0,0 +1,7 @@ +namespace Duplicati.Library.Backend; + +internal record TahoeStamps +{ + public double Linkmotime { get; set; } + public double Linkcrtime { get; set; } +} \ No newline at end of file From 1526853001836f316745140a4dd55382afa8152d Mon Sep 17 00:00:00 2001 From: "Marcelo C." Date: Thu, 8 May 2025 09:07:25 -0300 Subject: [PATCH 2/2] Corrections to cancelation tokens scope Propagating innerCancelationToken from timeouts. Migrating CreateRequest to outer scope & disposable pattern --- .../Library/Backend/TahoeLAFS/TahoeBackend.cs | 77 +++++++++++-------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/Duplicati/Library/Backend/TahoeLAFS/TahoeBackend.cs b/Duplicati/Library/Backend/TahoeLAFS/TahoeBackend.cs index af5b2dab1..18033d45e 100644 --- a/Duplicati/Library/Backend/TahoeLAFS/TahoeBackend.cs +++ b/Duplicati/Library/Backend/TahoeLAFS/TahoeBackend.cs @@ -36,17 +36,17 @@ public class TahoeBackend : IStreamingBackend /// /// Base URL for the Tahoe-LAFS backend /// - private readonly string? _url; - + private readonly string _url; + /// /// The timeout options for API requests. /// - private readonly TimeoutOptionsHelper.Timeouts? _timeouts ; + private readonly TimeoutOptionsHelper.Timeouts _timeouts; /// /// The options for the SSL certificate validation /// - private readonly SslOptionsHelper.SslCertificateOptions _certificateOptions = null!; + private readonly SslOptionsHelper.SslCertificateOptions _certificateOptions; /// /// Cached instance of HttpClient to be used @@ -55,7 +55,9 @@ public class TahoeBackend : IStreamingBackend public TahoeBackend() { - + _url = null!; + _timeouts = null!; + _certificateOptions = null!; } public TahoeBackend(string url, Dictionary options) @@ -73,18 +75,21 @@ public class TahoeBackend : IStreamingBackend _url = Util.AppendDirSeparator(_url, "/"); _timeouts = TimeoutOptionsHelper.Parse(options); } - + /// public Task TestAsync(CancellationToken cancelToken) => this.TestListAsync(cancelToken); - + /// public async Task CreateFolderAsync(CancellationToken cancelToken) { - using var resp = await Utility.Utility.WithTimeout(_timeouts!.ListTimeout, cancelToken, - _ => GetHttpClient().SendAsync(CreateRequest(string.Empty, "t=mkdir", HttpMethod.Post), - HttpCompletionOption.ResponseContentRead, cancelToken)).ConfigureAwait(false); - + using var resp = await Utility.Utility.WithTimeout(_timeouts.ListTimeout, cancelToken, + innerCancelToken => + { + using var request = CreateRequest(string.Empty, "t=mkdir", HttpMethod.Post); + return GetHttpClient().SendAsync(request, HttpCompletionOption.ResponseContentRead, innerCancelToken); + }).ConfigureAwait(false); + resp.EnsureSuccessStatusCode(); } @@ -101,9 +106,12 @@ public class TahoeBackend : IStreamingBackend try { - using var resp = await Utility.Utility.WithTimeout(_timeouts!.ListTimeout, cancelToken, - _ => GetHttpClient().SendAsync(CreateRequest(string.Empty, "t=json", HttpMethod.Get), - HttpCompletionOption.ResponseContentRead, cancelToken)).ConfigureAwait(false); + using var resp = await Utility.Utility.WithTimeout(_timeouts.ListTimeout, cancelToken, + innerCancelToken => + { + using var request = CreateRequest(string.Empty, "t=json", HttpMethod.Get); + return GetHttpClient().SendAsync(request, HttpCompletionOption.ResponseContentRead, innerCancelToken); + }).ConfigureAwait(false); resp.EnsureSuccessStatusCode(); await using var rs = await resp.Content.ReadAsStreamAsync(cancelToken).ConfigureAwait(false); @@ -119,7 +127,7 @@ public class TahoeBackend : IStreamingBackend { throw new FolderMissingException(Strings.TahoeBackend.MissingFolderError(_url, wex.Message), wex); } - + if (data is not { Node: not null } || data.Nodetype != "dirnode") throw new Exception("Invalid folder listing response"); @@ -139,7 +147,7 @@ public class TahoeBackend : IStreamingBackend IsFolder = isDir }; - if (e.Value.Node.Metadata != null && e.Value.Node.Metadata.Tahoe != null) + if (e.Value.Node.Metadata is { Tahoe: not null }) fe.LastModification = Utility.Utility.EPOCH + TimeSpan.FromSeconds(e.Value.Node.Metadata.Tahoe.Linkmotime); if (isFile) @@ -168,9 +176,13 @@ public class TahoeBackend : IStreamingBackend { try { - using (await Utility.Utility.WithTimeout(_timeouts!.ShortTimeout, cancelToken, - _ => GetHttpClient().SendAsync(CreateRequest(remotename, string.Empty, HttpMethod.Delete), - cancelToken)).ConfigureAwait(false)) + using (await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, cancelToken, + innerCancelToken => + { + using var request = CreateRequest(remotename, string.Empty, HttpMethod.Delete); + return GetHttpClient().SendAsync(request, + innerCancelToken); + }).ConfigureAwait(false)) { } } catch (WebException wex) @@ -182,7 +194,7 @@ public class TahoeBackend : IStreamingBackend /// public IList SupportedCommands => [ - .. SslOptionsHelper.GetSslOnlyOption(), + .. SslOptionsHelper.GetSslOnlyOption(), .. TimeoutOptionsHelper.GetOptions() ]; /// @@ -201,37 +213,42 @@ public class TahoeBackend : IStreamingBackend { try { - var request = CreateRequest(remotename, string.Empty, HttpMethod.Put); + using var request = CreateRequest(remotename, string.Empty, HttpMethod.Put); - await using var timeoutStream = stream.ObserveReadTimeout(_timeouts!.ReadWriteTimeout, false); + await using var timeoutStream = stream.ObserveReadTimeout(_timeouts.ReadWriteTimeout, false); request.Content = new StreamContent(timeoutStream); request.Content.Headers.Add("Content-Type", "application/binary"); request.Content.Headers.Add("Content-Length", timeoutStream.Length.ToString()); - - - var response = await GetHttpClient().UploadStream(request, cancelToken).ConfigureAwait(false); + + using var response = await GetHttpClient().UploadStream(request, cancelToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); } catch (HttpRequestException wex) when (wex.StatusCode is HttpStatusCode.Conflict or HttpStatusCode.NotFound) { - throw new FolderMissingException(Strings.TahoeBackend.MissingFolderError(_url!, wex.Message), wex); + throw new FolderMissingException(Strings.TahoeBackend.MissingFolderError(_url, wex.Message), wex); } } /// public async Task GetAsync(string remotename, Stream stream, CancellationToken cancelToken) { - using var resp = await Utility.Utility.WithTimeout(_timeouts!.ListTimeout, cancelToken, _ => GetHttpClient().SendAsync(CreateRequest(remotename, string.Empty, HttpMethod.Get),cancelToken)).ConfigureAwait(false); + using var resp = await Utility.Utility.WithTimeout(_timeouts.ListTimeout, cancelToken, + innerCancelToken => + { + using var request = CreateRequest(remotename, string.Empty, HttpMethod.Get); + return GetHttpClient().SendAsync(request, innerCancelToken); + }).ConfigureAwait(false); + resp.EnsureSuccessStatusCode(); await using var s = await resp.Content.ReadAsStreamAsync(cancelToken).ConfigureAwait(false); await using var t = s.ObserveReadTimeout(_timeouts.ReadWriteTimeout); await Utility.Utility.CopyStreamAsync(t, stream, true, cancelToken).ConfigureAwait(false); } - + /// /// Prepares the base request for Tahoe-LAFS /// @@ -245,14 +262,14 @@ public class TahoeBackend : IStreamingBackend request.Headers.UserAgent.ParseAdd($"Duplicati Tahoe-LAFS Client {Assembly.GetExecutingAssembly().GetName().Version?.ToString()}"); return request; } - + /// /// Returns the HttpClient instance to use for requests, cached for reuse. /// private HttpClient GetHttpClient() { if (_httpClient != null) return _httpClient; - + _httpClient = HttpClientHelper.CreateClient(_certificateOptions.CreateHandler()); _httpClient.Timeout = Timeout.InfiniteTimeSpan;