diff --git a/Duplicati/Library/Backend/GoogleServices/Duplicati.Library.Backend.GoogleServices.csproj b/Duplicati/Library/Backend/GoogleServices/Duplicati.Library.Backend.GoogleServices.csproj
index 198df6dd6..be6989d62 100644
--- a/Duplicati/Library/Backend/GoogleServices/Duplicati.Library.Backend.GoogleServices.csproj
+++ b/Duplicati/Library/Backend/GoogleServices/Duplicati.Library.Backend.GoogleServices.csproj
@@ -4,7 +4,10 @@
net8.0
Library
Copyright © 2025 Team Duplicati, MIT license
+ enable
+ enable
+
diff --git a/Duplicati/Library/Backend/GoogleServices/GCSConfig.cs b/Duplicati/Library/Backend/GoogleServices/GCSConfig.cs
index 2166afad0..4effd5c35 100644
--- a/Duplicati/Library/Backend/GoogleServices/GCSConfig.cs
+++ b/Duplicati/Library/Backend/GoogleServices/GCSConfig.cs
@@ -1,29 +1,25 @@
-// 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
+// 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;
using Duplicati.Library.Interface;
-using System.Collections.Generic;
-using System.Linq;
-using Duplicati.Library.Localization.Short;
namespace Duplicati.Library.Backend.GoogleServices
{
@@ -32,7 +28,7 @@ namespace Duplicati.Library.Backend.GoogleServices
public class GCSConfig : IWebModule
{
private const ConfigType DEFAULT_CONFIG_TYPE = ConfigType.Locations;
- private static readonly string DEFAULT_CONFIG_TYPE_STR = Enum.GetName(typeof(ConfigType), DEFAULT_CONFIG_TYPE);
+ private static readonly string DEFAULT_CONFIG_TYPE_STR = DEFAULT_CONFIG_TYPE.ToString();
private const string KEY_CONFIGTYPE = "gcs-config";
public enum ConfigType
@@ -46,15 +42,13 @@ namespace Duplicati.Library.Backend.GoogleServices
#region IWebModule implementation
- public System.Collections.Generic.IDictionary Execute(System.Collections.Generic.IDictionary options)
+ public IDictionary Execute(IDictionary options)
{
- string k;
- options.TryGetValue(KEY_CONFIGTYPE, out k);
+ options.TryGetValue(KEY_CONFIGTYPE, out var k);
if (string.IsNullOrWhiteSpace(k))
k = DEFAULT_CONFIG_TYPE_STR;
- ConfigType ct;
- if (!Enum.TryParse(k, true, out ct))
+ if (!Enum.TryParse(k, true, out var ct))
ct = DEFAULT_CONFIG_TYPE;
switch (ct)
@@ -66,23 +60,17 @@ namespace Duplicati.Library.Backend.GoogleServices
}
}
- public string Key { get { return "gcs-getconfig"; } }
+ public string Key => "gcs-getconfig";
- public string DisplayName { get { return LC.L("Google Cloud Storage configuration module"); } }
+ public string DisplayName => Strings.GCSConfig.DisplayName;
- public string Description { get { return LC.L("Expose Google Cloud Storage configuration as a web module"); } }
+ public string Description => Strings.GCSConfig.Description;
- public System.Collections.Generic.IList SupportedCommands
- {
- get
- {
- return new List([
- new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, LC.L("The config to get"), LC.L("Provide different config values"), DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType)))
-
- ]);
- }
- }
+ public IList SupportedCommands =>
+ [
+ new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, Strings.GCSConfig.ConfigTypeShort, Strings.GCSConfig.ConfigTypeLong, DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType)))
+ ];
#endregion
}
diff --git a/Duplicati/Library/Backend/GoogleServices/GoogleCloudStorage.cs b/Duplicati/Library/Backend/GoogleServices/GoogleCloudStorage.cs
index 30e9db6de..c566115d2 100644
--- a/Duplicati/Library/Backend/GoogleServices/GoogleCloudStorage.cs
+++ b/Duplicati/Library/Backend/GoogleServices/GoogleCloudStorage.cs
@@ -23,14 +23,11 @@ using Duplicati.Library.Backend.GoogleServices;
using Duplicati.Library.Common.IO;
using Duplicati.Library.Interface;
using Duplicati.Library.Utility;
+using Duplicati.Library.Utility.Options;
using Newtonsoft.Json;
-using System;
-using System.Collections.Generic;
using System.Net;
using System.Runtime.CompilerServices;
using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
namespace Duplicati.Library.Backend.GoogleCloudStorage
{
@@ -38,7 +35,7 @@ namespace Duplicati.Library.Backend.GoogleCloudStorage
// This class is instantiated dynamically in the BackendLoader.
public class GoogleCloudStorage : IBackend, IStreamingBackend, IRenameEnabledBackend, ITimeoutExemptBackend
{
- private const string AUTHID_OPTION = "authid";
+ private static readonly string TOKEN_URL = OAuthHelper.OAUTH_LOGIN_URL("gcs");
private const string PROJECT_OPTION = "gcs-project";
private const string LOCATION_OPTION = "gcs-location";
@@ -46,17 +43,22 @@ namespace Duplicati.Library.Backend.GoogleCloudStorage
private readonly string m_bucket;
private readonly string m_prefix;
- private readonly string m_project;
+ private readonly string? m_project;
private readonly OAuthHelper m_oauth;
- private readonly string m_location;
- private readonly string m_storage_class;
+ private readonly string? m_location;
+ private readonly string? m_storage_class;
+ private readonly TimeoutOptionsHelper.Timeouts m_timeouts;
public GoogleCloudStorage()
{
+ m_bucket = null!;
+ m_prefix = null!;
+ m_oauth = null!;
+ m_timeouts = null!;
}
- public GoogleCloudStorage(string url, Dictionary options)
+ public GoogleCloudStorage(string url, Dictionary options)
{
var uri = new Utility.Uri(url);
@@ -67,38 +69,37 @@ namespace Duplicati.Library.Backend.GoogleCloudStorage
if (m_prefix.StartsWith("/", StringComparison.Ordinal))
m_prefix = m_prefix.Substring(1);
- string authid;
- options.TryGetValue(AUTHID_OPTION, out authid);
- options.TryGetValue(PROJECT_OPTION, out m_project);
- options.TryGetValue(LOCATION_OPTION, out m_location);
- options.TryGetValue(STORAGECLASS_OPTION, out m_storage_class);
+ var authId = AuthIdOptionsHelper.Parse(options)
+ .RequireCredentials(TOKEN_URL);
- if (string.IsNullOrEmpty(authid))
- throw new UserInformationException(Strings.GoogleCloudStorage.MissingAuthID(AUTHID_OPTION), "GoogleCloudStorageMissingAuthID");
+ m_timeouts = TimeoutOptionsHelper.Parse(options);
+ m_project = options.GetValueOrDefault(PROJECT_OPTION);
+ m_location = options.GetValueOrDefault(LOCATION_OPTION);
+ m_storage_class = options.GetValueOrDefault(STORAGECLASS_OPTION);
- m_oauth = new OAuthHelper(authid, this.ProtocolKey);
+ m_oauth = new OAuthHelper(authId.AuthId!, this.ProtocolKey);
m_oauth.AutoAuthHeader = true;
}
private class ListBucketResponse
{
- public string nextPageToken { get; set; }
- public BucketResourceItem[] items { get; set; }
+ public string? nextPageToken { get; set; }
+ public BucketResourceItem[]? items { get; set; }
}
private class BucketResourceItem
{
- public string name { get; set; }
+ public string? name { get; set; }
public DateTime? updated { get; set; }
public long? size { get; set; }
}
private class CreateBucketRequest
{
- public string name { get; set; }
- public string location { get; set; }
- public string storageClass { get; set; }
+ public string? name { get; set; }
+ public string? location { get; set; }
+ public string? storageClass { get; set; }
}
private async Task HandleListExceptions(Func> func)
@@ -123,12 +124,15 @@ namespace Duplicati.Library.Backend.GoogleCloudStorage
var url = WebApi.GoogleCloudStorage.ListUrl(m_bucket, Utility.Uri.UrlEncode(m_prefix));
while (true)
{
- var resp = await HandleListExceptions(() => m_oauth.ReadJSONResponseAsync(url, cancelToken)).ConfigureAwait(false);
+ var resp = await HandleListExceptions(() =>
+ Utility.Utility.WithTimeout(m_timeouts.ListTimeout, cancelToken, ct =>
+ m_oauth.ReadJSONResponseAsync(url, ct))
+ ).ConfigureAwait(false);
if (resp.items != null)
foreach (var f in resp.items)
{
- var name = f.name;
+ var name = f.name ?? "";
if (name.StartsWith(m_prefix, StringComparison.OrdinalIgnoreCase))
name = name.Substring(m_prefix.Length);
if (f.size == null)
@@ -148,33 +152,33 @@ namespace Duplicati.Library.Backend.GoogleCloudStorage
public async Task PutAsync(string remotename, string filename, CancellationToken cancelToken)
{
- using (System.IO.FileStream fs = System.IO.File.OpenRead(filename))
+ using (var fs = File.OpenRead(filename))
await PutAsync(remotename, fs, cancelToken).ConfigureAwait(false);
}
public async Task GetAsync(string remotename, string filename, CancellationToken cancelToken)
{
- using (System.IO.FileStream fs = System.IO.File.Create(filename))
+ using (var fs = File.Create(filename))
await GetAsync(remotename, fs, cancelToken).ConfigureAwait(false);
}
public Task DeleteAsync(string remotename, CancellationToken cancelToken)
{
-
var req = m_oauth.CreateRequest(WebApi.GoogleCloudStorage.DeleteUrl(m_bucket, Library.Utility.Uri.UrlPathEncode(m_prefix + remotename)));
req.Method = "DELETE";
- return m_oauth.ReadJSONResponseAsync