diff --git a/.github/workflows/backendtests.yml b/.github/workflows/backendtests.yml
index 9b3148002..9cd5239df 100644
--- a/.github/workflows/backendtests.yml
+++ b/.github/workflows/backendtests.yml
@@ -14,6 +14,7 @@ jobs:
google_secrets_available: "${{ steps.check_google.outputs.available }}"
pcloud_secrets_available: "${{ steps.check_pcloud.outputs.available }}"
b2_secrets_available: "${{ steps.check_b2.outputs.available }}"
+ box_secrets_available: "${{ steps.check_box.outputs.available }}"
steps:
- id: check_pcloud
name: Check pCloud secrets
@@ -120,6 +121,20 @@ jobs:
echo "available=false" >> $GITHUB_OUTPUT
fi
echo "B2 check completed"
+ - id: check_box
+ name: Check Box.com secrets
+ shell: bash
+ run: |
+ echo "Starting Box.com secrets check..."
+ if [[ -n "${{ secrets.TESTCREDENTIAL_BOX_FOLDER }}" ]] && \
+ [[ -n "${{ secrets.TESTCREDENTIAL_BOX_AUTHID }}" ]]; then
+ echo "All Box.com secrets found"
+ echo "available=true" >> $GITHUB_OUTPUT
+ else
+ echo "Missing some Box.com secrets"
+ echo "available=false" >> $GITHUB_OUTPUT
+ fi
+ echo "Box.com check completed"
- name: Debug Output
shell: bash
run: |
@@ -130,6 +145,7 @@ jobs:
echo "pCloud Available: ${{ steps.check_pcloud.outputs.available }}"
echo "Azure Available: ${{ steps.check_azure.outputs.available }}"
echo "B2 Available: ${{ steps.check_b2.outputs.available }}"
+ echo "Box.com Available: ${{ steps.check_box.outputs.available }}"
test_ftp:
needs: check_secrets
if: needs.check_secrets.outputs.testcontainers_secrets_available == 'true'
@@ -468,40 +484,76 @@ jobs:
--logger:"console;verbosity=detailed"
LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
test_b2:
- needs: check_secrets
- if: needs.check_secrets.outputs.b2_secrets_available == 'true'
- name: Backblaze B2 Tests
- runs-on: "${{ matrix.os }}"
- strategy:
- max-parallel: 1
- fail-fast: false
- matrix:
- os:
- - ubuntu-latest
- - windows-latest
- - macos-latest
- steps:
- - name: Set up .NET
- uses: actions/setup-dotnet@v4
- with:
- dotnet-version: 8.x
- - name: Checkout source
- uses: actions/checkout@v4
- - name: Restore NuGet dependencies
- run: >-
- dotnet restore
- LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
- - name: Build project
- run: >-
- dotnet build --no-restore
- LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
- - name: Run B2 tests
- env:
- TESTCREDENTIAL_B2_BUCKET: "${{ secrets.TESTCREDENTIAL_B2_BUCKET }}"
- TESTCREDENTIAL_B2_FOLDER: "${{ secrets.TESTCREDENTIAL_B2_FOLDER }}"
- TESTCREDENTIAL_B2_USERNAME: "${{ secrets.TESTCREDENTIAL_B2_USERNAME }}"
- TESTCREDENTIAL_B2_PASSWORD: "${{ secrets.TESTCREDENTIAL_B2_PASSWORD }}"
- run: >-
- dotnet test --no-build --filter="ClassName~B2Tests"
- --logger:"console;verbosity=detailed"
- LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
\ No newline at end of file
+ needs: check_secrets
+ if: needs.check_secrets.outputs.b2_secrets_available == 'true'
+ name: Backblaze B2 Tests
+ runs-on: "${{ matrix.os }}"
+ strategy:
+ max-parallel: 1
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-latest
+ - windows-latest
+ - macos-latest
+ steps:
+ - name: Set up .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.x
+ - name: Checkout source
+ uses: actions/checkout@v4
+ - name: Restore NuGet dependencies
+ run: >-
+ dotnet restore
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ - name: Build project
+ run: >-
+ dotnet build --no-restore
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ - name: Run B2 tests
+ env:
+ TESTCREDENTIAL_B2_BUCKET: "${{ secrets.TESTCREDENTIAL_B2_BUCKET }}"
+ TESTCREDENTIAL_B2_FOLDER: "${{ secrets.TESTCREDENTIAL_B2_FOLDER }}"
+ TESTCREDENTIAL_B2_USERNAME: "${{ secrets.TESTCREDENTIAL_B2_USERNAME }}"
+ TESTCREDENTIAL_B2_PASSWORD: "${{ secrets.TESTCREDENTIAL_B2_PASSWORD }}"
+ run: >-
+ dotnet test --no-build --filter="ClassName~B2Tests"
+ --logger:"console;verbosity=detailed"
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ test_box:
+ needs: check_secrets
+ if: needs.check_secrets.outputs.box_secrets_available == 'true'
+ name: Box.com Tests
+ runs-on: "${{ matrix.os }}"
+ strategy:
+ max-parallel: 1
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-latest
+ - windows-latest
+ - macos-latest
+ steps:
+ - name: Set up .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.x
+ - name: Checkout source
+ uses: actions/checkout@v4
+ - name: Restore NuGet dependencies
+ run: >-
+ dotnet restore
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ - name: Build project
+ run: >-
+ dotnet build --no-restore
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
+ - name: Run Box.com tests
+ env:
+ TESTCREDENTIAL_BOX_FOLDER: "${{ secrets.TESTCREDENTIAL_BOX_FOLDER }}"
+ TESTCREDENTIAL_BOX_AUTHID: "${{ secrets.TESTCREDENTIAL_BOX_AUTHID }}"
+ run: >-
+ dotnet test --no-build --filter="ClassName~Box.BoxTests"
+ --logger:"console;verbosity=detailed"
+ LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
diff --git a/Duplicati/Agent/Program.cs b/Duplicati/Agent/Program.cs
index 12451ea3d..c02ecbdd0 100644
--- a/Duplicati/Agent/Program.cs
+++ b/Duplicati/Agent/Program.cs
@@ -34,7 +34,7 @@ using Duplicati.Library.RemoteControl;
using Duplicati.Library.RestAPI;
using Duplicati.Library.Utility;
using Duplicati.Server;
-using Duplicati.WebserverCore.Abstractions;
+using Duplicati.WebserverCore.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Duplicati.Agent;
@@ -46,12 +46,6 @@ public static class Program
///
private static readonly string LogTag = Log.LogTagFromType(typeof(Program));
- ///
- /// Create a random pre-shared key for the agent
- ///
- private static string? PreSharedKey = null;
-
-
///
/// Commandline arguments for the agent
///
@@ -275,7 +269,7 @@ public static class Program
// Set the pre-shared key for the agent
if (!agentConfig.DisablePreSharedKey)
- WebserverCore.Middlewares.PreSharedKeyFilter.PreSharedKey = PreSharedKey = RandomNumberGenerator.GetHexString(128);
+ WebserverCore.Middlewares.PreSharedKeyFilter.PreSharedKey = RandomNumberGenerator.GetHexString(128);
using var cts = new CancellationTokenSource();
@@ -313,8 +307,10 @@ public static class Program
settings.CertificateUrl,
settings.ServerCertificates,
cts.Token,
- c => ReKey(agentConfig, c),
- (m) => OnMessage(m, agentConfig)
+ OnConnect,
+ m => ReKey(m, agentConfig),
+ OnControl,
+ OnMessage
)
);
@@ -324,24 +320,19 @@ public static class Program
}
}
- private static async Task OnMessage(KeepRemoteConnection.CommandMessage message, CommandLineArguments agentConfig)
- {
- Log.WriteMessage(LogMessageType.Verbose, LogTag, "OnMessage", "Received message: {0}", message);
-
- var provider = FIXMEGlobal.Provider.GetRequiredService();
- var token = provider.CreateAccessToken("agent", provider.TemporaryFamilyId, TimeSpan.FromMinutes(2));
- using var httpClient = FIXMEGlobal.Provider.GetRequiredService().CreateClient();
- httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
- httpClient.BaseAddress = new System.Uri($"http://127.0.0.1:{agentConfig.WebservicePort}");
-
- if (!string.IsNullOrWhiteSpace(WebserverCore.Middlewares.PreSharedKeyFilter.PreSharedKey))
- httpClient.DefaultRequestHeaders.Add(WebserverCore.Middlewares.PreSharedKeyFilter.HeaderName, PreSharedKey);
-
- await message.Handle(httpClient);
- }
-
- private static Task ReKey(CommandLineArguments agentConfig, ClaimedClientData keydata)
+ private static Task> OnConnect(Dictionary metadata)
+ => FIXMEGlobal.Provider.GetRequiredService().OnConnect(metadata);
+
+ private static Task OnControl(KeepRemoteConnection.ControlMessage message)
+ => FIXMEGlobal.Provider.GetRequiredService().OnControl(message);
+
+ private static Task OnMessage(KeepRemoteConnection.CommandMessage message)
+ => FIXMEGlobal.Provider.GetRequiredService().OnMessage(message);
+
+
+ private static Task ReKey(ClaimedClientData keydata, CommandLineArguments agentConfig)
{
+ // ReKey is handled here because we store the config outside of the database
Log.WriteMessage(LogMessageType.Verbose, LogTag, "ReKey", "Rekeying the settings");
var settings = Settings.Load(agentConfig.AgentSettingsFile.FullName, agentConfig.AgentSettingsFilePassphrase);
if (!string.IsNullOrWhiteSpace(keydata.JWT) && settings.JWT != keydata.JWT)
diff --git a/Duplicati/CommandLine/CLI/Commands.cs b/Duplicati/CommandLine/CLI/Commands.cs
index 76425af0c..9aa3a8e99 100644
--- a/Duplicati/CommandLine/CLI/Commands.cs
+++ b/Duplicati/CommandLine/CLI/Commands.cs
@@ -668,6 +668,7 @@ namespace Duplicati.CommandLine
if (parsedStats.KnownFileCount > 0)
{
output.MessageEvent(string.Format(" Remote files: {0}", parsedStats.KnownFileCount));
+ output.MessageEvent(string.Format(" Remote filesets: {0}", parsedStats.KnownFilesets));
output.MessageEvent(string.Format(" Remote size: {0}", Library.Utility.Utility.FormatSizeString(parsedStats.KnownFileSize)));
}
diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs
index d9e924630..2529f79f4 100644
--- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs
+++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs
@@ -224,7 +224,7 @@ No password provided, unable to connect to server, exiting");
if (hosted != null && Server.Program.ApplicationInstance != null)
Server.Program.ApplicationInstance.SecondInstanceDetected +=
new Server.SingleInstance.SecondInstanceDelegate(
- x => { tk.ShowUrlInWindow(serverURL.ToString()); });
+ x => tk.ShowStatusWindow());
// TODO: If we change to hosted browser this should be a callback
if (openui)
diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayIconBase.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayIconBase.cs
index 1b769718f..d6815b21f 100644
--- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayIconBase.cs
+++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayIconBase.cs
@@ -180,7 +180,7 @@ namespace Duplicati.GUI.TrayIcon
});
}
- protected void ShowStatusWindow()
+ public void ShowStatusWindow()
{
Program.Connection.GetStatusWindowURLAsync().ContinueWith(t =>
{
diff --git a/Duplicati/Library/Backend/Backblaze/B2.cs b/Duplicati/Library/Backend/Backblaze/B2.cs
index 8d72ed7c0..4ed35e91b 100644
--- a/Duplicati/Library/Backend/Backblaze/B2.cs
+++ b/Duplicati/Library/Backend/Backblaze/B2.cs
@@ -272,7 +272,7 @@ public class B2 : IStreamingBackend, ITimeoutExemptBackend
_uploadUrl = await _b2AuthHelper.PostAndGetJsonDataAsync(
$"{_b2AuthHelper.ApiUrl}/b2api/v1/b2_get_upload_url",
- new UploadUrlRequest { BucketID = GetBucketAsync(cancellationToken).Await().BucketID },
+ new UploadUrlRequest { BucketID = (await GetBucketAsync(cancellationToken)).BucketID },
combinedCancellationToken.Token
).ConfigureAwait(false);
@@ -487,7 +487,7 @@ public class B2 : IStreamingBackend, ITimeoutExemptBackend
$"{_b2AuthHelper.ApiUrl}/b2api/v1/b2_list_file_versions",
new ListFilesRequest
{
- BucketID = GetBucketAsync(cancellationToken).Await().BucketID,
+ BucketID = (await GetBucketAsync(cancellationToken)).BucketID,
MaxFileCount = _pageSize,
Prefix = _prefix,
StartFileID = nextFileId,
diff --git a/Duplicati/Library/Interface/ResultInterfaces.cs b/Duplicati/Library/Interface/ResultInterfaces.cs
index 1848e98ca..0c886c43b 100644
--- a/Duplicati/Library/Interface/ResultInterfaces.cs
+++ b/Duplicati/Library/Interface/ResultInterfaces.cs
@@ -1,22 +1,22 @@
-// 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;
@@ -38,7 +38,7 @@ namespace Duplicati.Library.Interface
DateTime BeginTime { get; }
DateTime EndTime { get; }
TimeSpan Duration { get; }
-
+
IEnumerable Errors { get; }
IEnumerable Warnings { get; }
IEnumerable Messages { get; }
@@ -64,6 +64,7 @@ namespace Duplicati.Library.Interface
long UnknownFileCount { get; set; }
long KnownFileCount { get; set; }
long KnownFileSize { get; set; }
+ long KnownFilesets { get; set; }
DateTime LastBackupDate { get; set; }
long BackupListCount { get; set; }
long TotalQuotaSpace { get; set; }
@@ -116,14 +117,14 @@ namespace Duplicati.Library.Interface
IEnumerable LogMessages { get; }
IEnumerable RemoteVolumes { get; }
}
-
+
public interface IDeleteResults : IBasicResults
{
IEnumerable> DeletedSets { get; }
ICompactResults CompactResults { get; }
bool Dryrun { get; }
}
-
+
public interface IBackupResults : IBasicResults, IBackendStatsticsReporter
{
long DeletedFiles { get; }
@@ -143,16 +144,16 @@ namespace Duplicati.Library.Interface
long ModifiedFolders { get; }
long ModifiedSymlinks { get; }
long AddedSymlinks { get; }
- long DeletedSymlinks { get; }
+ long DeletedSymlinks { get; }
bool PartialBackup { get; }
bool Dryrun { get; }
-
+
ICompactResults CompactResults { get; }
IVacuumResults VacuumResults { get; }
IDeleteResults DeleteResults { get; }
IRepairResults RepairResults { get; }
}
-
+
public interface IRestoreResults : IBasicResults
{
long RestoredFiles { get; }
@@ -163,10 +164,10 @@ namespace Duplicati.Library.Interface
long DeletedFiles { get; }
long DeletedFolders { get; }
long DeletedSymlinks { get; }
-
+
IRecreateDatabaseResults RecreateDatabaseResults { get; }
}
-
+
public interface IRecreateDatabaseResults : IBasicResults
{
}
@@ -188,23 +189,23 @@ namespace Duplicati.Library.Interface
IVacuumResults VacuumResults { get; }
}
-
+
public interface ICreateLogDatabaseResults : IBasicResults
{
string TargetPath { get; }
}
-
+
public interface IRestoreControlFilesResults : IBasicResults
{
IEnumerable Files { get; }
}
-
+
public interface IRepairResults : IBasicResults
{
IRecreateDatabaseResults RecreateDatabaseResults { get; }
}
-
-
+
+
///
/// The possible change types for an entry
///
@@ -222,8 +223,8 @@ namespace Duplicati.Library.Interface
/// The element was modified
///
Modified
- }
-
+ }
+
///
/// The possible entry types
///
@@ -242,16 +243,16 @@ namespace Duplicati.Library.Interface
///
File
}
-
+
public interface IListChangesResults : IBasicResults
{
DateTime BaseVersionTimestamp { get; }
DateTime CompareVersionTimestamp { get; }
long BaseVersionIndex { get; }
long CompareVersionIndex { get; }
-
- IEnumerable> ChangeDetails { get; }
-
+
+ IEnumerable> ChangeDetails { get; }
+
long AddedFolders { get; }
long AddedSymlinks { get; }
long AddedFiles { get; }
@@ -263,14 +264,14 @@ namespace Duplicati.Library.Interface
long ModifiedFolders { get; }
long ModifiedSymlinks { get; }
long ModifiedFiles { get; }
-
+
long PreviousSize { get; }
long CurrentSize { get; }
long AddedSize { get; }
long DeletedSize { get; }
}
-
+
///
/// The status of a change in a test entry
///
@@ -298,7 +299,7 @@ namespace Duplicati.Library.Interface
{
IEnumerable>>> Verifications { get; }
}
-
+
public interface ITestFilterResults : IBasicResults
{
long FileSize { get; set; }
diff --git a/Duplicati/Library/Localization/ILocalizationService.cs b/Duplicati/Library/Localization/ILocalizationService.cs
index c0c35f262..f9eeb825f 100644
--- a/Duplicati/Library/Localization/ILocalizationService.cs
+++ b/Duplicati/Library/Localization/ILocalizationService.cs
@@ -1,31 +1,35 @@
-// 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 System.Collections.Generic;
+using System.Globalization;
namespace Duplicati.Library.Localization
{
public interface ILocalizationService
{
+ ///
+ /// The culture of the localization service
+ ///
+ public CultureInfo Culture { get; }
+
///
/// Localizes the string similar to how string.Format works
///
diff --git a/Duplicati/Library/Localization/LocalizationContext.cs b/Duplicati/Library/Localization/LocalizationContext.cs
index d2162bd05..9fed23cbb 100644
--- a/Duplicati/Library/Localization/LocalizationContext.cs
+++ b/Duplicati/Library/Localization/LocalizationContext.cs
@@ -1,26 +1,26 @@
-// 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 Duplicati.Library.Logging;
using System;
+using System.Threading;
namespace Duplicati.Library.Localization
{
@@ -29,10 +29,14 @@ namespace Duplicati.Library.Localization
///
internal class LocalizationContext : IDisposable
{
+ ///
+ /// The current culture, using an async local to keep the call context
+ ///
+ private static readonly AsyncLocal _currentCulture = new();
///
/// The previous context
///
- private readonly object m_prev;
+ private readonly string m_prev;
///
/// Flag to prevent double dispose
@@ -45,12 +49,17 @@ namespace Duplicati.Library.Localization
/// The localization to use.
public LocalizationContext(System.Globalization.CultureInfo ci)
{
- m_prev = CallContext.GetData(LocalizationService.LOGICAL_CONTEXT_KEY) as string;
- CallContext.SetData(LocalizationService.LOGICAL_CONTEXT_KEY, ci.Name);
+ m_prev = _currentCulture.Value;
+ _currentCulture.Value = ci.Name;
m_isDisposed = false;
}
+ ///
+ /// Returns the current culture value for the call context
+ ///
+ public static string Current => _currentCulture.Value;
+
///
/// Releases all resource used by the object.
///
@@ -64,7 +73,7 @@ namespace Duplicati.Library.Localization
{
if (!m_isDisposed)
{
- CallContext.SetData(LocalizationService.LOGICAL_CONTEXT_KEY, m_prev);
+ _currentCulture.Value = m_prev;
m_isDisposed = true;
}
}
diff --git a/Duplicati/Library/Localization/LocalizationService.cs b/Duplicati/Library/Localization/LocalizationService.cs
index b86b67182..636a75217 100644
--- a/Duplicati/Library/Localization/LocalizationService.cs
+++ b/Duplicati/Library/Localization/LocalizationService.cs
@@ -1,22 +1,22 @@
-// 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;
@@ -24,7 +24,6 @@ using System.Linq;
using System.Globalization;
using System.Collections.Generic;
using System.Text.RegularExpressions;
-using Duplicati.Library.Localization.Short;
using Duplicati.Library.Logging;
namespace Duplicati.Library.Localization
@@ -39,11 +38,6 @@ namespace Duplicati.Library.Localization
///
private static readonly Dictionary Services = new Dictionary();
- ///
- /// The key for accessing logical context
- ///
- internal const string LOGICAL_CONTEXT_KEY = "DUPLICATI_LOCALIZATION_CULTURE_CONTEXT";
-
///
/// Regular expression to match a locale
///
@@ -97,13 +91,27 @@ namespace Duplicati.Library.Localization
{
get
{
- var lc = CallContext.GetData(LOGICAL_CONTEXT_KEY) as string;
+ var lc = LocalizationContext.Current;
if (!string.IsNullOrWhiteSpace(lc))
return Get(new CultureInfo(lc));
return Get(CultureInfo.CurrentCulture);
}
}
+ ///
+ /// Gets a localization provider with the current UI language
+ ///
+ public static ILocalizationService CurrentUI
+ {
+ get
+ {
+ var lc = LocalizationContext.Current;
+ if (!string.IsNullOrWhiteSpace(lc))
+ return Get(new CultureInfo(lc));
+ return Get(CultureInfo.CurrentUICulture);
+ }
+ }
+
///
/// Gets a localization provider with the OS install language
///
@@ -147,7 +155,7 @@ namespace Duplicati.Library.Localization
///
/// Returns true if the culture has localization support
///
- public static Boolean isCultureSupported(CultureInfo culture)
+ public static bool IsCultureSupported(CultureInfo culture)
{
return MoLocalizationService.isCultureSupported(culture);
}
diff --git a/Duplicati/Library/Localization/MoLocalizationService.cs b/Duplicati/Library/Localization/MoLocalizationService.cs
index e0a81fbbb..a5983cf77 100644
--- a/Duplicati/Library/Localization/MoLocalizationService.cs
+++ b/Duplicati/Library/Localization/MoLocalizationService.cs
@@ -75,6 +75,7 @@ namespace Duplicati.Library.Localization
/// The culture to find.
public MoLocalizationService(CultureInfo ci)
{
+ Culture = ci;
var filenames = new string[] {
// Load the specialized version first
string.Format("localization-{0}.mo", ci.Name.Replace('-', '_')),
@@ -117,6 +118,11 @@ namespace Duplicati.Library.Localization
}
}
+ ///
+ /// Gets the culture of the localization
+ ///
+ public CultureInfo Culture { get; }
+
///
/// Pre-processes the message to use Linux line endings
///
diff --git a/Duplicati/Library/Localization/MockLocalizationService.cs b/Duplicati/Library/Localization/MockLocalizationService.cs
index d0e894cf9..1e2ee86e9 100644
--- a/Duplicati/Library/Localization/MockLocalizationService.cs
+++ b/Duplicati/Library/Localization/MockLocalizationService.cs
@@ -1,31 +1,35 @@
-// 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 System.Collections.Generic;
+using System.Globalization;
namespace Duplicati.Library.Localization
{
public class MockLocalizationService : ILocalizationService
{
+ ///
+ /// The culture of the localization service
+ ///
+ public CultureInfo Culture => CultureInfo.InvariantCulture;
+
///
/// Localizes the string similar to how string.Format works
///
diff --git a/Duplicati/Library/Logging/CallContext.cs b/Duplicati/Library/Logging/CallContext.cs
deleted file mode 100644
index dfb6e8f36..000000000
--- a/Duplicati/Library/Logging/CallContext.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-// 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.Collections.Concurrent;
-using System.Threading;
-
-namespace Duplicati.Library.Logging
-{
- ///
- /// Provides a way to set contextual data that flows with the call and
- /// async context of a test or invocation.
- ///
- public static class CallContext
- {
- static ConcurrentDictionary> state = new ConcurrentDictionary>();
-
- ///
- /// Stores a given object and associates it with the specified name.
- ///
- /// The name with which to associate the new item in the call context.
- /// The object to store in the call context.
- public static void SetData(string name, object data) =>
- state.GetOrAdd(name, _ => new AsyncLocal