Merge branch 'master' into feature/httpclientmigration-idrivee2
This commit is contained in:
@@ -17,6 +17,7 @@ jobs:
|
||||
box_secrets_available: "${{ steps.check_box.outputs.available }}"
|
||||
filen_secrets_available: "${{ steps.check_filen.outputs.available }}"
|
||||
filejump_secrets_available: "${{ steps.check_filejump.outputs.available }}"
|
||||
onedrive_secrets_available: "${{ steps.check_onedrive.outputs.available }}"
|
||||
steps:
|
||||
- id: check_pcloud
|
||||
name: Check pCloud secrets
|
||||
@@ -156,7 +157,7 @@ jobs:
|
||||
name: Check Filejump secrets
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Starting Filen.io secrets check..."
|
||||
echo "Starting Filejump secrets check..."
|
||||
if [[ -n "${{ secrets.TESTCREDENTIAL_FILEJUMP_FOLDER }}" ]] && \
|
||||
[[ -n "${{ secrets.TESTCREDENTIAL_FILEJUMP_TOKEN }}" ]]; then
|
||||
echo "All Filejump secrets found"
|
||||
@@ -166,6 +167,20 @@ jobs:
|
||||
echo "available=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
echo "Filejump check completed"
|
||||
- id: check_onedrive
|
||||
name: Check OneDrive secrets
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Starting OneDrive secrets check..."
|
||||
if [[ -n "${{ secrets.TESTCREDENTIAL_ONEDRIVE_FOLDER }}" ]] && \
|
||||
[[ -n "${{ secrets.TESTCREDENTIAL_ONEDRIVE_AUTHID }}" ]]; then
|
||||
echo "All OneDrive secrets found"
|
||||
echo "available=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Missing some OneDrive secrets"
|
||||
echo "available=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
echo "OneDrive check completed"
|
||||
- name: Debug Output
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -178,6 +193,8 @@ jobs:
|
||||
echo "B2 Available: ${{ steps.check_b2.outputs.available }}"
|
||||
echo "Box.com Available: ${{ steps.check_box.outputs.available }}"
|
||||
echo "Filen.io Available: ${{ steps.check_filen.outputs.available }}"
|
||||
echo "Filejump Available: ${{ steps.check_filejump.outputs.available }}"
|
||||
echo "OneDrive Available: ${{ steps.check_onedrive.outputs.available }}"
|
||||
test_ftp:
|
||||
needs: check_secrets
|
||||
if: needs.check_secrets.outputs.testcontainers_secrets_available == 'true'
|
||||
@@ -662,3 +679,39 @@ jobs:
|
||||
dotnet test --no-build --filter="ClassName~Filejump.FilejumpTests"
|
||||
--logger:"console;verbosity=detailed"
|
||||
LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
|
||||
test_onedrive:
|
||||
needs: check_secrets
|
||||
if: needs.check_secrets.outputs.onedrive_secrets_available == 'true'
|
||||
name: OneDrive 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 OneDrive tests
|
||||
env:
|
||||
TESTCREDENTIAL_ONEDRIVE_FOLDER: "${{ secrets.TESTCREDENTIAL_ONEDRIVE_FOLDER }}"
|
||||
TESTCREDENTIAL_ONEDRIVE_AUTHID: "${{ secrets.TESTCREDENTIAL_ONEDRIVE_AUTHID }}"
|
||||
run: >-
|
||||
dotnet test --no-build --filter="ClassName~OneDrive.OneDriveTests"
|
||||
--logger:"console;verbosity=detailed"
|
||||
LiveTests/Duplicati.Backend.Tests/Duplicati.Backend.Tests.sln
|
||||
|
||||
@@ -21,8 +21,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine", "Du
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.WEBDAV", "Duplicati\Library\Backend\WEBDAV\Duplicati.Library.Backend.WEBDAV.csproj", "{BAE27510-8B5D-44B2-B33E-372A98908041}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.CloudFiles", "Duplicati\Library\Backend\CloudFiles\Duplicati.Library.Backend.CloudFiles.csproj", "{1BFAE226-8364-4086-825C-BB83F6F3EE4C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.BackendTester", "Duplicati\CommandLine\BackendTester\Duplicati.CommandLine.BackendTester.csproj", "{E7280DCA-7776-4A73-B9B5-41FD77FC8799}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Interface", "Duplicati\Library\Interface\Duplicati.Library.Interface.csproj", "{C5899F45-B0FF-483C-9D38-24A9FCAAB237}"
|
||||
@@ -234,10 +232,6 @@ Global
|
||||
{BAE27510-8B5D-44B2-B33E-372A98908041}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BAE27510-8B5D-44B2-B33E-372A98908041}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BAE27510-8B5D-44B2-B33E-372A98908041}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1BFAE226-8364-4086-825C-BB83F6F3EE4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1BFAE226-8364-4086-825C-BB83F6F3EE4C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1BFAE226-8364-4086-825C-BB83F6F3EE4C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1BFAE226-8364-4086-825C-BB83F6F3EE4C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E7280DCA-7776-4A73-B9B5-41FD77FC8799}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E7280DCA-7776-4A73-B9B5-41FD77FC8799}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E7280DCA-7776-4A73-B9B5-41FD77FC8799}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -553,7 +547,6 @@ Global
|
||||
{C03F6DFD-805A-4BE0-9338-64870ADDB4A2} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{81765A64-3661-4E3E-B850-2F6F87A51F74} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
|
||||
{BAE27510-8B5D-44B2-B33E-372A98908041} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{1BFAE226-8364-4086-825C-BB83F6F3EE4C} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{E7280DCA-7776-4A73-B9B5-41FD77FC8799} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
|
||||
{0CA86ECF-5BEC-4909-B4F6-110A03B30B92} = {566EBBDA-19A4-4056-A615-D901D57D2439}
|
||||
{C0270709-2A40-43B5-8CF1-69581B9FA2A1} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
|
||||
+20
-15
@@ -30,9 +30,9 @@ using Duplicati.Library.Interface;
|
||||
using Duplicati.Library.Logging;
|
||||
using Duplicati.Library.Main;
|
||||
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;
|
||||
|
||||
@@ -225,6 +225,9 @@ public static class Program
|
||||
if (string.IsNullOrWhiteSpace(agentConfig.SettingsEncryptionKey))
|
||||
agentConfig = agentConfig with { SettingsEncryptionKey = Environment.GetEnvironmentVariable(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME) };
|
||||
|
||||
// Prepare the application settings for the server
|
||||
var applicationSettings = new ApplicationSettings();
|
||||
|
||||
// Apply the secret provider, if present
|
||||
if (!string.IsNullOrWhiteSpace(agentConfig.SecretProvider))
|
||||
{
|
||||
@@ -245,7 +248,7 @@ public static class Program
|
||||
options["secret-provider-cache"] = agentConfig.SecretProviderCache.ToString();
|
||||
options["secret-provider-pattern"] = agentConfig.SecretProviderPattern;
|
||||
|
||||
FIXMEGlobal.SecretProvider = await SecretProviderHelper.ApplySecretProviderAsync([], [], options, TempFolder.SystemTempPath, null, CancellationToken.None);
|
||||
applicationSettings.SecretProvider = await SecretProviderHelper.ApplySecretProviderAsync([], [], options, TempFolder.SystemTempPath, null, CancellationToken.None);
|
||||
|
||||
// Apply the secret provider to the agent configuration
|
||||
foreach (var prop in agentProps)
|
||||
@@ -299,7 +302,7 @@ public static class Program
|
||||
}
|
||||
|
||||
var t = await Task.WhenAny(
|
||||
Task.Run(() => StartLocalServer(agentConfig, settings, cts.Token)),
|
||||
Task.Run(() => StartLocalServer(agentConfig, applicationSettings, settings, cts.Token)),
|
||||
KeepRemoteConnection.Start(
|
||||
settings.ServerUrl,
|
||||
settings.JWT,
|
||||
@@ -307,7 +310,7 @@ public static class Program
|
||||
settings.ServerCertificates,
|
||||
cts.Token,
|
||||
OnConnect,
|
||||
m => ReKey(m, agentConfig),
|
||||
m => ReKey(applicationSettings, m, agentConfig),
|
||||
OnControl,
|
||||
OnMessage
|
||||
)
|
||||
@@ -320,16 +323,16 @@ public static class Program
|
||||
}
|
||||
|
||||
private static Task<Dictionary<string, string?>> OnConnect(Dictionary<string, string?> metadata)
|
||||
=> FIXMEGlobal.Provider.GetRequiredService<IRemoteControllerHandler>().OnConnect(metadata);
|
||||
=> Server.Program.DuplicatiWebserver.Provider.GetRequiredService<IRemoteControllerHandler>().OnConnect(metadata);
|
||||
|
||||
private static Task OnControl(KeepRemoteConnection.ControlMessage message)
|
||||
=> FIXMEGlobal.Provider.GetRequiredService<IRemoteControllerHandler>().OnControl(message);
|
||||
=> Server.Program.DuplicatiWebserver.Provider.GetRequiredService<IRemoteControllerHandler>().OnControl(message);
|
||||
|
||||
private static Task OnMessage(KeepRemoteConnection.CommandMessage message)
|
||||
=> FIXMEGlobal.Provider.GetRequiredService<IRemoteControllerHandler>().OnMessage(message);
|
||||
=> Server.Program.DuplicatiWebserver.Provider.GetRequiredService<IRemoteControllerHandler>().OnMessage(message);
|
||||
|
||||
|
||||
private static Task ReKey(ClaimedClientData keydata, CommandLineArguments agentConfig)
|
||||
private static Task ReKey(IApplicationSettings applicationSettings, 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");
|
||||
@@ -340,7 +343,7 @@ public static class Program
|
||||
settings = settings with { ServerCertificates = keydata.ServerCertificates };
|
||||
|
||||
// Only allow re-keying if the settings encryption key is not set
|
||||
if (!FIXMEGlobal.SettingsEncryptionKeyProvidedExternally)
|
||||
if (!applicationSettings.SettingsEncryptionKeyProvidedExternally)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(keydata.LocalEncryptionKey) && settings.SettingsEncryptionKey != keydata.LocalEncryptionKey)
|
||||
{
|
||||
@@ -358,14 +361,15 @@ public static class Program
|
||||
/// <summary>
|
||||
/// Runs the server, restarting on crashes
|
||||
/// </summary>
|
||||
/// <param name="applicationSettings">The application settings for the server</param>
|
||||
/// <param name="args">The commandline arguments passed to the server</param>
|
||||
/// <param name="cancellationToken">The cancellation token to use for the process</param>
|
||||
/// <returns>An awaitable task</returns>
|
||||
private static async Task RunServer(string[] args, CancellationToken cancellationToken)
|
||||
private static async Task RunServer(IApplicationSettings applicationSettings, string[] args, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.Register(() =>
|
||||
{
|
||||
Server.Program.ApplicationExitEvent.Set();
|
||||
applicationSettings.ApplicationExitEvent.Set();
|
||||
});
|
||||
|
||||
var lastRestart = DateTime.Now;
|
||||
@@ -374,7 +378,7 @@ public static class Program
|
||||
try
|
||||
{
|
||||
lastRestart = DateTime.Now;
|
||||
Server.Program.Main(args);
|
||||
Server.Program.Main(applicationSettings, args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -445,10 +449,11 @@ public static class Program
|
||||
/// Starts the local webserver in locked down mode, restarting on crashes
|
||||
/// </summary>
|
||||
/// <param name="agentConfig">The agent configuration</param>
|
||||
/// <param name="applicationSettings">The application settings for the server</param>
|
||||
/// <param name="settings">The settings for the agent</param>
|
||||
/// <param name="cancellationToken">The cancellation token that stops the server</param>
|
||||
/// <returns>An awaitable task</returns>
|
||||
private static async Task StartLocalServer(CommandLineArguments agentConfig, Settings settings, CancellationToken cancellationToken)
|
||||
private static async Task StartLocalServer(CommandLineArguments agentConfig, IApplicationSettings applicationSettings, Settings settings, CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO: Look into pipes for Kestrel to prevent network access
|
||||
|
||||
@@ -487,10 +492,10 @@ public static class Program
|
||||
.ToArray();
|
||||
|
||||
// Set the global origin
|
||||
FIXMEGlobal.Origin = "Agent";
|
||||
applicationSettings.Origin = "Agent";
|
||||
|
||||
// Start the server
|
||||
await RunServer(args, cancellationToken);
|
||||
await RunServer(applicationSettings, args, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ using System.Net.Security;
|
||||
using System.Text.Json;
|
||||
using Duplicati.Library.AutoUpdater;
|
||||
using Duplicati.WebserverCore.Middlewares;
|
||||
using Duplicati.WebserverCore.Services;
|
||||
|
||||
namespace Duplicati.CommandLine.ServerUtil;
|
||||
|
||||
@@ -141,7 +142,6 @@ public class Connection
|
||||
/// <returns>The connection</returns>
|
||||
public static async Task<Connection> Connect(Settings settings, bool obtainRefreshToken = false, OutputInterceptor? console = null)
|
||||
{
|
||||
|
||||
if (console != null)
|
||||
console.AppendConsoleMessage($"Connecting to {settings.HostUrl}...");
|
||||
else
|
||||
@@ -201,7 +201,7 @@ public class Connection
|
||||
if (File.Exists(Path.Combine(DataFolderManager.GetDataFolder(DataFolderManager.AccessMode.ProbeOnly), DataFolderManager.SERVER_DATABASE_FILENAME)))
|
||||
{
|
||||
string? cfg = null;
|
||||
using (var connection = Server.Program.GetDatabaseConnection(opts, true))
|
||||
using (var connection = Server.Program.GetDatabaseConnection(new ApplicationSettings(), opts, true))
|
||||
{
|
||||
cfg = connection.ApplicationSettings.JWTConfig;
|
||||
if (settings.HostUrl.Scheme == "https" && connection.ApplicationSettings.ServerSSLCertificate != null && trustedCertificateHashes.Count == 0)
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
|
||||
namespace Duplicati.GUI.TrayIcon
|
||||
{
|
||||
@@ -32,9 +33,11 @@ namespace Duplicati.GUI.TrayIcon
|
||||
private readonly System.Threading.Thread m_runner;
|
||||
private Exception m_runnerException = null;
|
||||
public Action InstanceShutdown;
|
||||
private readonly IApplicationSettings applicationSettings;
|
||||
|
||||
public HostedInstanceKeeper(string[] args)
|
||||
public HostedInstanceKeeper(IApplicationSettings applicationSettings, string[] args)
|
||||
{
|
||||
this.applicationSettings = applicationSettings;
|
||||
m_runner = new System.Threading.Thread(_ =>
|
||||
{
|
||||
try
|
||||
@@ -44,13 +47,13 @@ namespace Duplicati.GUI.TrayIcon
|
||||
if (args == null || !args.Any(x => x.Trim().StartsWith("--" + Server.WebServerLoader.OPTION_PORT + "=", StringComparison.OrdinalIgnoreCase)))
|
||||
args = (args ?? new string[0]).Union(new string[] { "--" + Server.WebServerLoader.OPTION_PORT + "=8200,8300,8400,8500,8600,8700,8800,8900,8989" }).ToArray();
|
||||
|
||||
Server.Program.Main(args);
|
||||
Server.Program.Main(applicationSettings, args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
m_runnerException = ex;
|
||||
Server.Program.ServerStartedEvent?.Set();
|
||||
Server.Program.ApplicationExitEvent?.Set();
|
||||
applicationSettings.ApplicationExitEvent?.Set();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -85,7 +88,7 @@ namespace Duplicati.GUI.TrayIcon
|
||||
{
|
||||
try
|
||||
{
|
||||
Server.Program.ApplicationExitEvent.Set();
|
||||
applicationSettings.ApplicationExitEvent.Set();
|
||||
if (!m_runner.Join(TimeSpan.FromSeconds(10)))
|
||||
{
|
||||
m_runner.Interrupt();
|
||||
|
||||
@@ -334,9 +334,10 @@ namespace Duplicati.GUI.TrayIcon
|
||||
private async Task ObtainAccessTokenAsync()
|
||||
{
|
||||
// If we host the server, issue the access token from the service
|
||||
if (FIXMEGlobal.IsServerStarted && m_passwordSource == Program.PasswordSource.HostedServer)
|
||||
var sp = Server.Program.DuplicatiWebserver?.Provider;
|
||||
if (sp != null && m_passwordSource == Program.PasswordSource.HostedServer)
|
||||
{
|
||||
var provider = FIXMEGlobal.Provider.GetRequiredService<IJWTTokenProvider>();
|
||||
var provider = sp.GetRequiredService<IJWTTokenProvider>();
|
||||
m_accesstoken = provider.CreateAccessToken("trayicon", provider.TemporaryFamilyId);
|
||||
return;
|
||||
}
|
||||
@@ -412,12 +413,13 @@ namespace Duplicati.GUI.TrayIcon
|
||||
string signinjwt = null;
|
||||
|
||||
// If we host the server, issue the token from the service
|
||||
if (FIXMEGlobal.IsServerStarted && m_passwordSource == Program.PasswordSource.HostedServer)
|
||||
var sp = Server.Program.DuplicatiWebserver?.Provider;
|
||||
if (sp != null && m_passwordSource == Program.PasswordSource.HostedServer)
|
||||
{
|
||||
if (FIXMEGlobal.DataConnection.ApplicationSettings.DisableSigninTokens)
|
||||
if (sp.GetRequiredService<Server.Database.Connection>().ApplicationSettings.DisableSigninTokens)
|
||||
return null;
|
||||
|
||||
signinjwt = FIXMEGlobal.Provider.GetRequiredService<IJWTTokenProvider>().CreateSigninToken("trayicon");
|
||||
signinjwt = sp.GetRequiredService<IJWTTokenProvider>().CreateSigninToken("trayicon");
|
||||
}
|
||||
|
||||
// If we have database access, grab the issuer key from the db and issue a token
|
||||
|
||||
@@ -30,9 +30,11 @@ using CoCoL;
|
||||
using Duplicati.CommandLine;
|
||||
using Duplicati.Library.AutoUpdater;
|
||||
using Duplicati.Library.Interface;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Server;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.WebserverCore.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Uri = System.Uri;
|
||||
|
||||
namespace Duplicati.GUI.TrayIcon
|
||||
@@ -144,12 +146,13 @@ namespace Duplicati.GUI.TrayIcon
|
||||
try
|
||||
{
|
||||
// Tell the hosted server it was started by the TrayIcon
|
||||
FIXMEGlobal.Origin = "Tray icon";
|
||||
var applicationSettings = new ApplicationSettings();
|
||||
applicationSettings.Origin = "Tray icon";
|
||||
passwordSource = PasswordSource.HostedServer;
|
||||
// Ignore TrayIcon specific settings
|
||||
foreach (var c in BasicSupportedCommands.Select(x => x.Name))
|
||||
Server.Program.ValidationIgnoredOptions.Add(c);
|
||||
hosted = new HostedInstanceKeeper(_args);
|
||||
hosted = new HostedInstanceKeeper(applicationSettings, _args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -164,17 +167,18 @@ namespace Duplicati.GUI.TrayIcon
|
||||
|
||||
// We have a hosted server, if this is the first run,
|
||||
// we should open the main page
|
||||
openui = Server.Program.IsFirstRun || Server.Program.ServerPortChanged;
|
||||
var connection = Server.Program.DuplicatiWebserver.Provider.GetRequiredService<Connection>();
|
||||
openui = connection.ApplicationSettings.IsFirstRun || connection.ApplicationSettings.ServerPortChanged;
|
||||
|
||||
var scheme = Server.Program.DataConnection.ApplicationSettings.UseHTTPS ? "https" : "http";
|
||||
var scheme = connection.ApplicationSettings.UseHTTPS ? "https" : "http";
|
||||
serverURL = new UriBuilder(serverURL)
|
||||
{
|
||||
Port = Server.Program.ServerPort,
|
||||
Port = Server.Program.DuplicatiWebserver.Port,
|
||||
Scheme = scheme
|
||||
}.Uri;
|
||||
|
||||
if (Server.Program.DataConnection.ApplicationSettings.UseHTTPS && string.IsNullOrWhiteSpace(acceptedHostCertificate))
|
||||
acceptedHostCertificate = Server.Program.DataConnection.ApplicationSettings.ServerSSLCertificate?.FirstOrDefault(x => x.HasPrivateKey)?.GetCertHashString();
|
||||
if (connection.ApplicationSettings.UseHTTPS && string.IsNullOrWhiteSpace(acceptedHostCertificate))
|
||||
acceptedHostCertificate = connection.ApplicationSettings.ServerSSLCertificate?.FirstOrDefault(x => x.HasPrivateKey)?.GetCertHashString();
|
||||
|
||||
}
|
||||
else if (Utility.ParseBoolOption(options, READCONFIGFROMDB_OPTION))
|
||||
@@ -182,7 +186,7 @@ namespace Duplicati.GUI.TrayIcon
|
||||
if (File.Exists(Path.Combine(DataFolderManager.GetDataFolder(DataFolderManager.AccessMode.ReadWritePermissionSet), DataFolderManager.SERVER_DATABASE_FILENAME)))
|
||||
{
|
||||
passwordSource = PasswordSource.Database;
|
||||
databaseConnection = Server.Program.GetDatabaseConnection(options, true);
|
||||
databaseConnection = Server.Program.GetDatabaseConnection(new ApplicationSettings(), options, true);
|
||||
|
||||
if (databaseConnection != null)
|
||||
{
|
||||
@@ -267,8 +271,9 @@ No password provided, unable to connect to server, exiting");
|
||||
}
|
||||
|
||||
tk.ShowUrlInWindow(t.Result);
|
||||
Server.Program.IsFirstRun = false;
|
||||
Server.Program.ServerPortChanged = false;
|
||||
var connection = Server.Program.DuplicatiWebserver.Provider.GetRequiredService<Connection>();
|
||||
connection.ApplicationSettings.IsFirstRun = false;
|
||||
connection.ApplicationSettings.ServerPortChanged = false;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@@ -198,9 +198,14 @@ namespace Duplicati.Library.Backend.AliyunOSS
|
||||
var client = GetClient();
|
||||
try
|
||||
{
|
||||
var metadata = new ObjectMetadata
|
||||
{
|
||||
ContentLength = stream.Length
|
||||
};
|
||||
|
||||
using var timeoutStream = stream.ObserveReadTimeout(_timeouts.ReadWriteTimeout, false);
|
||||
var objectResult = await Task.Factory.FromAsync(
|
||||
(cb, state) => client.BeginPutObject(bucketName, objectName, timeoutStream, cb, state),
|
||||
(cb, state) => client.BeginPutObject(bucketName, objectName, timeoutStream, metadata, cb, state),
|
||||
client.EndPutObject,
|
||||
null).ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -310,43 +310,8 @@ public class B2 : IStreamingBackend
|
||||
|
||||
public async Task PutAsync(string remotename, Stream stream, CancellationToken cancelToken)
|
||||
{
|
||||
TempFile? tmp = null;
|
||||
|
||||
var measure = stream;
|
||||
while (measure is OverrideableStream os &&
|
||||
os.GetType().GetField("m_basestream", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(os) is Stream baseStream)
|
||||
{
|
||||
measure = baseStream;
|
||||
}
|
||||
|
||||
if (measure == null)
|
||||
throw new Exception($"Unable to unwrap stream from: {stream.GetType()}");
|
||||
|
||||
string sha1;
|
||||
if (measure.CanSeek)
|
||||
{
|
||||
var p = measure.Position;
|
||||
|
||||
// Compute the hash
|
||||
using (var hashalg = HashFactory.CreateHasher("SHA1"))
|
||||
sha1 = Utility.Utility.ByteArrayAsHexString(hashalg.ComputeHash(measure));
|
||||
|
||||
measure.Position = p;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No seeking possible, use a temp file
|
||||
tmp = new TempFile();
|
||||
await using (var sr = File.OpenWrite(tmp))
|
||||
using (var hasher = HashFactory.CreateHasher("SHA1"))
|
||||
await using (var hc = new HashCalculatingStream(measure, hasher))
|
||||
{
|
||||
await Utility.Utility.CopyStreamAsync(hc, sr, cancelToken).ConfigureAwait(false);
|
||||
sha1 = hc.GetFinalHashString();
|
||||
}
|
||||
|
||||
stream = File.OpenRead(tmp);
|
||||
}
|
||||
(stream, var sha1, var tmp) = await Utility.Utility.CalculateThrottledStreamHash(stream, "SHA1", cancelToken).ConfigureAwait(false);
|
||||
using var _ = tmp;
|
||||
|
||||
if (_filecache == null)
|
||||
await RebuildFileCache(cancelToken).ConfigureAwait(false);
|
||||
@@ -400,10 +365,6 @@ public class B2 : IStreamingBackend
|
||||
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
tmp?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,450 +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 Duplicati.Library.Common.IO;
|
||||
using Duplicati.Library.Interface;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Library.Utility.Options;
|
||||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Duplicati.Library.Backend
|
||||
{
|
||||
// ReSharper disable once UnusedMember.Global
|
||||
// This class is instantiated dynamically in the BackendLoader.
|
||||
public class CloudFiles : IBackend, IStreamingBackend
|
||||
{
|
||||
/// <summary>
|
||||
/// The log tag for this class
|
||||
/// </summary>
|
||||
private static readonly string LOGTAG = Logging.Log.LogTagFromType<CloudFiles>();
|
||||
|
||||
public const string AUTH_URL_US = "https://identity.api.rackspacecloud.com/auth";
|
||||
public const string AUTH_URL_UK = "https://lon.auth.api.rackspacecloud.com/v1.0";
|
||||
private const string DUMMY_HOSTNAME = "api.mosso.com";
|
||||
|
||||
private const string AUTH_USERNAME_OPTION = "cloudfiles-username";
|
||||
private const string AUTH_PASSWORD_OPTION = "cloudfiles-accesskey";
|
||||
|
||||
private const int ITEM_LIST_LIMIT = 1000;
|
||||
private readonly string m_username;
|
||||
private readonly string m_password;
|
||||
private readonly string m_path;
|
||||
|
||||
private string? m_storageUrl = null;
|
||||
private string? m_authToken = null;
|
||||
private readonly string m_authUrl;
|
||||
private readonly TimeoutOptionsHelper.Timeouts m_timeouts;
|
||||
|
||||
// ReSharper disable once UnusedMember.Global
|
||||
// This constructor is needed by the BackendLoader.
|
||||
public CloudFiles()
|
||||
{
|
||||
m_username = null!;
|
||||
m_password = null!;
|
||||
m_path = null!;
|
||||
m_authUrl = null!;
|
||||
m_timeouts = null!;
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedMember.Global
|
||||
// This constructor is needed by the BackendLoader.
|
||||
public CloudFiles(string url, Dictionary<string, string?> options)
|
||||
{
|
||||
var uri = new Utility.Uri(url);
|
||||
var auth = AuthOptionsHelper.ParseWithAlias(options, uri, AUTH_USERNAME_OPTION, AUTH_PASSWORD_OPTION);
|
||||
|
||||
if (!auth.HasUsername)
|
||||
throw new UserInformationException(Strings.CloudFiles.NoUserIDError, "CloudFilesNoUserID");
|
||||
if (auth.HasPassword)
|
||||
throw new UserInformationException(Strings.CloudFiles.NoAPIKeyError, "CloudFilesNoApiKey");
|
||||
|
||||
(m_username, m_password) = auth.GetCredentials();
|
||||
|
||||
//Fallback to the previous format
|
||||
if (url.Contains(DUMMY_HOSTNAME))
|
||||
{
|
||||
Logging.Log.WriteWarningMessage(LOGTAG, "CloudFilesDeprecatedFormat", null, Strings.CloudFiles.DeprecatedFormatWarning(DUMMY_HOSTNAME));
|
||||
var u = new System.Uri(url);
|
||||
|
||||
if (!string.IsNullOrEmpty(u.UserInfo))
|
||||
{
|
||||
if (u.UserInfo.IndexOf(":", StringComparison.Ordinal) >= 0)
|
||||
{
|
||||
m_username = u.UserInfo.Substring(0, u.UserInfo.IndexOf(":", StringComparison.Ordinal));
|
||||
m_password = u.UserInfo.Substring(u.UserInfo.IndexOf(":", StringComparison.Ordinal) + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_username = u.UserInfo;
|
||||
}
|
||||
}
|
||||
|
||||
//We use the api.mosso.com hostname.
|
||||
//This allows the use of containers that have names that are not valid hostnames,
|
||||
// such as container names with spaces in them
|
||||
if (u.Host.Equals(DUMMY_HOSTNAME))
|
||||
m_path = Library.Utility.Uri.UrlDecode(u.PathAndQuery);
|
||||
else
|
||||
m_path = u.Host + Library.Utility.Uri.UrlDecode(u.PathAndQuery);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_path = uri.HostAndPath;
|
||||
}
|
||||
|
||||
if (m_path.EndsWith("/", StringComparison.Ordinal))
|
||||
m_path = m_path.Substring(0, m_path.Length - 1);
|
||||
if (!m_path.StartsWith("/", StringComparison.Ordinal))
|
||||
m_path = "/" + m_path;
|
||||
|
||||
var authUrl = options.GetValueOrDefault("cloudfiles-authentication-url");
|
||||
if (string.IsNullOrEmpty(authUrl))
|
||||
authUrl = Utility.Utility.ParseBoolOption(options, "cloudfiles-uk-account") ? AUTH_URL_UK : AUTH_URL_US;
|
||||
m_authUrl = authUrl;
|
||||
m_timeouts = TimeoutOptionsHelper.Parse(options);
|
||||
}
|
||||
|
||||
#region IBackend Members
|
||||
|
||||
public string DisplayName
|
||||
{
|
||||
get { return Strings.CloudFiles.DisplayName; }
|
||||
}
|
||||
|
||||
public string ProtocolKey
|
||||
{
|
||||
get { return "cloudfiles"; }
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<IFileEntry> ListAsync([EnumeratorCancellation] CancellationToken cancelToken)
|
||||
{
|
||||
var extraUrl = "?format=xml&limit=" + ITEM_LIST_LIMIT.ToString();
|
||||
var markerUrl = "";
|
||||
|
||||
bool repeat;
|
||||
do
|
||||
{
|
||||
var doc = new System.Xml.XmlDocument();
|
||||
var req = await CreateRequest("", extraUrl + markerUrl, cancelToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await Utility.Utility.WithTimeout(m_timeouts.ListTimeout, cancelToken, _ =>
|
||||
{
|
||||
var areq = new Utility.AsyncHttpRequest(req);
|
||||
using (var resp = (HttpWebResponse)areq.GetResponse())
|
||||
using (var s = areq.GetResponseStream())
|
||||
doc.Load(s);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch (WebException wex)
|
||||
{
|
||||
if (markerUrl == "") //Only check on first iteration
|
||||
if (wex.Response is HttpWebResponse response && response.StatusCode == HttpStatusCode.NotFound)
|
||||
throw new FolderMissingException(wex);
|
||||
|
||||
//Other error, just re-throw
|
||||
throw;
|
||||
}
|
||||
|
||||
var lst = doc.SelectNodes("container/object");
|
||||
|
||||
//Perhaps the folder does not exist?
|
||||
//The response should be 404 from the server, but it is not :(
|
||||
if (lst == null || lst.Count == 0 && markerUrl == "") //Only on first iteration
|
||||
{
|
||||
try { await CreateFolderAsync(cancelToken).ConfigureAwait(false); }
|
||||
catch { } //Ignore
|
||||
}
|
||||
|
||||
if (lst == null)
|
||||
yield break;
|
||||
|
||||
var lastItemName = "";
|
||||
foreach (System.Xml.XmlNode n in lst)
|
||||
{
|
||||
var name = n["name"]?.InnerText;
|
||||
if (!long.TryParse(n["bytes"]?.InnerText, out var size))
|
||||
size = -1;
|
||||
if (!DateTime.TryParse(n["last_modified"]?.InnerText, out var mod))
|
||||
mod = new DateTime();
|
||||
|
||||
lastItemName = name;
|
||||
yield return new FileEntry(name, size, mod, mod);
|
||||
}
|
||||
|
||||
repeat = lst.Count == ITEM_LIST_LIMIT;
|
||||
|
||||
if (repeat)
|
||||
markerUrl = "&marker=" + Library.Utility.Uri.UrlEncode(lastItemName);
|
||||
|
||||
} while (repeat);
|
||||
}
|
||||
|
||||
public async Task PutAsync(string remotename, string filename, CancellationToken cancelToken)
|
||||
{
|
||||
using (var fs = File.OpenRead(filename))
|
||||
await PutAsync(remotename, fs, cancelToken);
|
||||
}
|
||||
|
||||
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 = await CreateRequest("/" + remotename, "", cancelToken).ConfigureAwait(false);
|
||||
await Utility.Utility.WithTimeout(m_timeouts.ShortTimeout, cancelToken, _ =>
|
||||
{
|
||||
req.Method = "DELETE";
|
||||
var areq = new AsyncHttpRequest(req);
|
||||
using (var resp = (HttpWebResponse)areq.GetResponse())
|
||||
{
|
||||
if (resp.StatusCode == HttpStatusCode.NotFound)
|
||||
throw new FileMissingException();
|
||||
|
||||
if ((int)resp.StatusCode >= 300)
|
||||
throw new WebException(Strings.CloudFiles.FileDeleteError, null, WebExceptionStatus.ProtocolError, resp);
|
||||
else
|
||||
using (areq.GetResponseStream())
|
||||
{ }
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch (WebException wex)
|
||||
{
|
||||
if (wex.Response is HttpWebResponse response && response.StatusCode == HttpStatusCode.NotFound)
|
||||
throw new FileMissingException(wex);
|
||||
else
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public IList<ICommandLineArgument> SupportedCommands =>
|
||||
[
|
||||
new CommandLineArgument(AUTH_USERNAME_OPTION, CommandLineArgument.ArgumentType.String, Strings.CloudFiles.DescriptionUsernameShort, Strings.CloudFiles.DescriptionUsernameLong, null, [AuthOptionsHelper.AuthUsernameOption] ),
|
||||
new CommandLineArgument(AUTH_PASSWORD_OPTION, CommandLineArgument.ArgumentType.Password, Strings.CloudFiles.DescriptionPasswordShort, Strings.CloudFiles.DescriptionPasswordLong, null, [AuthOptionsHelper.AuthPasswordOption]),
|
||||
new CommandLineArgument("cloudfiles-uk-account", CommandLineArgument.ArgumentType.Boolean, Strings.CloudFiles.DescriptionUKAccountShort, Strings.CloudFiles.DescriptionUKAccountLong("cloudfiles-authentication-url", AUTH_URL_UK)),
|
||||
new CommandLineArgument("cloudfiles-authentication-url", CommandLineArgument.ArgumentType.String, Strings.CloudFiles.DescriptionAuthenticationURLShort, Strings.CloudFiles.DescriptionAuthenticationURLLong_v2("cloudfiles-uk-account"), AUTH_URL_US),
|
||||
.. TimeoutOptionsHelper.GetOptions(),
|
||||
];
|
||||
|
||||
public string Description
|
||||
{
|
||||
get { return Strings.CloudFiles.Description_v2; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IBackend_v2 Members
|
||||
|
||||
public Task TestAsync(CancellationToken cancelToken) =>
|
||||
//The "Folder not found" is not detectable :(
|
||||
this.TestListAsync(cancelToken);
|
||||
|
||||
public async Task CreateFolderAsync(CancellationToken cancelToken)
|
||||
{
|
||||
var createReq = await CreateRequest("", "", cancelToken).ConfigureAwait(false);
|
||||
await Utility.Utility.WithTimeout(m_timeouts.ShortTimeout, cancelToken, _ =>
|
||||
{
|
||||
createReq.Method = "PUT";
|
||||
var areq = new AsyncHttpRequest(createReq);
|
||||
using (var resp = (HttpWebResponse)areq.GetResponse())
|
||||
{ }
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable Members
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamingBackend Members
|
||||
|
||||
public Task<string[]> GetDNSNamesAsync(CancellationToken cancelToken) => Task.FromResult(
|
||||
new string?[] {
|
||||
new System.Uri(m_authUrl).Host,
|
||||
string.IsNullOrWhiteSpace(m_storageUrl) ? null : new System.Uri(m_storageUrl).Host
|
||||
}
|
||||
.WhereNotNullOrWhiteSpace()
|
||||
.ToArray()
|
||||
);
|
||||
|
||||
public async Task GetAsync(string remotename, System.IO.Stream stream, CancellationToken cancelToken)
|
||||
{
|
||||
var req = await CreateRequest("/" + remotename, "", cancelToken).ConfigureAwait(false);
|
||||
req.Method = "GET";
|
||||
|
||||
var areq = new AsyncHttpRequest(req);
|
||||
using (var resp = areq.GetResponse())
|
||||
using (var s = await Utility.Utility.WithTimeout(m_timeouts.ShortTimeout, cancelToken, _ => areq.GetResponseStream()).ConfigureAwait(false))
|
||||
using (var timeoutStream = s.ObserveReadTimeout(m_timeouts.ReadWriteTimeout))
|
||||
using (var hasher = MD5.Create())
|
||||
using (var mds = new HashCalculatingStream(timeoutStream, hasher))
|
||||
{
|
||||
var md5Hash = resp.Headers["ETag"];
|
||||
await Utility.Utility.CopyStreamAsync(mds, stream, true, cancelToken).ConfigureAwait(false);
|
||||
|
||||
if (!string.Equals(mds.GetFinalHashString(), md5Hash, StringComparison.OrdinalIgnoreCase))
|
||||
throw new Exception(Strings.CloudFiles.ETagVerificationError);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PutAsync(string remotename, System.IO.Stream stream, CancellationToken cancelToken)
|
||||
{
|
||||
var req = await CreateRequest("/" + remotename, "", cancelToken).ConfigureAwait(false);
|
||||
req.Method = "PUT";
|
||||
req.ContentType = "application/octet-stream";
|
||||
|
||||
try { req.ContentLength = stream.Length; }
|
||||
catch { }
|
||||
|
||||
// TODO: When reviewing this, lets build a common method to unwrap the stream passed from
|
||||
// BackendManager.PutOperation, so we can use the same logic to compute hashes in all backends
|
||||
|
||||
//If we can pre-calculate the MD5 hash before transmission, do so
|
||||
/*if (stream.CanSeek)
|
||||
{
|
||||
System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create();
|
||||
req.Headers["ETag"] = Core.Utility.ByteArrayAsHexString(md5.ComputeHash(stream)).ToLower(System.Globalization.CultureInfo.InvariantCulture);
|
||||
stream.Seek(0, System.IO.SeekOrigin.Begin);
|
||||
|
||||
using (System.IO.Stream s = req.GetRequestStream())
|
||||
Core.Utility.CopyStream(stream, s);
|
||||
|
||||
//Reset the timeout to the default value of 100 seconds to
|
||||
// avoid blocking the GetResponse() call
|
||||
req.Timeout = 100000;
|
||||
|
||||
//The server handles the eTag verification for us, and gives an error if the hash was a mismatch
|
||||
using (HttpWebResponse resp = (HttpWebResponse)req.GetResponse())
|
||||
if ((int)resp.StatusCode >= 300)
|
||||
throw new WebException(Strings.CloudFiles.FileUploadError, null, WebExceptionStatus.ProtocolError, resp);
|
||||
|
||||
}
|
||||
else //Otherwise use a client-side calculation
|
||||
*/
|
||||
//TODO: We cannot use the local MD5 calculation, because that could involve a throttled read,
|
||||
// and may invoke various events
|
||||
{
|
||||
string? fileHash = null;
|
||||
|
||||
long streamLen = -1;
|
||||
try { streamLen = stream.Length; }
|
||||
catch { }
|
||||
|
||||
var areq = new AsyncHttpRequest(req);
|
||||
using (var s = areq.GetRequestStream(streamLen))
|
||||
using (var timeoutStream = s.ObserveWriteTimeout(m_timeouts.ReadWriteTimeout))
|
||||
using (var hasher = MD5.Create())
|
||||
using (var mds = new HashCalculatingStream(timeoutStream, hasher))
|
||||
{
|
||||
await Utility.Utility.CopyStreamAsync(stream, mds, tryRewindSource: true, cancelToken: cancelToken);
|
||||
fileHash = mds.GetFinalHashString();
|
||||
}
|
||||
|
||||
string? md5Hash = null;
|
||||
|
||||
//We need to verify the eTag locally
|
||||
try
|
||||
{
|
||||
using (HttpWebResponse resp = (HttpWebResponse)areq.GetResponse())
|
||||
if ((int)resp.StatusCode >= 300)
|
||||
throw new WebException(Strings.CloudFiles.FileUploadError, null, WebExceptionStatus.ProtocolError, resp);
|
||||
else
|
||||
md5Hash = resp.Headers["ETag"];
|
||||
}
|
||||
catch (WebException wex)
|
||||
{
|
||||
//Catch 404 and turn it into a FolderNotFound error
|
||||
if (wex.Response is HttpWebResponse response && response.StatusCode == HttpStatusCode.NotFound)
|
||||
throw new FolderMissingException(wex);
|
||||
|
||||
//Other error, just re-throw
|
||||
throw;
|
||||
}
|
||||
|
||||
|
||||
if (md5Hash == null || !string.Equals(md5Hash, fileHash, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
//Remove the broken file
|
||||
try { await DeleteAsync(remotename, cancelToken); }
|
||||
catch { }
|
||||
|
||||
throw new Exception(Strings.CloudFiles.ETagVerificationError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task<HttpWebRequest> CreateRequest(string remotename, string query, CancellationToken cancelToken)
|
||||
{
|
||||
//If this is the first call, get an authentication token
|
||||
if (string.IsNullOrEmpty(m_authToken) || string.IsNullOrEmpty(m_storageUrl))
|
||||
{
|
||||
await Utility.Utility.WithTimeout(m_timeouts.ShortTimeout, cancelToken, _ =>
|
||||
{
|
||||
var authReq = (HttpWebRequest)HttpWebRequest.Create(m_authUrl);
|
||||
authReq.Headers.Add("X-Auth-User", m_username);
|
||||
authReq.Headers.Add("X-Auth-Key", m_password);
|
||||
authReq.Method = "GET";
|
||||
|
||||
var areq = new AsyncHttpRequest(authReq);
|
||||
using (var resp = areq.GetResponse())
|
||||
{
|
||||
m_storageUrl = resp.Headers["X-Storage-Url"];
|
||||
m_authToken = resp.Headers["X-Auth-Token"];
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(m_authToken) || string.IsNullOrEmpty(m_storageUrl))
|
||||
throw new Exception(Strings.CloudFiles.UnexpectedResponseError);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var req = (HttpWebRequest)HttpWebRequest.Create(m_storageUrl + UrlEncode(m_path + remotename) + query);
|
||||
req.Headers.Add("X-Auth-Token", UrlEncode(m_authToken!));
|
||||
|
||||
req.UserAgent = "Duplicati CloudFiles Backend v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
|
||||
req.KeepAlive = false;
|
||||
req.PreAuthenticate = true;
|
||||
req.AllowWriteStreamBuffering = false;
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
private static string UrlEncode(string value)
|
||||
{
|
||||
return Utility.Uri.UrlEncode(value).Replace("+", "%20").Replace("%2f", "/");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Library</OutputType>
|
||||
<RootNamespace>Duplicati.Library.Backend</RootNamespace>
|
||||
<Copyright>Copyright © 2025 Team Duplicati, MIT license</Copyright>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Interface\Duplicati.Library.Interface.csproj" />
|
||||
<ProjectReference Include="..\..\Localization\Duplicati.Library.Localization.csproj" />
|
||||
<ProjectReference Include="..\..\Utility\Duplicati.Library.Utility.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.DotNet.Analyzers.Compatibility" Version="0.2.12-alpha">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,44 +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 Duplicati.Library.Localization.Short;
|
||||
namespace Duplicati.Library.Backend.Strings
|
||||
{
|
||||
internal static class CloudFiles
|
||||
{
|
||||
public static string Description_v2 { get { return LC.L(@"This backend can read and write data to CloudFiles. Allowed format is ""cloudfiles://container/folder""."); } }
|
||||
public static string DisplayName { get { return LC.L(@"Rackspace CloudFiles"); } }
|
||||
public static string DescriptionAuthenticationURLLong_v2(string optionname) { return LC.L(@"CloudFiles use different servers for authentication based on where the account resides. Use this option to set an alternate authentication URL. This option overrides --{0}.", optionname); }
|
||||
public static string DescriptionAuthenticationURLShort { get { return LC.L(@"Provide another authentication URL"); } }
|
||||
public static string DescriptionPasswordLong { get { return LC.L(@"The API Access Key used to authenticate with CloudFiles."); } }
|
||||
public static string DescriptionPasswordShort { get { return LC.L(@"Supply the access key used to connect to the server"); } }
|
||||
public static string DescriptionUKAccountLong(string optionname, string optionvalue) { return LC.L(@"Duplicati will assume that the credentials given are for a US account. Use this option if the account is a UK based account. Note that this is equivalent to setting --{0}={1}.", optionname, optionvalue); }
|
||||
public static string DescriptionUKAccountShort { get { return LC.L(@"Use a UK account"); } }
|
||||
public static string DescriptionUsernameLong { get { return LC.L(@"The username used to authenticate with CloudFiles."); } }
|
||||
public static string DescriptionUsernameShort { get { return LC.L(@"Supply the username used to authenticate with CloudFiles"); } }
|
||||
public static string ETagVerificationError { get { return LC.L(@"MD5 Hash (ETag) verification failed"); } }
|
||||
public static string FileDeleteError { get { return LC.L(@"Failed to delete file"); } }
|
||||
public static string FileUploadError { get { return LC.L(@"Failed to upload file"); } }
|
||||
public static string NoAPIKeyError { get { return LC.L(@"No CloudFiles API Access Key given"); } }
|
||||
public static string NoUserIDError { get { return LC.L(@"No CloudFiles userID given"); } }
|
||||
public static string UnexpectedResponseError { get { return LC.L(@"Unexpected CloudFiles response. Perhaps the API has changed?"); } }
|
||||
public static string DeprecatedFormatWarning(string hostname) { return LC.L(@$"The URL is using the marker hostname {hostname} which is no longer supported. Please use the new format ""cloudfiles://container/folder"" instead."); }
|
||||
}
|
||||
}
|
||||
@@ -216,7 +216,7 @@ namespace Duplicati.Library.Backend
|
||||
var (accessKeyId, accessKeySecret) = _auth.GetCredentials();
|
||||
|
||||
var host = await GetRegionEndpointAsync("https://api.idrivee2.com/api/service/get_region_end_point/" + accessKeyId, cancellationToken).ConfigureAwait(false);
|
||||
_s3Client = new S3AwsClient(accessKeyId, accessKeySecret, null, host, null, true, false, _timeouts, _options);
|
||||
_s3Client = new S3AwsClient(accessKeyId, accessKeySecret, null, host, null, true, false, false, _timeouts, _options);
|
||||
|
||||
return _s3Client;
|
||||
}
|
||||
|
||||
@@ -489,108 +489,71 @@ namespace Duplicati.Library.Backend
|
||||
// we specify the file size in a custom header. And if the stream is not seek-able we are not able
|
||||
// to use stream.Length, so we are back at square one.
|
||||
|
||||
// TODO: This unwrapping code is shared with CloudFiles
|
||||
TempFile? tmpFile = null;
|
||||
Stream? baseStream = stream;
|
||||
while (baseStream is OverrideableStream)
|
||||
baseStream = typeof(OverrideableStream).GetField("m_basestream", System.Reflection.BindingFlags.DeclaredOnly | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)?.GetValue(baseStream) as System.IO.Stream;
|
||||
if (baseStream == null)
|
||||
throw new Exception(string.Format("Unable to unwrap stream from: {0}", stream.GetType()));
|
||||
string md5Hash;
|
||||
if (baseStream.CanSeek)
|
||||
{
|
||||
var originalPosition = baseStream.Position;
|
||||
using (var md5 = MD5.Create())
|
||||
md5Hash = Utility.Utility.ByteArrayAsHexString(md5.ComputeHash(baseStream));
|
||||
baseStream.Position = originalPosition;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No seeking possible, use a temp file
|
||||
tmpFile = new TempFile();
|
||||
using (var os = File.OpenWrite(tmpFile))
|
||||
using (var hasher = MD5.Create())
|
||||
using (var md5 = new HashCalculatingStream(baseStream, hasher))
|
||||
{
|
||||
await Utility.Utility.CopyStreamAsync(md5, os, true, cancelToken).ConfigureAwait(false);
|
||||
md5Hash = md5.GetFinalHashString();
|
||||
}
|
||||
stream = File.OpenRead(tmpFile);
|
||||
}
|
||||
try
|
||||
{
|
||||
// Create request, with query parameter, and a few custom headers.
|
||||
// NB: If we wanted to we could send the same POST request as below but without the file contents
|
||||
// and with "cphash=[md5Hash]" as the only query parameter. Then we will get an HTTP 200 (OK) response
|
||||
// if an identical file already exists, and we can skip uploading the new file. We will get
|
||||
// HTTP 404 (Not Found) if file does not exists or it exists with a different hash, in which
|
||||
// case we must send a new request to upload the new content.
|
||||
var fileSize = stream.Length;
|
||||
var req = CreateRequest(WebRequestMethods.Http.Post, remotename, "umode=nomultipart", true);
|
||||
req.Headers.Add("JMd5", md5Hash); // Not required, but it will make the server verify the content and mark the file as corrupt if there is a mismatch.
|
||||
req.Headers.Add("JSize", fileSize.ToString()); // Required, and used to mark file as incomplete if we upload something be the total size of the original file!
|
||||
// File time stamp headers: Since we are working with a stream here we do not know the local file's timestamps,
|
||||
// and then we can just omit the JCreated and JModified and let the server automatically set the current time.
|
||||
//req.Headers.Add("JCreated", timeCreated);
|
||||
//req.Headers.Add("JModified", timeModified);
|
||||
req.ContentType = "application/octet-stream";
|
||||
req.ContentLength = fileSize;
|
||||
(stream, var md5Hash, var tmp) = await Utility.Utility.CalculateThrottledStreamHash(stream, "MD5", cancelToken).ConfigureAwait(false);
|
||||
using var _ = tmp;
|
||||
|
||||
// Write post data request
|
||||
var areq = new AsyncHttpRequest(req);
|
||||
using (var rs = areq.GetRequestStream())
|
||||
using (var ts = rs.ObserveWriteTimeout(m_timeouts.ReadWriteTimeout))
|
||||
await Utility.Utility.CopyStreamAsync(stream, rs, true, cancelToken).ConfigureAwait(false);
|
||||
// Send request, and check response
|
||||
using (var resp = (HttpWebResponse)areq.GetResponse())
|
||||
{
|
||||
if (resp.StatusCode != HttpStatusCode.Created)
|
||||
throw new WebException(Strings.Jottacloud.FileUploadError, null, WebExceptionStatus.ProtocolError, resp);
|
||||
// Create request, with query parameter, and a few custom headers.
|
||||
// NB: If we wanted to we could send the same POST request as below but without the file contents
|
||||
// and with "cphash=[md5Hash]" as the only query parameter. Then we will get an HTTP 200 (OK) response
|
||||
// if an identical file already exists, and we can skip uploading the new file. We will get
|
||||
// HTTP 404 (Not Found) if file does not exists or it exists with a different hash, in which
|
||||
// case we must send a new request to upload the new content.
|
||||
var fileSize = stream.Length;
|
||||
var req = CreateRequest(WebRequestMethods.Http.Post, remotename, "umode=nomultipart", true);
|
||||
req.Headers.Add("JMd5", md5Hash); // Not required, but it will make the server verify the content and mark the file as corrupt if there is a mismatch.
|
||||
req.Headers.Add("JSize", fileSize.ToString()); // Required, and used to mark file as incomplete if we upload something be the total size of the original file!
|
||||
// File time stamp headers: Since we are working with a stream here we do not know the local file's timestamps,
|
||||
// and then we can just omit the JCreated and JModified and let the server automatically set the current time.
|
||||
//req.Headers.Add("JCreated", timeCreated);
|
||||
//req.Headers.Add("JModified", timeModified);
|
||||
req.ContentType = "application/octet-stream";
|
||||
req.ContentLength = fileSize;
|
||||
|
||||
// Request seems to be successful, but we must verify the response XML content to be sure that the file
|
||||
// was correctly uploaded: The server will verify the JSize header and mark the file as incomplete if
|
||||
// there was mismatch, and it will verify the JMd5 header and mark the file as corrupt if there was a hash
|
||||
// mismatch. The returned XML contains a file element, and if upload was error free it contains a single
|
||||
// child element "currentRevision", which has a "state" child element with the string "COMPLETED".
|
||||
// If there was a problem we should have a "latestRevision" child element, and this will have state with
|
||||
// value "INCOMPLETE" or "CORRUPT". If the file was new or had no previous complete versions the latestRevision
|
||||
// will be the only child, but if not there may also be a "currentRevision" representing the previous
|
||||
// complete version - and then we need to detect the case where our upload failed but there was an existing
|
||||
// complete version!
|
||||
using (var rs = areq.GetResponseStream())
|
||||
// Write post data request
|
||||
var areq = new AsyncHttpRequest(req);
|
||||
using (var rs = areq.GetRequestStream())
|
||||
using (var ts = rs.ObserveWriteTimeout(m_timeouts.ReadWriteTimeout))
|
||||
await Utility.Utility.CopyStreamAsync(stream, rs, true, cancelToken).ConfigureAwait(false);
|
||||
// Send request, and check response
|
||||
using (var resp = (HttpWebResponse)areq.GetResponse())
|
||||
{
|
||||
if (resp.StatusCode != HttpStatusCode.Created)
|
||||
throw new WebException(Strings.Jottacloud.FileUploadError, null, WebExceptionStatus.ProtocolError, resp);
|
||||
|
||||
// Request seems to be successful, but we must verify the response XML content to be sure that the file
|
||||
// was correctly uploaded: The server will verify the JSize header and mark the file as incomplete if
|
||||
// there was mismatch, and it will verify the JMd5 header and mark the file as corrupt if there was a hash
|
||||
// mismatch. The returned XML contains a file element, and if upload was error free it contains a single
|
||||
// child element "currentRevision", which has a "state" child element with the string "COMPLETED".
|
||||
// If there was a problem we should have a "latestRevision" child element, and this will have state with
|
||||
// value "INCOMPLETE" or "CORRUPT". If the file was new or had no previous complete versions the latestRevision
|
||||
// will be the only child, but if not there may also be a "currentRevision" representing the previous
|
||||
// complete version - and then we need to detect the case where our upload failed but there was an existing
|
||||
// complete version!
|
||||
using (var rs = areq.GetResponseStream())
|
||||
{
|
||||
var doc = new System.Xml.XmlDocument();
|
||||
try { doc.Load(rs); }
|
||||
catch (System.Xml.XmlException)
|
||||
{
|
||||
var doc = new System.Xml.XmlDocument();
|
||||
try { doc.Load(rs); }
|
||||
catch (System.Xml.XmlException)
|
||||
{
|
||||
throw new WebException(Strings.Jottacloud.FileUploadError, WebExceptionStatus.ProtocolError);
|
||||
}
|
||||
bool uploadCompletedSuccessfully = false;
|
||||
var xFile = doc["file"];
|
||||
if (xFile != null)
|
||||
{
|
||||
var xRevState = xFile.SelectSingleNode("latestRevision");
|
||||
if (xRevState == null)
|
||||
{
|
||||
xRevState = xFile.SelectSingleNode("currentRevision/state");
|
||||
if (xRevState != null)
|
||||
uploadCompletedSuccessfully = xRevState.InnerText == "COMPLETED"; // Success: There is no "latestRevision", only a "currentRevision" (and it specifies the file is complete, but I think it always will).
|
||||
}
|
||||
}
|
||||
if (!uploadCompletedSuccessfully) // Report error (and we just let the incomplete/corrupt file revision stay on the server..)
|
||||
throw new WebException(Strings.Jottacloud.FileUploadError, WebExceptionStatus.ProtocolError);
|
||||
throw new WebException(Strings.Jottacloud.FileUploadError, WebExceptionStatus.ProtocolError);
|
||||
}
|
||||
bool uploadCompletedSuccessfully = false;
|
||||
var xFile = doc["file"];
|
||||
if (xFile != null)
|
||||
{
|
||||
var xRevState = xFile.SelectSingleNode("latestRevision");
|
||||
if (xRevState == null)
|
||||
{
|
||||
xRevState = xFile.SelectSingleNode("currentRevision/state");
|
||||
if (xRevState != null)
|
||||
uploadCompletedSuccessfully = xRevState.InnerText == "COMPLETED"; // Success: There is no "latestRevision", only a "currentRevision" (and it specifies the file is complete, but I think it always will).
|
||||
}
|
||||
}
|
||||
if (!uploadCompletedSuccessfully) // Report error (and we just let the incomplete/corrupt file revision stay on the server..)
|
||||
throw new WebException(Strings.Jottacloud.FileUploadError, WebExceptionStatus.ProtocolError);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (tmpFile != null)
|
||||
tmpFile.Dispose();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,9 +158,9 @@ namespace Duplicati.Library.Backend
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
using var timer = timeout == Timeout.InfiniteTimeSpan
|
||||
var timer = timeout == Timeout.InfiniteTimeSpan
|
||||
? new TaskCompletionSource<bool>().Task
|
||||
: Task.Delay(timeout);
|
||||
: Task.Delay(timeout, cancelToken);
|
||||
|
||||
var killed = false;
|
||||
while (!process.HasExited)
|
||||
|
||||
@@ -61,6 +61,10 @@ namespace Duplicati.Library.Backend
|
||||
/// The option to specify if chunk encoding should be used
|
||||
/// </summary>
|
||||
private readonly bool m_useChunkEncoding;
|
||||
/// <summary>
|
||||
/// The option to specify if payload signing should be disabled
|
||||
/// </summary>
|
||||
private readonly bool m_disablePayloadSigning;
|
||||
|
||||
/// <summary>
|
||||
/// The DNS host of the S3 server
|
||||
@@ -93,7 +97,7 @@ namespace Duplicati.Library.Backend
|
||||
]);
|
||||
|
||||
public S3AwsClient(string awsID, string awsKey, string? locationConstraint, string servername,
|
||||
string? storageClass, bool useSSL, bool disableChunkEncoding, TimeoutOptionsHelper.Timeouts timeouts, Dictionary<string, string?> options)
|
||||
string? storageClass, bool useSSL, bool disableChunkEncoding, bool disablePayloadSigning, TimeoutOptionsHelper.Timeouts timeouts, Dictionary<string, string?> options)
|
||||
{
|
||||
var cfg = GetDefaultAmazonS3Config();
|
||||
cfg.UseHttp = !useSSL;
|
||||
@@ -109,6 +113,7 @@ namespace Duplicati.Library.Backend
|
||||
m_storageClass = storageClass;
|
||||
m_dnsHost = string.IsNullOrWhiteSpace(cfg.ServiceURL) ? null : new System.Uri(cfg.ServiceURL).Host;
|
||||
m_useChunkEncoding = !disableChunkEncoding;
|
||||
m_disablePayloadSigning = disablePayloadSigning;
|
||||
m_archiveClasses = ParseStorageClasses(options.GetValueOrDefault(S3_ARCHIVE_CLASSES_OPTION));
|
||||
}
|
||||
|
||||
@@ -217,13 +222,20 @@ namespace Duplicati.Library.Backend
|
||||
public virtual async Task AddFileStreamAsync(string bucketName, string keyName, Stream source,
|
||||
CancellationToken cancelToken)
|
||||
{
|
||||
(source, var md5, var tmp) = await Utility.Utility.CalculateThrottledStreamHash(source, "MD5", cancelToken).ConfigureAwait(false);
|
||||
using var _ = tmp;
|
||||
|
||||
md5 = Convert.ToBase64String(Utility.Utility.HexStringAsByteArray(md5));
|
||||
|
||||
using var ts = source.ObserveReadTimeout(m_timeouts.ReadWriteTimeout, false);
|
||||
var objectAddRequest = new PutObjectRequest
|
||||
{
|
||||
BucketName = bucketName,
|
||||
Key = keyName,
|
||||
InputStream = ts,
|
||||
UseChunkEncoding = m_useChunkEncoding
|
||||
UseChunkEncoding = m_useChunkEncoding,
|
||||
MD5Digest = md5,
|
||||
DisablePayloadSigning = m_disablePayloadSigning
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(m_storageClass))
|
||||
objectAddRequest.StorageClass = new S3StorageClass(m_storageClass);
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace Duplicati.Library.Backend
|
||||
private const string SSL_OPTION = "use-ssl";
|
||||
private const string S3_CLIENT_OPTION = "s3-client";
|
||||
private const string S3_DISABLE_CHUNK_ENCODING_OPTION = "s3-disable-chunk-encoding";
|
||||
private const string S3_DISABLE_PAYLOAD_SIGNING_OPTION = "s3-disable-payload-signing";
|
||||
private const string S3_LIST_API_VERSION_OPTION = "s3-list-api-version";
|
||||
private const string S3_RECURSIVE_LIST = "s3-recursive-list";
|
||||
|
||||
@@ -242,14 +243,15 @@ namespace Duplicati.Library.Backend
|
||||
if (!options.ContainsKey("s3-ext-forcepathstyle") && !hostname.EndsWith(".amazonaws.com", StringComparison.OrdinalIgnoreCase))
|
||||
options["s3-ext-forcepathstyle"] = "true";
|
||||
|
||||
var disableChunkEncoding = Utility.Utility.ParseBoolOption(options, S3_DISABLE_CHUNK_ENCODING_OPTION);
|
||||
|
||||
var s3ClientOptionValue = options.GetValueOrDefault(S3_CLIENT_OPTION);
|
||||
|
||||
(var awsID, var awsKey) = auth.GetCredentials();
|
||||
if (string.IsNullOrWhiteSpace(s3ClientOptionValue) || string.Equals(s3ClientOptionValue, "aws", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
m_s3Client = new S3AwsClient(awsID, awsKey, locationConstraint, hostname, storageClass, useSSL, disableChunkEncoding, timeout, options);
|
||||
var disableChunkEncoding = Utility.Utility.ParseBoolOption(options, S3_DISABLE_CHUNK_ENCODING_OPTION);
|
||||
var disablePayloadSigning = Utility.Utility.ParseBoolOption(options, S3_DISABLE_PAYLOAD_SIGNING_OPTION);
|
||||
m_s3Client = new S3AwsClient(awsID, awsKey, locationConstraint, hostname, storageClass, useSSL, disableChunkEncoding, disablePayloadSigning, timeout, options);
|
||||
}
|
||||
else if (string.Equals(s3ClientOptionValue, "minio", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -328,14 +330,15 @@ namespace Duplicati.Library.Backend
|
||||
var exts = S3AwsClient.GetAwsExtendedOptions();
|
||||
|
||||
return [
|
||||
new CommandLineArgument("aws-access-key-id", CommandLineArgument.ArgumentType.String, Strings.S3Backend.AMZUserIDDescriptionShort, Strings.S3Backend.AMZUserIDDescriptionLong, null, [AuthOptionsHelper.AuthUsernameOption], null),
|
||||
new CommandLineArgument("aws-secret-access-key", CommandLineArgument.ArgumentType.Password, Strings.S3Backend.AMZKeyDescriptionShort, Strings.S3Backend.AMZKeyDescriptionLong,null, [AuthOptionsHelper.AuthPasswordOption], null ),
|
||||
new CommandLineArgument(AUTH_USERNAME_OPTION, CommandLineArgument.ArgumentType.String, Strings.S3Backend.AMZUserIDDescriptionShort, Strings.S3Backend.AMZUserIDDescriptionLong, null, [AuthOptionsHelper.AuthUsernameOption], null),
|
||||
new CommandLineArgument(AUTH_PASSWORD_OPTION, CommandLineArgument.ArgumentType.Password, Strings.S3Backend.AMZKeyDescriptionShort, Strings.S3Backend.AMZKeyDescriptionLong,null, [AuthOptionsHelper.AuthPasswordOption], null ),
|
||||
new CommandLineArgument(STORAGECLASS_OPTION, CommandLineArgument.ArgumentType.String, Strings.S3Backend.S3StorageclassDescriptionShort, Strings.S3Backend.S3StorageclassDescriptionLong, "", null, KNOWN_S3_STORAGE_CLASSES.Select(x => x.Value).WhereNotNullOrWhiteSpace().ToArray()),
|
||||
new CommandLineArgument(SERVER_NAME, CommandLineArgument.ArgumentType.String, Strings.S3Backend.S3ServerNameDescriptionShort, Strings.S3Backend.S3ServerNameDescriptionLong(hostnames.ToString()), DEFAULT_S3_HOST),
|
||||
new CommandLineArgument(LOCATION_OPTION, CommandLineArgument.ArgumentType.String, Strings.S3Backend.S3LocationDescriptionShort, Strings.S3Backend.S3LocationDescriptionLong(locations.ToString())),
|
||||
new CommandLineArgument(SSL_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.S3Backend.DescriptionUseSSLShort, Strings.S3Backend.DescriptionUseSSLLong),
|
||||
new CommandLineArgument(S3_CLIENT_OPTION, CommandLineArgument.ArgumentType.Enumeration, Strings.S3Backend.S3ClientDescriptionShort, Strings.S3Backend.S3ClientDescriptionLong, "aws", null, new string[] { "aws", "minio" }),
|
||||
new CommandLineArgument(S3_DISABLE_CHUNK_ENCODING_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.S3Backend.DescriptionDisableChunkEncodingShort, Strings.S3Backend.DescriptionDisableChunkEncodingLong, "false"),
|
||||
new CommandLineArgument(S3_DISABLE_PAYLOAD_SIGNING_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.S3Backend.DescriptionDisablePayloadSigningShort, Strings.S3Backend.DescriptionDisablePayloadSigningLong, "false"),
|
||||
new CommandLineArgument(S3AwsClient.S3_ARCHIVE_CLASSES_OPTION, CommandLineArgument.ArgumentType.Flags, Strings.S3Backend.S3ArchiveClassesDescriptionShort, Strings.S3Backend.S3ArchiveClassesDescriptionLong, string.Join(",", S3AwsClient.DEFAULT_ARCHIVE_CLASSES.Select(x => x.Value)), null, KNOWN_S3_STORAGE_CLASSES.Select(x => x.Value).WhereNotNullOrWhiteSpace().ToArray()),
|
||||
new CommandLineArgument(S3_LIST_API_VERSION_OPTION, CommandLineArgument.ArgumentType.Enumeration, Strings.S3Backend.DescriptionListApiVersionShort, Strings.S3Backend.DescriptionListApiVersionLong, "v1", null, ["v1", "v2"]),
|
||||
new CommandLineArgument(S3_RECURSIVE_LIST, CommandLineArgument.ArgumentType.Boolean, Strings.S3Backend.DescriptionRecursiveListShort, Strings.S3Backend.DescriptionRecursiveListLong, "false"),
|
||||
|
||||
@@ -47,6 +47,8 @@ namespace Duplicati.Library.Backend.Strings
|
||||
public static string DescriptionUseSSLShort { get { return LC.L(@"Instruct Duplicati to use an SSL (https) connection"); } }
|
||||
public static string DescriptionDisableChunkEncodingLong { get { return LC.L(@"This disables chunk encoding for the aws client, which is not supported by all S3 providers."); } }
|
||||
public static string DescriptionDisableChunkEncodingShort { get { return LC.L(@"Disable chunk encoding (aws client only)"); } }
|
||||
public static string DescriptionDisablePayloadSigningLong { get { return LC.L(@"This disables payload signing for the aws client, which is not supported by all S3 providers."); } }
|
||||
public static string DescriptionDisablePayloadSigningShort { get { return LC.L(@"Disable payload signing (aws client only)"); } }
|
||||
public static string S3StorageclassDescriptionLong { get { return LC.L(@"Use this option to specify a storage class. If this option is not used, the server will choose a default storage class."); } }
|
||||
public static string S3StorageclassDescriptionShort { get { return LC.L(@"Specify storage class"); } }
|
||||
public static string S3ArchiveClassesDescriptionShort { get { return LC.L(@"Specify archive storage class"); } }
|
||||
|
||||
@@ -42,7 +42,6 @@ public static class BackendModules
|
||||
new Backend.AzureBlob.AzureBlobBackend(),
|
||||
new Backend.Backblaze.B2(),
|
||||
new Backend.Box.BoxBackend(),
|
||||
new Backend.CloudFiles(),
|
||||
new Backend.Dropbox(),
|
||||
new Backend.FTP(),
|
||||
new Backend.AlternateFTPBackend(),
|
||||
|
||||
@@ -18,22 +18,18 @@
|
||||
// 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.Text;
|
||||
|
||||
namespace Duplicati.Library.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for a log destination
|
||||
/// </summary>
|
||||
public interface ILogDestination
|
||||
{
|
||||
/// <summary>
|
||||
/// The function called when a message is logged
|
||||
/// </summary>
|
||||
/// <param name="entry">The message to log</param>
|
||||
void WriteMessage(LogEntry entry);
|
||||
}
|
||||
}
|
||||
|
||||
namespace Duplicati.Library.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for a log destination
|
||||
/// </summary>
|
||||
public interface ILogDestination
|
||||
{
|
||||
/// <summary>
|
||||
/// The function called when a message is logged
|
||||
/// </summary>
|
||||
/// <param name="entry">The message to log</param>
|
||||
void WriteMessage(LogEntry entry);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -89,50 +89,56 @@ namespace Duplicati.Library.Main.Operation
|
||||
versionsToDelete = versionsToDelete.OrderBy(x => x.Version).Skip(1).ToList();
|
||||
}
|
||||
|
||||
if (versionsToDelete.Count > 0)
|
||||
Logging.Log.WriteInformationMessage(LOGTAG, "DeleteRemoteFileset", "Deleting {0} remote fileset(s) ...", versionsToDelete.Count);
|
||||
|
||||
var lst = db.DropFilesetsFromTable(versionsToDelete.Select(x => x.Time).ToArray(), rtr.Transaction).ToArray();
|
||||
foreach (var f in lst)
|
||||
db.UpdateRemoteVolume(f.Key, RemoteVolumeState.Deleting, f.Value, null, rtr.Transaction);
|
||||
|
||||
if (!m_options.Dryrun)
|
||||
rtr.Commit("CommitBeforeDelete");
|
||||
|
||||
foreach (var f in lst)
|
||||
if (versionsToDelete.Count == 0)
|
||||
{
|
||||
if (!await m_result.TaskControl.ProgressRendevouz().ConfigureAwait(false))
|
||||
{
|
||||
await backendManager.WaitForEmptyAsync(db, rtr.Transaction, m_result.TaskControl.ProgressToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_options.Dryrun)
|
||||
await backendManager.DeleteAsync(f.Key, f.Value, false, m_result.TaskControl.ProgressToken).ConfigureAwait(false);
|
||||
else
|
||||
Logging.Log.WriteDryrunMessage(LOGTAG, "WouldDeleteRemoteFileset", "Would delete remote fileset: {0}", f.Key);
|
||||
}
|
||||
|
||||
await backendManager.WaitForEmptyAsync(db, rtr.Transaction, m_result.TaskControl.ProgressToken).ConfigureAwait(false);
|
||||
|
||||
var count = lst.Length;
|
||||
if (!m_options.Dryrun)
|
||||
{
|
||||
if (count == 0)
|
||||
Logging.Log.WriteInformationMessage(LOGTAG, "DeleteResults", "No remote filesets were deleted");
|
||||
else
|
||||
Logging.Log.WriteInformationMessage(LOGTAG, "DeleteResults", "Deleted {0} remote fileset(s)", count);
|
||||
Logging.Log.WriteInformationMessage(LOGTAG, "NoFilesetsToDelete", "No remote filesets should be deleted");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Log.WriteInformationMessage(LOGTAG, "DeleteRemoteFileset", "Deleting {0} remote fileset(s) ...", versionsToDelete.Count);
|
||||
|
||||
if (count == 0)
|
||||
Logging.Log.WriteDryrunMessage(LOGTAG, "WouldDeleteResults", "No remote filesets would be deleted");
|
||||
var lst = db.DropFilesetsFromTable(versionsToDelete.Select(x => x.Time).ToArray(), rtr.Transaction).ToArray();
|
||||
foreach (var f in lst)
|
||||
db.UpdateRemoteVolume(f.Key, RemoteVolumeState.Deleting, f.Value, null, rtr.Transaction);
|
||||
|
||||
if (!m_options.Dryrun)
|
||||
rtr.Commit("CommitBeforeDelete");
|
||||
|
||||
foreach (var f in lst)
|
||||
{
|
||||
if (!await m_result.TaskControl.ProgressRendevouz().ConfigureAwait(false))
|
||||
{
|
||||
await backendManager.WaitForEmptyAsync(db, rtr.Transaction, m_result.TaskControl.ProgressToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_options.Dryrun)
|
||||
await backendManager.DeleteAsync(f.Key, f.Value, false, m_result.TaskControl.ProgressToken).ConfigureAwait(false);
|
||||
else
|
||||
Logging.Log.WriteDryrunMessage(LOGTAG, "WouldDeleteRemoteFileset", "Would delete remote fileset: {0}", f.Key);
|
||||
}
|
||||
|
||||
await backendManager.WaitForEmptyAsync(db, rtr.Transaction, m_result.TaskControl.ProgressToken).ConfigureAwait(false);
|
||||
|
||||
var count = lst.Length;
|
||||
if (!m_options.Dryrun)
|
||||
{
|
||||
if (count == 0)
|
||||
Logging.Log.WriteInformationMessage(LOGTAG, "DeleteResults", "No remote filesets were deleted");
|
||||
else
|
||||
Logging.Log.WriteInformationMessage(LOGTAG, "DeleteResults", "Deleted {0} remote fileset(s)", count);
|
||||
}
|
||||
else
|
||||
Logging.Log.WriteDryrunMessage(LOGTAG, "WouldDeleteResults", "{0} remote fileset(s) would be deleted", count);
|
||||
{
|
||||
|
||||
if (count > 0 && m_options.Dryrun)
|
||||
Logging.Log.WriteDryrunMessage(LOGTAG, "WouldDeleteHelp", "Remove --dry-run to actually delete files");
|
||||
if (count == 0)
|
||||
Logging.Log.WriteDryrunMessage(LOGTAG, "WouldDeleteResults", "No remote filesets would be deleted");
|
||||
else
|
||||
Logging.Log.WriteDryrunMessage(LOGTAG, "WouldDeleteResults", "{0} remote fileset(s) would be deleted", count);
|
||||
|
||||
if (count > 0 && m_options.Dryrun)
|
||||
Logging.Log.WriteDryrunMessage(LOGTAG, "WouldDeleteHelp", "Remove --dry-run to actually delete files");
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_options.NoAutoCompact && (forceCompact || versionsToDelete.Count > 0))
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// 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.
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Duplicati.Library.Interface;
|
||||
|
||||
namespace Duplicati.WebserverCore.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for application-wide settings for the server.
|
||||
/// </summary>
|
||||
public interface IApplicationSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Action to start or stop the usage reporter
|
||||
/// </summary>
|
||||
Action? StartOrStopUsageReporter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the folder where Duplicati data is stored
|
||||
/// </summary>
|
||||
string DataFolder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Used to check the origin of the web server (e.g. Tray icon or a stand alone Server)
|
||||
/// </summary>
|
||||
string Origin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The application exit event
|
||||
/// </summary>
|
||||
ManualResetEvent ApplicationExitEvent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The shared secret provider from the server invocation
|
||||
/// </summary>
|
||||
ISecretProvider? SecretProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Flag to indicate if the settings encryption key was provided externally
|
||||
/// </summary>
|
||||
bool SettingsEncryptionKeyProvidedExternally { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using Duplicati.Library.Interface;
|
||||
using Duplicati.Library.Logging;
|
||||
|
||||
namespace Duplicati.WebserverCore.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Handles logging from the server,
|
||||
/// and provides an entry point for the runner
|
||||
/// to redirect log output to a file
|
||||
/// </summary>
|
||||
public interface ILogWriteHandler : ILogDestination, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a single log event
|
||||
/// </summary>
|
||||
public struct LiveLogEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// The context key used for conveying the backup ID
|
||||
/// </summary>
|
||||
public const string LOG_EXTRA_BACKUPID = "BackupID";
|
||||
/// <summary>
|
||||
/// The context key used for conveying the task ID
|
||||
/// </summary>
|
||||
public const string LOG_EXTRA_TASKID = "TaskID";
|
||||
|
||||
/// <summary>
|
||||
/// A unique ID that sequentially increments
|
||||
/// </summary>
|
||||
private static long _id;
|
||||
|
||||
/// <summary>
|
||||
/// The time the message was logged
|
||||
/// </summary>
|
||||
public readonly DateTime When;
|
||||
|
||||
/// <summary>
|
||||
/// The ID assigned to the message
|
||||
/// </summary>
|
||||
public readonly long ID;
|
||||
|
||||
/// <summary>
|
||||
/// The logged message
|
||||
/// </summary>
|
||||
public readonly string Message;
|
||||
|
||||
/// <summary>
|
||||
/// The log tag
|
||||
/// </summary>
|
||||
public readonly string Tag;
|
||||
|
||||
/// <summary>
|
||||
/// The message ID
|
||||
/// </summary>
|
||||
public readonly string MessageID;
|
||||
|
||||
/// <summary>
|
||||
/// The message ID
|
||||
/// </summary>
|
||||
public readonly string? ExceptionID;
|
||||
|
||||
/// <summary>
|
||||
/// The message type
|
||||
/// </summary>
|
||||
public readonly LogMessageType Type;
|
||||
|
||||
/// <summary>
|
||||
/// Exception data attached to the message
|
||||
/// </summary>
|
||||
public readonly Exception? Exception;
|
||||
|
||||
/// <summary>
|
||||
/// The backup ID, if any
|
||||
/// </summary>
|
||||
public readonly string? BackupID;
|
||||
|
||||
/// <summary>
|
||||
/// The task ID, if any
|
||||
/// </summary>
|
||||
public readonly string TaskID;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LiveLogEntry"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="entry">The log entry to store</param>
|
||||
public LiveLogEntry(LogEntry entry)
|
||||
{
|
||||
this.ID = System.Threading.Interlocked.Increment(ref _id);
|
||||
this.When = entry.When;
|
||||
this.Message = entry.FormattedMessage;
|
||||
this.Type = entry.Level;
|
||||
this.Exception = entry.Exception;
|
||||
this.Tag = entry.FilterTag;
|
||||
this.MessageID = entry.Id;
|
||||
this.BackupID = entry[LOG_EXTRA_BACKUPID];
|
||||
this.TaskID = entry[LOG_EXTRA_TASKID];
|
||||
|
||||
if (entry.Exception == null)
|
||||
this.ExceptionID = null;
|
||||
else if (entry.Exception is UserInformationException exception)
|
||||
this.ExceptionID = exception.HelpID;
|
||||
else
|
||||
this.ExceptionID = entry.Exception.GetType().FullName;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void RenewTimeout(LogMessageType type);
|
||||
|
||||
void SetServerFile(string path, LogMessageType level);
|
||||
|
||||
void AppendLogDestination(ILogDestination destination, LogMessageType level);
|
||||
|
||||
LiveLogEntry[] AfterTime(DateTime offset, LogMessageType level);
|
||||
|
||||
LiveLogEntry[] AfterID(long id, LogMessageType level, int pagesize);
|
||||
}
|
||||
|
||||
+12
-12
@@ -18,20 +18,20 @@
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Server;
|
||||
|
||||
namespace Duplicati.Library.RestAPI.Abstractions;
|
||||
namespace Duplicati.WebserverCore.Abstractions;
|
||||
|
||||
public interface IWorkerThreadsManager
|
||||
/// <summary>
|
||||
/// Produces system information.
|
||||
/// </summary>
|
||||
public interface IProgressStateProviderService
|
||||
{
|
||||
void Spawn(Action<Runner.IRunnerData> item);
|
||||
|
||||
Tuple<long, string>? CurrentTask { get; }
|
||||
WorkerThread<Runner.IRunnerData>? WorkerThread { get; }
|
||||
void UpdateThrottleSpeeds(string? uploadSpeed, string? downloadSpeed);
|
||||
|
||||
long AddTask(Runner.IRunnerData data, bool skipQueue = false);
|
||||
}
|
||||
/// <summary>
|
||||
/// Handler for progress state generation.
|
||||
/// </summary>
|
||||
Func<Server.Serialization.Interface.IProgressEventData>? GenerateProgressState { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
|
||||
// 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.
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Library.Interface;
|
||||
using Duplicati.Server.Serialization.Interface;
|
||||
|
||||
namespace Duplicati.WebserverCore.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// A cached task result
|
||||
/// </summary>
|
||||
/// <param name="TaskID">The task ID</param>
|
||||
/// <param name="BackupId">The backup ID</param>
|
||||
/// <param name="TaskStarted">The time the task started</param>
|
||||
/// <param name="TaskFinished">The time the task finished</param>
|
||||
/// <param name="Exception">The exception that was thrown</param>
|
||||
public sealed record CachedTaskResult(long TaskID, string? BackupId, DateTime? TaskStarted, DateTime? TaskFinished, Exception? Exception);
|
||||
|
||||
/// <summary>
|
||||
/// Class to encapsulate a thread that runs a list of queued operations
|
||||
/// </summary>
|
||||
/// <typeparam name="Tx">The type to operate on</typeparam>
|
||||
public interface IQueueRunnerService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a copy of the current tasks in the queue
|
||||
/// </summary>
|
||||
/// <returns>A list of queued tasks</returns>
|
||||
List<IQueuedTask> GetCurrentTasks();
|
||||
/// <summary>
|
||||
/// Gets a flag indicating if the queue is currently executing a task
|
||||
/// </summary>
|
||||
/// <returns>True if the queue is executing a task, false otherwise</returns>
|
||||
bool GetIsActive();
|
||||
/// <summary>
|
||||
/// Returns the currently executing task in the queue
|
||||
/// </summary>
|
||||
/// <returns>The currently executing task, or null if no task is executing</returns>
|
||||
IQueuedTask? GetCurrentTask();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cached task results for a given task ID
|
||||
/// </summary>
|
||||
/// <param name="taskID">The task ID</param>
|
||||
/// <returns>The cached task result</returns>
|
||||
CachedTaskResult? GetCachedTaskResults(long taskID);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a task to the queue
|
||||
/// </summary>
|
||||
/// <param name="task">The task to add</param>
|
||||
long AddTask(IQueuedTask task);
|
||||
/// <summary>
|
||||
/// Adds a task to the queue, optionally skipping the queue
|
||||
/// </summary>
|
||||
/// <param name="task">The task to add</param>
|
||||
/// <param name="skipQueue">Whether to skip the queue</param>
|
||||
long AddTask(IQueuedTask task, bool skipQueue);
|
||||
/// <summary>
|
||||
/// Removes a task from the queue
|
||||
/// </summary>
|
||||
/// <param name="wait">Whether to wait for the task to finish</param>
|
||||
void Terminate(bool wait);
|
||||
/// <summary>
|
||||
/// Resumes processing items in the queue
|
||||
/// </summary>
|
||||
void Resume();
|
||||
/// <summary>
|
||||
/// Pauses processing items in the queue
|
||||
/// </summary>
|
||||
void Pause();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the IDs of the tasks in the worker queue
|
||||
/// </summary>
|
||||
/// <returns>A list of tuples containing the task ID and backup ID</returns>
|
||||
IList<Tuple<long, string?>> GetQueueWithIds();
|
||||
|
||||
/// <summary>
|
||||
/// Runs a task immediately, bypassing the queue.
|
||||
/// Note that the task will run concurrently with the queue tasks and may cause database lock issues.
|
||||
/// </summary>
|
||||
/// <param name="task">The task to run</param>
|
||||
IBasicResults? RunImmediately(IQueuedTask task);
|
||||
}
|
||||
+1
-24
@@ -20,25 +20,12 @@
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Server;
|
||||
using Duplicati.Server.Serialization.Interface;
|
||||
|
||||
namespace Duplicati.WebserverCore.Abstractions;
|
||||
|
||||
public interface IScheduler
|
||||
public interface ISchedulerService
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes scheduler
|
||||
/// </summary>
|
||||
/// <param name="worker">The worker thread</param>
|
||||
void Init(WorkerThread<Runner.IRunnerData> worker);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current ids in the scheduler queue
|
||||
/// </summary>
|
||||
IList<Tuple<long, string>> GetSchedulerQueueIds();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current proposed schedule
|
||||
/// </summary>
|
||||
@@ -50,21 +37,11 @@ public interface IScheduler
|
||||
/// <param name="wait">True if the call should block until the thread has exited, false otherwise</param>
|
||||
void Terminate(bool wait);
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the event that is triggered when the schedule changes
|
||||
/// </summary>
|
||||
void SubScribeToNewSchedule(Action handler);
|
||||
|
||||
/// <summary>
|
||||
/// A snapshot copy of the current schedule list
|
||||
/// </summary>
|
||||
List<KeyValuePair<DateTime, ISchedule>> Schedule { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A snapshot copy of the current worker queue, that is items that are scheduled, but waiting for execution
|
||||
/// </summary>
|
||||
List<Runner.IRunnerData> WorkerQueue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Forces the scheduler to re-evaluate the order.
|
||||
/// Call this method if something changes
|
||||
@@ -32,6 +32,7 @@ using Duplicati.Library.AutoUpdater;
|
||||
using System.Data;
|
||||
using Duplicati.Library.Main.Database;
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@@ -39,6 +40,11 @@ namespace Duplicati.Server.Database
|
||||
{
|
||||
public class Connection : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The placeholder for passwords in the UI
|
||||
/// </summary>
|
||||
public const string PASSWORD_PLACEHOLDER = "**********";
|
||||
|
||||
private readonly IDbConnection m_connection;
|
||||
private readonly IDbCommand m_errorcmd;
|
||||
public readonly object m_lock = new object();
|
||||
@@ -47,6 +53,11 @@ namespace Duplicati.Server.Database
|
||||
private readonly Dictionary<string, Backup> m_temporaryBackups = new Dictionary<string, Backup>();
|
||||
private readonly bool m_encryptSensitiveFields;
|
||||
private readonly EncryptedFieldHelper.KeyInstance? m_key;
|
||||
private IServiceProvider? m_serviceProvider;
|
||||
private INotificationUpdateService? m_notificationUpdateService;
|
||||
private EventPollNotify? m_eventPollNotifyer;
|
||||
private readonly string m_dataFolder;
|
||||
|
||||
private static readonly HashSet<string> _encryptedFields =
|
||||
BackendLoader.Backends.SelectMany(x => x.SupportedCommands ?? [])
|
||||
.Concat(EncryptionLoader.Modules.SelectMany(x => x.SupportedCommands ?? []))
|
||||
@@ -66,14 +77,31 @@ namespace Duplicati.Server.Database
|
||||
])
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public Connection(IDbConnection connection, bool disableFieldEncryption, EncryptedFieldHelper.KeyInstance? key)
|
||||
public Connection(IDbConnection connection, bool disableFieldEncryption, EncryptedFieldHelper.KeyInstance? key, string dataFolder, Action startOrStopUsageReporter)
|
||||
{
|
||||
m_dataFolder = dataFolder;
|
||||
m_encryptSensitiveFields = !disableFieldEncryption;
|
||||
m_key = key;
|
||||
m_connection = connection;
|
||||
m_errorcmd = m_connection.CreateCommand(@"INSERT INTO ""ErrorLog"" (""BackupID"", ""Message"", ""Exception"", ""Timestamp"") VALUES (@BackupId,@Message,@Exception,@Timestamp)");
|
||||
|
||||
this.ApplicationSettings = new ServerSettings(this);
|
||||
this.ApplicationSettings = new ServerSettings(this, startOrStopUsageReporter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The service provider is used to resolve dependencies
|
||||
/// </summary>
|
||||
internal IServiceProvider? ServiceProvider => m_serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Set the service provider to be used for resolving dependencies
|
||||
/// </summary>
|
||||
/// <param name="sp">The service provider</param>
|
||||
public void SetServiceProvider(IServiceProvider sp)
|
||||
{
|
||||
m_serviceProvider = sp;
|
||||
m_notificationUpdateService = sp?.GetRequiredService<INotificationUpdateService>();
|
||||
m_eventPollNotifyer = sp?.GetRequiredService<EventPollNotify>();
|
||||
}
|
||||
|
||||
public bool IsEncryptingFields => m_encryptSensitiveFields;
|
||||
@@ -125,7 +153,7 @@ namespace Duplicati.Server.Database
|
||||
this.ApplicationSettings.PreloadSettingsHash = settingsHash;
|
||||
}
|
||||
|
||||
public void LogError(string backupid, string message, Exception ex)
|
||||
public void LogError(string? backupid, string message, Exception ex)
|
||||
{
|
||||
lock (m_lock)
|
||||
{
|
||||
@@ -310,7 +338,7 @@ namespace Duplicati.Server.Database
|
||||
cmd => cmd.SetCommandAndParameters(@"INSERT INTO ""Option"" (""BackupID"", ""Filter"", ""Name"", ""Value"") VALUES (@BackupId, @Filter, @Name, @Value)"),
|
||||
(cmd, f) =>
|
||||
{
|
||||
if (FIXMEGlobal.PASSWORD_PLACEHOLDER.Equals(f.Value))
|
||||
if (PASSWORD_PLACEHOLDER.Equals(f.Value))
|
||||
throw new Exception("Attempted to save a property with the placeholder password");
|
||||
|
||||
cmd.SetParameterValue("@BackupId", id)
|
||||
@@ -608,8 +636,8 @@ namespace Duplicati.Server.Database
|
||||
}
|
||||
}
|
||||
|
||||
FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
m_notificationUpdateService?.IncrementLastDataUpdateId();
|
||||
m_eventPollNotifyer?.SignalNewEvent();
|
||||
}
|
||||
|
||||
private void AddOrUpdateBackup(IBackup item, bool updateSchedule, ISchedule? schedule)
|
||||
@@ -619,7 +647,7 @@ namespace Duplicati.Server.Database
|
||||
bool update = item.ID != null;
|
||||
if (!update && item.DBPath == null)
|
||||
{
|
||||
var folder = FIXMEGlobal.DataFolder;
|
||||
var folder = m_dataFolder;
|
||||
if (!System.IO.Directory.Exists(folder))
|
||||
System.IO.Directory.CreateDirectory(folder);
|
||||
|
||||
@@ -652,7 +680,7 @@ namespace Duplicati.Server.Database
|
||||
},
|
||||
(cmd, n) =>
|
||||
{
|
||||
if (n.TargetURL.IndexOf(FIXMEGlobal.PASSWORD_PLACEHOLDER, StringComparison.Ordinal) >= 0)
|
||||
if (n.TargetURL.IndexOf(PASSWORD_PLACEHOLDER, StringComparison.Ordinal) >= 0)
|
||||
throw new Exception("Attempted to save a backup with the password placeholder");
|
||||
if (update && long.Parse(n.ID) <= 0)
|
||||
throw new Exception("Invalid update, cannot update application settings through update method");
|
||||
@@ -722,8 +750,8 @@ namespace Duplicati.Server.Database
|
||||
}
|
||||
|
||||
tr.Commit();
|
||||
FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
m_notificationUpdateService?.IncrementLastDataUpdateId();
|
||||
m_eventPollNotifyer?.SignalNewEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -735,8 +763,8 @@ namespace Duplicati.Server.Database
|
||||
{
|
||||
AddOrUpdateSchedule(item, tr);
|
||||
tr.Commit();
|
||||
FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
m_notificationUpdateService?.IncrementLastDataUpdateId();
|
||||
m_eventPollNotifyer?.SignalNewEvent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -800,8 +828,8 @@ namespace Duplicati.Server.Database
|
||||
}
|
||||
}
|
||||
|
||||
FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
m_notificationUpdateService?.IncrementLastDataUpdateId();
|
||||
m_eventPollNotifyer?.SignalNewEvent();
|
||||
}
|
||||
|
||||
public void DeleteBackup(IBackup backup)
|
||||
@@ -820,8 +848,8 @@ namespace Duplicati.Server.Database
|
||||
lock (m_lock)
|
||||
DeleteFromDb("Schedule", ID);
|
||||
|
||||
FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
m_notificationUpdateService?.IncrementLastDataUpdateId();
|
||||
m_eventPollNotifyer?.SignalNewEvent();
|
||||
}
|
||||
|
||||
public void DeleteSchedule(ISchedule schedule)
|
||||
@@ -906,21 +934,27 @@ namespace Duplicati.Server.Database
|
||||
return false;
|
||||
|
||||
DeleteFromDb(typeof(Notification).Name, id);
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.UnackedError = notifications.Any(x => x.ID != id && x.Type == Duplicati.Server.Serialization.NotificationType.Error);
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.UnackedWarning = notifications.Any(x => x.ID != id && x.Type == Duplicati.Server.Serialization.NotificationType.Warning);
|
||||
this.ApplicationSettings.UnackedError = notifications.Any(x => x.ID != id && x.Type == Duplicati.Server.Serialization.NotificationType.Error);
|
||||
this.ApplicationSettings.UnackedWarning = notifications.Any(x => x.ID != id && x.Type == Duplicati.Server.Serialization.NotificationType.Warning);
|
||||
}
|
||||
|
||||
// Guard against dismissing notifications before the provider is initialized
|
||||
if (FIXMEGlobal.Provider != null)
|
||||
{
|
||||
FIXMEGlobal.NotificationUpdateService.IncrementLastNotificationUpdateId();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
}
|
||||
m_notificationUpdateService?.IncrementLastNotificationUpdateId();
|
||||
m_eventPollNotifyer?.SignalNewEvent();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void RegisterNotification(Serialization.NotificationType type, string title, string message, Exception ex, string backupid, string action, string logid, string messageid, string logtag, Func<INotification, INotification[], INotification> conflicthandler)
|
||||
public void RegisterNotification(
|
||||
Serialization.NotificationType type,
|
||||
string title,
|
||||
string message,
|
||||
Exception? ex,
|
||||
string? backupid,
|
||||
string action,
|
||||
string? logid,
|
||||
string? messageid,
|
||||
string? logtag,
|
||||
Func<INotification, INotification[], INotification> conflicthandler)
|
||||
{
|
||||
lock (m_lock)
|
||||
{
|
||||
@@ -949,13 +983,13 @@ namespace Duplicati.Server.Database
|
||||
OverwriteAndUpdateDb(null, null, [notification], false);
|
||||
|
||||
if (type == Serialization.NotificationType.Error)
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.UnackedError = true;
|
||||
ApplicationSettings.UnackedError = true;
|
||||
else if (type == Serialization.NotificationType.Warning)
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.UnackedWarning = true;
|
||||
ApplicationSettings.UnackedWarning = true;
|
||||
}
|
||||
|
||||
FIXMEGlobal.NotificationUpdateService.IncrementLastNotificationUpdateId();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
m_notificationUpdateService?.IncrementLastNotificationUpdateId();
|
||||
m_eventPollNotifyer?.SignalNewEvent();
|
||||
}
|
||||
|
||||
//Workaround to clean up the database after invalid settings update
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2025, The Duplicati Team
|
||||
// Copyright (C) 2025, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
@@ -29,6 +29,8 @@ using System.Text;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Library.AutoUpdater;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@@ -78,10 +80,12 @@ namespace Duplicati.Server.Database
|
||||
|
||||
private readonly Dictionary<string, string?> settings;
|
||||
private readonly Connection databaseConnection;
|
||||
private UpdateInfo? m_latestUpdate;
|
||||
private UpdateInfo? latestUpdate;
|
||||
private readonly Action? startOrStopUsageReporter;
|
||||
|
||||
internal ServerSettings(Connection con)
|
||||
internal ServerSettings(Connection con, Action startOrStopUsageReporter)
|
||||
{
|
||||
this.startOrStopUsageReporter = startOrStopUsageReporter;
|
||||
settings = new Dictionary<string, string?>();
|
||||
databaseConnection = con;
|
||||
ReloadSettings();
|
||||
@@ -113,7 +117,7 @@ namespace Duplicati.Server.Database
|
||||
|
||||
lock (databaseConnection.m_lock)
|
||||
{
|
||||
m_latestUpdate = null;
|
||||
latestUpdate = null;
|
||||
if (clearExisting)
|
||||
settings.Clear();
|
||||
|
||||
@@ -142,17 +146,17 @@ namespace Duplicati.Server.Database
|
||||
Value = n.Value
|
||||
}, Database.Connection.SERVER_SETTINGS_ID);
|
||||
|
||||
if (FIXMEGlobal.IsServerStarted)
|
||||
var provider = databaseConnection.ServiceProvider;
|
||||
if (provider != null)
|
||||
{
|
||||
FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
provider?.GetRequiredService<INotificationUpdateService>()?.IncrementLastDataUpdateId();
|
||||
provider?.GetRequiredService<EventPollNotify>()?.SignalNewEvent();
|
||||
// If throttle options were changed, update now
|
||||
FIXMEGlobal.WorkerThreadsManager.UpdateThrottleSpeeds(UploadSpeedLimit, DownloadSpeedLimit);
|
||||
provider?.GetRequiredService<IQueueRunnerService>()?.GetCurrentTask()?.UpdateThrottleSpeeds(UploadSpeedLimit, DownloadSpeedLimit);
|
||||
}
|
||||
|
||||
// In case the usage reporter is enabled or disabled, refresh now
|
||||
if (FIXMEGlobal.StartOrStopUsageReporter != null)
|
||||
FIXMEGlobal.StartOrStopUsageReporter();
|
||||
startOrStopUsageReporter?.Invoke();
|
||||
}
|
||||
|
||||
public string? StartupDelayDuration
|
||||
@@ -602,7 +606,7 @@ namespace Duplicati.Server.Database
|
||||
lock (databaseConnection.m_lock)
|
||||
settings[CONST.UPDATE_CHECK_INTERVAL] = value;
|
||||
SaveSettings();
|
||||
FIXMEGlobal.UpdatePoller.Reschedule();
|
||||
databaseConnection?.ServiceProvider?.GetRequiredService<UpdatePollThread>()?.Reschedule();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,11 +635,11 @@ namespace Duplicati.Server.Database
|
||||
|
||||
try
|
||||
{
|
||||
if (m_latestUpdate != null)
|
||||
return m_latestUpdate;
|
||||
if (latestUpdate != null)
|
||||
return latestUpdate;
|
||||
|
||||
using (var tr = new System.IO.StringReader(updateNew))
|
||||
return m_latestUpdate = Server.Serialization.Serializer.Deserialize<Library.AutoUpdater.UpdateInfo>(tr);
|
||||
return latestUpdate = Server.Serialization.Serializer.Deserialize<Library.AutoUpdater.UpdateInfo>(tr);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -655,7 +659,7 @@ namespace Duplicati.Server.Database
|
||||
result = sb.ToString();
|
||||
}
|
||||
|
||||
m_latestUpdate = value;
|
||||
latestUpdate = value;
|
||||
lock (databaseConnection.m_lock)
|
||||
settings[CONST.UPDATE_CHECK_NEW_VERSION] = result;
|
||||
|
||||
|
||||
@@ -1,135 +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 Duplicati.Server;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Library.RestAPI.Abstractions;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Duplicati.Library.Interface;
|
||||
|
||||
namespace Duplicati.Library.RestAPI
|
||||
{
|
||||
/**
|
||||
* In the absense of dependancy injection, there is a significant amount of variables exposed through Program as globals.
|
||||
* This causes a problem decoupling classes and leads to circular dependancies.
|
||||
*/
|
||||
public static class FIXMEGlobal
|
||||
{
|
||||
/// <summary>
|
||||
/// The placeholder for passwords in the UI
|
||||
/// </summary>
|
||||
public const string PASSWORD_PLACEHOLDER = "**********";
|
||||
|
||||
public static IServiceProvider Provider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This is the only access to the database
|
||||
/// </summary>
|
||||
public static Server.Database.Connection DataConnection;
|
||||
|
||||
/// <summary>
|
||||
/// A delegate method for creating a copy of the current progress state
|
||||
/// </summary>
|
||||
public static Func<Duplicati.Server.Serialization.Interface.IProgressEventData> GenerateProgressState;
|
||||
|
||||
/// <summary>
|
||||
/// The status event signaler, used to control long polling of status updates
|
||||
/// </summary>
|
||||
public static EventPollNotify StatusEventNotifyer => Provider.GetRequiredService<EventPollNotify>();
|
||||
|
||||
/// <summary>
|
||||
/// For keeping and incrementing last last events Ids of db save and last notification
|
||||
/// </summary>
|
||||
public static INotificationUpdateService NotificationUpdateService => Provider.GetRequiredService<INotificationUpdateService>();
|
||||
/// <summary>
|
||||
/// Checks if the server has started and is listening for events
|
||||
/// </summary>
|
||||
public static bool IsServerStarted => Provider != null;
|
||||
|
||||
/// <summary>
|
||||
/// This is the working thread
|
||||
/// </summary>
|
||||
public static WorkerThread<Runner.IRunnerData> WorkThread =>
|
||||
Provider.GetRequiredService<IWorkerThreadsManager>().WorkerThread;
|
||||
|
||||
public static IWorkerThreadsManager WorkerThreadsManager =>
|
||||
Provider.GetRequiredService<IWorkerThreadsManager>();
|
||||
|
||||
public static Action StartOrStopUsageReporter;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the folder where Duplicati data is stored
|
||||
/// </summary>
|
||||
public static string DataFolder;
|
||||
|
||||
/// <summary>
|
||||
/// This is the scheduling thread
|
||||
/// </summary>
|
||||
public static IScheduler Scheduler => Provider.GetRequiredService<IScheduler>();
|
||||
|
||||
/// <summary>
|
||||
/// The log redirect handler
|
||||
/// </summary>
|
||||
public static readonly LogWriteHandler LogHandler = new LogWriteHandler();
|
||||
|
||||
/// <summary>
|
||||
/// The update poll thread.
|
||||
/// </summary>
|
||||
public static UpdatePollThread UpdatePoller => Provider.GetRequiredService<UpdatePollThread>();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Used to check the origin of the web server (e.g. Tray icon or a stand alone Server)
|
||||
/// </summary>
|
||||
public static string Origin = "Server";
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The application exit event
|
||||
/// </summary>
|
||||
public static System.Threading.ManualResetEvent ApplicationExitEvent;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// List of completed task results
|
||||
/// </summary>
|
||||
public static readonly List<KeyValuePair<long, Exception>> TaskResultCache = new List<KeyValuePair<long, Exception>>();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// This is the lock to be used before manipulating the shared resources
|
||||
/// </summary>
|
||||
public static readonly object MainLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// The shared secret provider from the server invocation
|
||||
/// </summary>
|
||||
public static ISecretProvider SecretProvider { get; set; }
|
||||
/// <summary>
|
||||
/// Flag to indicate if the settings encryption key was provided externally
|
||||
/// </summary>
|
||||
public static bool SettingsEncryptionKeyProvidedExternally { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,30 @@
|
||||
// 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.Linq;
|
||||
using Duplicati.Library.Logging;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Library.Interface;
|
||||
using Duplicati.Library.Main;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
using Duplicati.Library.Logging;
|
||||
|
||||
namespace Duplicati.Server
|
||||
{
|
||||
@@ -33,7 +33,7 @@ namespace Duplicati.Server
|
||||
/// and provides an entry point for the runner
|
||||
/// to redirect log output to a file
|
||||
/// </summary>
|
||||
public class LogWriteHandler : ILogDestination, IDisposable
|
||||
public class LogWriteHandler : ILogWriteHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// The number of messages to keep when inactive
|
||||
@@ -44,101 +44,6 @@ namespace Duplicati.Server
|
||||
/// </summary>
|
||||
private const int ACTIVE_SIZE = 5000;
|
||||
|
||||
/// <summary>
|
||||
/// The context key used for conveying the backup ID
|
||||
/// </summary>
|
||||
public const string LOG_EXTRA_BACKUPID = "BackupID";
|
||||
/// <summary>
|
||||
/// The context key used for conveying the task ID
|
||||
/// </summary>
|
||||
public const string LOG_EXTRA_TASKID = "TaskID";
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single log event
|
||||
/// </summary>
|
||||
public struct LogEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// A unique ID that sequentially increments
|
||||
/// </summary>
|
||||
private static long _id;
|
||||
|
||||
/// <summary>
|
||||
/// The time the message was logged
|
||||
/// </summary>
|
||||
public readonly DateTime When;
|
||||
|
||||
/// <summary>
|
||||
/// The ID assigned to the message
|
||||
/// </summary>
|
||||
public readonly long ID;
|
||||
|
||||
/// <summary>
|
||||
/// The logged message
|
||||
/// </summary>
|
||||
public readonly string Message;
|
||||
|
||||
/// <summary>
|
||||
/// The log tag
|
||||
/// </summary>
|
||||
public readonly string Tag;
|
||||
|
||||
/// <summary>
|
||||
/// The message ID
|
||||
/// </summary>
|
||||
public readonly string MessageID;
|
||||
|
||||
/// <summary>
|
||||
/// The message ID
|
||||
/// </summary>
|
||||
public readonly string ExceptionID;
|
||||
|
||||
/// <summary>
|
||||
/// The message type
|
||||
/// </summary>
|
||||
public readonly LogMessageType Type;
|
||||
|
||||
/// <summary>
|
||||
/// Exception data attached to the message
|
||||
/// </summary>
|
||||
public readonly Exception Exception;
|
||||
|
||||
/// <summary>
|
||||
/// The backup ID, if any
|
||||
/// </summary>
|
||||
public readonly string BackupID;
|
||||
|
||||
/// <summary>
|
||||
/// The task ID, if any
|
||||
/// </summary>
|
||||
public readonly string TaskID;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Duplicati.Server.LogWriteHandler+LogEntry"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="entry">The log entry to store</param>
|
||||
public LogEntry(Duplicati.Library.Logging.LogEntry entry)
|
||||
{
|
||||
this.ID = System.Threading.Interlocked.Increment(ref _id);
|
||||
this.When = entry.When;
|
||||
this.Message = entry.FormattedMessage;
|
||||
this.Type = entry.Level;
|
||||
this.Exception = entry.Exception;
|
||||
this.Tag = entry.FilterTag;
|
||||
this.MessageID = entry.Id;
|
||||
this.BackupID = entry[LOG_EXTRA_BACKUPID];
|
||||
this.TaskID = entry[LOG_EXTRA_TASKID];
|
||||
|
||||
if (entry.Exception == null)
|
||||
this.ExceptionID = null;
|
||||
else if (entry.Exception is UserInformationException exception)
|
||||
this.ExceptionID = exception.HelpID;
|
||||
else
|
||||
this.ExceptionID = entry.Exception.GetType().FullName;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Basic implementation of a ring-buffer
|
||||
/// </summary>
|
||||
@@ -208,7 +113,7 @@ namespace Duplicati.Server
|
||||
private readonly DateTime[] m_timeouts;
|
||||
private readonly object m_lock = new object();
|
||||
private volatile bool m_anytimeouts = false;
|
||||
private RingBuffer<LogEntry> m_buffer;
|
||||
private RingBuffer<ILogWriteHandler.LiveLogEntry> m_buffer;
|
||||
|
||||
|
||||
private readonly ControllerMultiLogTarget m_target = new ControllerMultiLogTarget(null, LogMessageType.Warning, null);
|
||||
@@ -218,7 +123,7 @@ namespace Duplicati.Server
|
||||
{
|
||||
var fields = Enum.GetValues(typeof(LogMessageType));
|
||||
m_timeouts = new DateTime[fields.Length];
|
||||
m_buffer = new RingBuffer<LogEntry>(INACTIVE_SIZE);
|
||||
m_buffer = new RingBuffer<ILogWriteHandler.LiveLogEntry>(INACTIVE_SIZE);
|
||||
}
|
||||
|
||||
public void RenewTimeout(LogMessageType type)
|
||||
@@ -228,7 +133,7 @@ namespace Duplicati.Server
|
||||
m_timeouts[(int)type] = DateTime.Now.AddSeconds(30);
|
||||
m_anytimeouts = true;
|
||||
if (m_buffer == null || m_buffer.Size == INACTIVE_SIZE)
|
||||
m_buffer = new RingBuffer<LogEntry>(ACTIVE_SIZE, m_buffer);
|
||||
m_buffer = new RingBuffer<ILogWriteHandler.LiveLogEntry>(ACTIVE_SIZE, m_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,7 +153,7 @@ namespace Duplicati.Server
|
||||
UpdateLogLevel();
|
||||
}
|
||||
|
||||
public LogEntry[] AfterTime(DateTime offset, LogMessageType level)
|
||||
public ILogWriteHandler.LiveLogEntry[] AfterTime(DateTime offset, LogMessageType level)
|
||||
{
|
||||
RenewTimeout(level);
|
||||
UpdateLogLevel();
|
||||
@@ -257,13 +162,13 @@ namespace Duplicati.Server
|
||||
lock (m_lock)
|
||||
{
|
||||
if (m_buffer == null)
|
||||
return new LogEntry[0];
|
||||
return new ILogWriteHandler.LiveLogEntry[0];
|
||||
|
||||
return m_buffer.FlatArray((x) => x.When > offset && x.Type >= level);
|
||||
}
|
||||
}
|
||||
|
||||
public LogEntry[] AfterID(long id, LogMessageType level, int pagesize)
|
||||
public ILogWriteHandler.LiveLogEntry[] AfterID(long id, LogMessageType level, int pagesize)
|
||||
{
|
||||
RenewTimeout(level);
|
||||
UpdateLogLevel();
|
||||
@@ -271,7 +176,7 @@ namespace Duplicati.Server
|
||||
lock (m_lock)
|
||||
{
|
||||
if (m_buffer == null)
|
||||
return new LogEntry[0];
|
||||
return [];
|
||||
|
||||
var buffer = m_buffer.FlatArray((x) => x.ID > id && x.Type >= level);
|
||||
// Return the <page_size> newest entries
|
||||
@@ -305,7 +210,7 @@ namespace Duplicati.Server
|
||||
|
||||
#region ILog implementation
|
||||
|
||||
public void WriteMessage(Duplicati.Library.Logging.LogEntry entry)
|
||||
public void WriteMessage(LogEntry entry)
|
||||
{
|
||||
if (entry.Level < m_logLevel)
|
||||
return;
|
||||
@@ -329,13 +234,13 @@ namespace Duplicati.Server
|
||||
UpdateLogLevel();
|
||||
m_anytimeouts = false;
|
||||
if (m_buffer == null || m_buffer.Size != INACTIVE_SIZE)
|
||||
m_buffer = new RingBuffer<LogEntry>(INACTIVE_SIZE, m_buffer);
|
||||
m_buffer = new RingBuffer<ILogWriteHandler.LiveLogEntry>(INACTIVE_SIZE, m_buffer);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (m_buffer != null)
|
||||
m_buffer.Enqueue(new LogEntry(entry));
|
||||
m_buffer.Enqueue(new ILogWriteHandler.LiveLogEntry(entry));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+220
-175
@@ -19,6 +19,8 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
@@ -26,46 +28,57 @@ using Duplicati.Library.Interface;
|
||||
using Duplicati.Server.Serialization;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Library.Utility;
|
||||
using System.Threading.Tasks;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.Server.Serialization.Interface;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
|
||||
namespace Duplicati.Server
|
||||
{
|
||||
public static class Runner
|
||||
{
|
||||
public interface IRunnerData : Duplicati.Server.Serialization.Interface.IQueuedTask
|
||||
public interface IRunnerData : Serialization.Interface.IQueuedTask
|
||||
{
|
||||
Duplicati.Server.Serialization.Interface.IBackup Backup { get; }
|
||||
IDictionary<string, string> ExtraOptions { get; }
|
||||
string[] FilterStrings { get; }
|
||||
string[] ExtraArguments { get; }
|
||||
Serialization.Interface.IBackup? Backup { get; }
|
||||
IDictionary<string, string?>? ExtraOptions { get; }
|
||||
string[]? FilterStrings { get; }
|
||||
string[]? ExtraArguments { get; }
|
||||
int PageSize { get; }
|
||||
int PageOffset { get; }
|
||||
void Stop();
|
||||
void Abort();
|
||||
void Pause(bool alsoTransfers);
|
||||
void Resume();
|
||||
void UpdateThrottleSpeed(string uploadSpeed, string downloadSpeed);
|
||||
void SetController(Duplicati.Library.Main.Controller controller);
|
||||
void SetController(Library.Main.Controller? controller);
|
||||
}
|
||||
|
||||
private class RunnerData : IRunnerData
|
||||
{
|
||||
private static long RunnerTaskID = 1;
|
||||
|
||||
public Duplicati.Server.Serialization.DuplicatiOperation Operation { get; internal set; }
|
||||
public Duplicati.Server.Serialization.Interface.IBackup Backup { get; internal set; }
|
||||
public IDictionary<string, string> ExtraOptions { get; internal set; }
|
||||
public string[] FilterStrings { get; internal set; }
|
||||
public Func<Task>? OnStarting { get; set; }
|
||||
public Func<Exception?, Task>? OnFinished { get; set; }
|
||||
|
||||
public string BackupID { get { return Backup.ID; } }
|
||||
/// <summary>
|
||||
/// Callback to be executed when the task is finished.
|
||||
/// Only supported for the delete operation for now.
|
||||
/// </summary>
|
||||
internal Action<IRunnerData>? AfterTaskFinished { get; set; }
|
||||
|
||||
public DuplicatiOperation Operation { get; internal set; }
|
||||
public Serialization.Interface.IBackup? Backup { get; internal set; }
|
||||
public IDictionary<string, string?>? ExtraOptions { get; internal set; }
|
||||
public string[]? FilterStrings { get; internal set; }
|
||||
|
||||
public string? BackupID { get { return Backup?.ID; } }
|
||||
public long TaskID { get { return m_taskID; } }
|
||||
|
||||
public string[] ExtraArguments { get; internal set; }
|
||||
public string[]? ExtraArguments { get; internal set; }
|
||||
public int PageSize { get; internal set; } = 0;
|
||||
public int PageOffset { get; internal set; } = 0;
|
||||
|
||||
internal Duplicati.Library.Main.Controller Controller { get; set; }
|
||||
public DateTime? TaskStarted { get; set; }
|
||||
public DateTime? TaskFinished { get; set; }
|
||||
|
||||
public void SetController(Duplicati.Library.Main.Controller controller)
|
||||
internal Library.Main.Controller? Controller { get; set; }
|
||||
|
||||
public void SetController(Library.Main.Controller? controller)
|
||||
{
|
||||
Controller = controller;
|
||||
}
|
||||
@@ -93,7 +106,7 @@ namespace Duplicati.Server
|
||||
public long OriginalUploadSpeed { get; set; }
|
||||
public long OriginalDownloadSpeed { get; set; }
|
||||
|
||||
public void UpdateThrottleSpeed(string uploadSpeed, string downloadSpeed)
|
||||
public void UpdateThrottleSpeeds(string? uploadSpeed, string? downloadSpeed)
|
||||
{
|
||||
var controller = this.Controller;
|
||||
if (controller == null)
|
||||
@@ -108,14 +121,14 @@ namespace Duplicati.Server
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(uploadSpeed))
|
||||
server_upload_throttle = Duplicati.Library.Utility.Sizeparser.ParseSize(uploadSpeed, "kb");
|
||||
server_upload_throttle = Sizeparser.ParseSize(uploadSpeed, "kb");
|
||||
}
|
||||
catch { }
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(downloadSpeed))
|
||||
server_download_throttle = Duplicati.Library.Utility.Sizeparser.ParseSize(downloadSpeed, "kb");
|
||||
server_download_throttle = Sizeparser.ParseSize(downloadSpeed, "kb");
|
||||
}
|
||||
catch { }
|
||||
|
||||
@@ -159,13 +172,13 @@ namespace Duplicati.Server
|
||||
return new CustomRunnerTask(runner);
|
||||
}
|
||||
|
||||
public static IRunnerData CreateTask(Duplicati.Server.Serialization.DuplicatiOperation operation, Duplicati.Server.Serialization.Interface.IBackup backup, IDictionary<string, string> extraOptions = null, string[] filterStrings = null, string[] extraArguments = null, int pageSize = 0, int pageOffset = 0)
|
||||
public static IRunnerData CreateTask(DuplicatiOperation operation, IBackup backup, IDictionary<string, string?>? extraOptions = null, string[]? filterStrings = null, string[]? extraArguments = null, int pageSize = 0, int pageOffset = 0)
|
||||
{
|
||||
return new RunnerData()
|
||||
{
|
||||
Operation = operation,
|
||||
Backup = backup,
|
||||
ExtraOptions = extraOptions,
|
||||
ExtraOptions = extraOptions ?? new Dictionary<string, string?>(),
|
||||
FilterStrings = filterStrings,
|
||||
ExtraArguments = extraArguments,
|
||||
PageSize = pageSize,
|
||||
@@ -173,15 +186,26 @@ namespace Duplicati.Server
|
||||
};
|
||||
}
|
||||
|
||||
public static IRunnerData CreateListTask(Duplicati.Server.Serialization.Interface.IBackup backup, string[] filters, bool onlyPrefix, bool allVersions, bool folderContents, DateTime time)
|
||||
public static IRunnerData CreateDeleteTask(IBackup backup, IDictionary<string, string?> extraOptions, Action<IRunnerData>? afterTaskFinished = null)
|
||||
{
|
||||
var dict = new Dictionary<string, string>();
|
||||
return new RunnerData()
|
||||
{
|
||||
Operation = DuplicatiOperation.Delete,
|
||||
Backup = backup,
|
||||
ExtraOptions = extraOptions,
|
||||
AfterTaskFinished = afterTaskFinished
|
||||
};
|
||||
}
|
||||
|
||||
public static IRunnerData CreateListTask(IBackup backup, string[]? filters, bool onlyPrefix, bool allVersions, bool folderContents, DateTime time)
|
||||
{
|
||||
var dict = new Dictionary<string, string?>();
|
||||
if (onlyPrefix)
|
||||
dict["list-prefix-only"] = "true";
|
||||
if (allVersions)
|
||||
dict["all-versions"] = "true";
|
||||
if (time.Ticks > 0)
|
||||
dict["time"] = Duplicati.Library.Utility.Utility.SerializeDateTime(time.ToUniversalTime());
|
||||
dict["time"] = Utility.SerializeDateTime(time.ToUniversalTime());
|
||||
if (folderContents)
|
||||
dict["list-folder-contents"] = "true";
|
||||
|
||||
@@ -192,19 +216,19 @@ namespace Duplicati.Server
|
||||
filters);
|
||||
}
|
||||
|
||||
public static IRunnerData CreateListFilesetsTask(Duplicati.Server.Serialization.Interface.IBackup backup, Dictionary<string, string> extraOptions = null)
|
||||
public static IRunnerData CreateListFilesetsTask(IBackup backup, Dictionary<string, string?>? extraOptions = null)
|
||||
{
|
||||
return CreateTask(
|
||||
DuplicatiOperation.ListFilesets,
|
||||
backup,
|
||||
extraOptions ?? new Dictionary<string, string>());
|
||||
extraOptions ?? new Dictionary<string, string?>());
|
||||
}
|
||||
|
||||
public static IRunnerData CreateListFolderContents(Duplicati.Server.Serialization.Interface.IBackup backup, string[] folders, DateTime time, int pageSize, int pageOffset)
|
||||
public static IRunnerData CreateListFolderContents(IBackup backup, string[]? folders, DateTime time, int pageSize, int pageOffset)
|
||||
{
|
||||
var dict = new Dictionary<string, string>();
|
||||
var dict = new Dictionary<string, string?>();
|
||||
if (time.Ticks > 0)
|
||||
dict["time"] = Duplicati.Library.Utility.Utility.SerializeDateTime(time.ToUniversalTime());
|
||||
dict["time"] = Utility.SerializeDateTime(time.ToUniversalTime());
|
||||
|
||||
return CreateTask(
|
||||
DuplicatiOperation.ListFolderContents,
|
||||
@@ -215,9 +239,9 @@ namespace Duplicati.Server
|
||||
pageOffset: pageOffset);
|
||||
}
|
||||
|
||||
public static IRunnerData ListFileVersionsTask(Duplicati.Server.Serialization.Interface.IBackup backup, string[] filepaths, int pageSize, int pageOffset)
|
||||
public static IRunnerData ListFileVersionsTask(IBackup backup, string[]? filepaths, int pageSize, int pageOffset)
|
||||
{
|
||||
var dict = new Dictionary<string, string>();
|
||||
var dict = new Dictionary<string, string?>();
|
||||
return CreateTask(
|
||||
DuplicatiOperation.ListFileVersions,
|
||||
backup,
|
||||
@@ -227,11 +251,11 @@ namespace Duplicati.Server
|
||||
pageOffset: pageOffset);
|
||||
}
|
||||
|
||||
public static IRunnerData CreateSearchEntriesTask(Duplicati.Server.Serialization.Interface.IBackup backup, string[] filters, string[] folders, DateTime time, int pageSize, int pageOffset)
|
||||
public static IRunnerData CreateSearchEntriesTask(IBackup backup, string[]? filters, string[]? folders, DateTime time, int pageSize, int pageOffset)
|
||||
{
|
||||
var dict = new Dictionary<string, string>();
|
||||
var dict = new Dictionary<string, string?>();
|
||||
if (time.Ticks > 0)
|
||||
dict["time"] = Duplicati.Library.Utility.Utility.SerializeDateTime(time.ToUniversalTime());
|
||||
dict["time"] = Utility.SerializeDateTime(time.ToUniversalTime());
|
||||
|
||||
return CreateTask(
|
||||
DuplicatiOperation.SearchEntries,
|
||||
@@ -244,17 +268,17 @@ namespace Duplicati.Server
|
||||
}
|
||||
|
||||
|
||||
public static IRunnerData CreateRestoreTask(Duplicati.Server.Serialization.Interface.IBackup backup, string[] filters,
|
||||
DateTime time, string restoreTarget, bool overwrite, bool restore_permissions,
|
||||
bool skip_metadata, string passphrase)
|
||||
public static IRunnerData CreateRestoreTask(IBackup backup, string[]? filters,
|
||||
DateTime time, string? restoreTarget, bool overwrite, bool restore_permissions,
|
||||
bool skip_metadata, string? passphrase)
|
||||
{
|
||||
var dict = new Dictionary<string, string>
|
||||
var dict = new Dictionary<string, string?>
|
||||
{
|
||||
["time"] = Library.Utility.Utility.SerializeDateTime(time.ToUniversalTime()),
|
||||
["overwrite"] = overwrite ? Boolean.TrueString : Boolean.FalseString,
|
||||
["restore-permissions"] = restore_permissions ? Boolean.TrueString : Boolean.FalseString,
|
||||
["skip-metadata"] = skip_metadata ? Boolean.TrueString : Boolean.FalseString,
|
||||
["allow-passphrase-change"] = Boolean.TrueString
|
||||
["time"] = Utility.SerializeDateTime(time.ToUniversalTime()),
|
||||
["overwrite"] = overwrite ? bool.TrueString : bool.FalseString,
|
||||
["restore-permissions"] = restore_permissions ? bool.TrueString : bool.FalseString,
|
||||
["skip-metadata"] = skip_metadata ? bool.TrueString : bool.FalseString,
|
||||
["allow-passphrase-change"] = bool.TrueString
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(restoreTarget))
|
||||
dict["restore-path"] = SpecialFolders.ExpandEnvironmentVariables(restoreTarget);
|
||||
@@ -267,26 +291,26 @@ namespace Duplicati.Server
|
||||
dict,
|
||||
filters);
|
||||
}
|
||||
private class MessageSink : Duplicati.Library.Main.IMessageSink
|
||||
private class MessageSink : Library.Main.IMessageSink
|
||||
{
|
||||
private class ProgressState : Server.Serialization.Interface.IProgressEventData
|
||||
private class ProgressState : IProgressEventData
|
||||
{
|
||||
private readonly string m_backupID;
|
||||
private readonly string? m_backupID;
|
||||
private readonly long m_taskID;
|
||||
|
||||
internal Duplicati.Library.Main.BackendActionType m_backendAction;
|
||||
internal string m_backendPath;
|
||||
internal Library.Main.BackendActionType m_backendAction;
|
||||
internal string? m_backendPath;
|
||||
internal long m_backendFileSize;
|
||||
internal long m_backendFileProgress;
|
||||
internal long m_backendSpeed;
|
||||
internal bool m_backendIsBlocking;
|
||||
|
||||
internal string m_currentFilename;
|
||||
internal string? m_currentFilename;
|
||||
internal long m_currentFilesize;
|
||||
internal long m_currentFileoffset;
|
||||
internal bool m_currentFilecomplete;
|
||||
|
||||
internal Duplicati.Library.Main.OperationPhase m_phase;
|
||||
internal Library.Main.OperationPhase m_phase;
|
||||
internal float m_overallProgress;
|
||||
internal long m_processedFileCount;
|
||||
internal long m_processedFileSize;
|
||||
@@ -294,7 +318,7 @@ namespace Duplicati.Server
|
||||
internal long m_totalFileSize;
|
||||
internal bool m_stillCounting;
|
||||
|
||||
public ProgressState(long taskId, string backupId)
|
||||
public ProgressState(long taskId, string? backupId)
|
||||
{
|
||||
m_backupID = backupId;
|
||||
m_taskID = taskId;
|
||||
@@ -306,15 +330,15 @@ namespace Duplicati.Server
|
||||
}
|
||||
|
||||
#region IProgressEventData implementation
|
||||
public string BackupID { get { return m_backupID; } }
|
||||
public string? BackupID { get { return m_backupID; } }
|
||||
public long TaskID { get { return m_taskID; } }
|
||||
public string BackendAction { get { return m_backendAction.ToString(); } }
|
||||
public string BackendPath { get { return m_backendPath; } }
|
||||
public string? BackendPath { get { return m_backendPath; } }
|
||||
public long BackendFileSize { get { return m_backendFileSize; } }
|
||||
public long BackendFileProgress { get { return m_backendFileProgress; } }
|
||||
public long BackendSpeed { get { return m_backendSpeed; } }
|
||||
public bool BackendIsBlocking { get { return m_backendIsBlocking; } }
|
||||
public string CurrentFilename { get { return m_currentFilename; } }
|
||||
public string? CurrentFilename { get { return m_currentFilename; } }
|
||||
public long CurrentFilesize { get { return m_currentFilesize; } }
|
||||
public long CurrentFileoffset { get { return m_currentFileoffset; } }
|
||||
public bool CurrentFilecomplete { get { return m_currentFilecomplete; } }
|
||||
@@ -329,16 +353,16 @@ namespace Duplicati.Server
|
||||
}
|
||||
|
||||
private readonly ProgressState m_state;
|
||||
private Duplicati.Library.Main.IBackendProgress m_backendProgress;
|
||||
private Duplicati.Library.Main.IOperationProgress m_operationProgress;
|
||||
private Library.Main.IBackendProgress? m_backendProgress;
|
||||
private Library.Main.IOperationProgress? m_operationProgress;
|
||||
private readonly object m_lock = new object();
|
||||
|
||||
public MessageSink(long taskId, string backupId)
|
||||
public MessageSink(long taskId, string? backupId)
|
||||
{
|
||||
m_state = new ProgressState(taskId, backupId);
|
||||
}
|
||||
|
||||
public Server.Serialization.Interface.IProgressEventData Copy()
|
||||
public Serialization.Interface.IProgressEventData Copy()
|
||||
{
|
||||
lock (m_lock)
|
||||
{
|
||||
@@ -387,16 +411,18 @@ namespace Duplicati.Server
|
||||
#endregion
|
||||
}
|
||||
|
||||
public static string GetCommandLine(IRunnerData data)
|
||||
public static string GetCommandLine(Connection databaseConnection, IRunnerData data)
|
||||
{
|
||||
var backup = data.Backup;
|
||||
if (backup == null)
|
||||
throw new ArgumentNullException(nameof(backup));
|
||||
|
||||
var options = ApplyOptions(backup, GetCommonOptions());
|
||||
var options = ApplyOptions(databaseConnection, backup, GetCommonOptions(databaseConnection));
|
||||
if (data.ExtraOptions != null)
|
||||
foreach (var k in data.ExtraOptions)
|
||||
options[k.Key] = k.Value;
|
||||
|
||||
var cf = FIXMEGlobal.DataConnection.Filters;
|
||||
var cf = databaseConnection.Filters;
|
||||
var bf = backup.Filters;
|
||||
|
||||
var sources =
|
||||
@@ -411,36 +437,38 @@ namespace Duplicati.Server
|
||||
);
|
||||
|
||||
var cmd = new System.Text.StringBuilder();
|
||||
cmd.Append(Library.Utility.Utility.WrapAsCommandLine(new string[] { exe, "backup", backup.TargetURL }, false));
|
||||
cmd.Append(Utility.WrapAsCommandLine([exe, "backup", backup.TargetURL], false));
|
||||
|
||||
cmd.Append(" ");
|
||||
cmd.Append(Library.Utility.Utility.WrapAsCommandLine(sources, true));
|
||||
cmd.Append(Utility.WrapAsCommandLine(sources, true));
|
||||
|
||||
// TODO: We should check each option to see if it is a path, and allow expansion on that
|
||||
foreach (var opt in options)
|
||||
cmd.AppendFormat(" --{0}={1}", opt.Key, Library.Utility.Utility.WrapCommandLineElement(opt.Value, false));
|
||||
cmd.AppendFormat(" --{0}={1}", opt.Key, Utility.WrapCommandLineElement(opt.Value, false));
|
||||
|
||||
if (cf != null)
|
||||
foreach (var f in cf)
|
||||
cmd.AppendFormat(" --{0}={1}", f.Include ? "include" : "exclude", Library.Utility.Utility.WrapCommandLineElement(f.Expression, true));
|
||||
cmd.AppendFormat(" --{0}={1}", f.Include ? "include" : "exclude", Utility.WrapCommandLineElement(f.Expression, true));
|
||||
|
||||
if (bf != null)
|
||||
foreach (var f in bf)
|
||||
cmd.AppendFormat(" --{0}={1}", f.Include ? "include" : "exclude", Library.Utility.Utility.WrapCommandLineElement(f.Expression, true));
|
||||
cmd.AppendFormat(" --{0}={1}", f.Include ? "include" : "exclude", Utility.WrapCommandLineElement(f.Expression, true));
|
||||
|
||||
return cmd.ToString();
|
||||
}
|
||||
|
||||
public static string[] GetCommandLineParts(IRunnerData data)
|
||||
public static string[] GetCommandLineParts(Connection databaseConnection, IRunnerData data)
|
||||
{
|
||||
var backup = data.Backup;
|
||||
if (backup == null)
|
||||
throw new ArgumentNullException(nameof(backup));
|
||||
|
||||
var options = ApplyOptions(backup, GetCommonOptions());
|
||||
var options = ApplyOptions(databaseConnection, backup, GetCommonOptions(databaseConnection));
|
||||
if (data.ExtraOptions != null)
|
||||
foreach (var k in data.ExtraOptions)
|
||||
options[k.Key] = k.Value;
|
||||
|
||||
var cf = FIXMEGlobal.DataConnection.Filters;
|
||||
var cf = databaseConnection.Filters;
|
||||
var bf = backup.Filters;
|
||||
|
||||
var sources =
|
||||
@@ -469,44 +497,56 @@ namespace Duplicati.Server
|
||||
return parts.ToArray();
|
||||
}
|
||||
|
||||
public static Duplicati.Library.Interface.IBasicResults Run(IRunnerData data, bool fromQueue)
|
||||
public static IBasicResults? Run(Connection databaseConnection, EventPollNotify eventPollNotify, INotificationUpdateService notificationUpdateService, IProgressStateProviderService progressStateProviderService, IApplicationSettings applicationSettings, IQueuedTask data, bool fromQueue)
|
||||
{
|
||||
if (data is IRunnerData runnerData)
|
||||
return RunInternal(databaseConnection, eventPollNotify, notificationUpdateService, progressStateProviderService, applicationSettings, runnerData, fromQueue);
|
||||
|
||||
throw new ArgumentException("Invalid task type", nameof(data));
|
||||
}
|
||||
|
||||
private static IBasicResults? RunInternal(Connection databaseConnection, EventPollNotify eventPollNotify, INotificationUpdateService notificationUpdateService, IProgressStateProviderService progressStateProviderService, IApplicationSettings applicationSettings, IRunnerData data, bool fromQueue)
|
||||
{
|
||||
data.TaskStarted = DateTime.Now;
|
||||
if (data is CustomRunnerTask task)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sink = new MessageSink(task.TaskID, null);
|
||||
FIXMEGlobal.GenerateProgressState = sink.Copy;
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
progressStateProviderService.GenerateProgressState = sink.Copy;
|
||||
eventPollNotify.SignalNewEvent();
|
||||
|
||||
task.Run(sink);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FIXMEGlobal.DataConnection.LogError(string.Empty, "Failed while executing custom task", ex);
|
||||
databaseConnection.LogError(string.Empty, "Failed while executing custom task", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
data.TaskFinished = DateTime.Now;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var backup = data.Backup;
|
||||
if (backup.Metadata == null)
|
||||
{
|
||||
backup.Metadata = new Dictionary<string, string>();
|
||||
}
|
||||
if (backup == null)
|
||||
throw new ArgumentNullException(nameof(backup));
|
||||
|
||||
Duplicati.Library.Utility.TempFolder tempfolder = null;
|
||||
backup.Metadata ??= new Dictionary<string, string>();
|
||||
TempFolder? tempfolder = null;
|
||||
|
||||
try
|
||||
{
|
||||
var sink = new MessageSink(data.TaskID, backup.ID);
|
||||
if (fromQueue)
|
||||
{
|
||||
FIXMEGlobal.GenerateProgressState = () => sink.Copy();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
progressStateProviderService.GenerateProgressState = () => sink.Copy();
|
||||
eventPollNotify.SignalNewEvent();
|
||||
}
|
||||
|
||||
var options = ApplyOptions(backup, GetCommonOptions());
|
||||
var options = ApplyOptions(databaseConnection, backup, GetCommonOptions(databaseConnection));
|
||||
if (data.ExtraOptions != null)
|
||||
foreach (var k in data.ExtraOptions)
|
||||
options[k.Key] = k.Value;
|
||||
@@ -514,14 +554,14 @@ namespace Duplicati.Server
|
||||
// Pack in the system or task config for easy restore
|
||||
if (data.Operation == DuplicatiOperation.Backup && options.ContainsKey("store-task-config"))
|
||||
{
|
||||
tempfolder = StoreTaskConfigAndGetTempFolder(data, options);
|
||||
tempfolder = StoreTaskConfigAndGetTempFolder(databaseConnection, data, options);
|
||||
}
|
||||
|
||||
// Attach a log scope that tags all messages to relay the TaskID and BackupID
|
||||
using (Library.Logging.Log.StartScope(log =>
|
||||
{
|
||||
log[LogWriteHandler.LOG_EXTRA_TASKID] = data.TaskID.ToString();
|
||||
log[LogWriteHandler.LOG_EXTRA_BACKUPID] = data.BackupID;
|
||||
log[ILogWriteHandler.LiveLogEntry.LOG_EXTRA_TASKID] = data.TaskID.ToString();
|
||||
log[ILogWriteHandler.LiveLogEntry.LOG_EXTRA_BACKUPID] = data.BackupID;
|
||||
}))
|
||||
|
||||
using (tempfolder)
|
||||
@@ -542,22 +582,23 @@ namespace Duplicati.Server
|
||||
catch { }
|
||||
|
||||
((RunnerData)data).Controller = controller;
|
||||
data.UpdateThrottleSpeed(FIXMEGlobal.DataConnection.ApplicationSettings.UploadSpeedLimit, FIXMEGlobal.DataConnection.ApplicationSettings.DownloadSpeedLimit);
|
||||
var appSettings = databaseConnection.ApplicationSettings;
|
||||
data.UpdateThrottleSpeeds(appSettings.UploadSpeedLimit, appSettings.DownloadSpeedLimit);
|
||||
|
||||
// Pass on the provider, will be replaced if configured in the backup
|
||||
controller.SetSecretProvider(FIXMEGlobal.SecretProvider);
|
||||
controller.SetSecretProvider(applicationSettings.SecretProvider);
|
||||
|
||||
if (backup.Metadata.ContainsKey("LastCompactFinished"))
|
||||
controller.LastCompact = Library.Utility.Utility.DeserializeDateTime(backup.Metadata["LastCompactFinished"]);
|
||||
controller.LastCompact = Utility.DeserializeDateTime(backup.Metadata["LastCompactFinished"]);
|
||||
|
||||
if (backup.Metadata.ContainsKey("LastVacuumFinished"))
|
||||
controller.LastVacuum = Library.Utility.Utility.DeserializeDateTime(backup.Metadata["LastVacuumFinished"]);
|
||||
controller.LastVacuum = Utility.DeserializeDateTime(backup.Metadata["LastVacuumFinished"]);
|
||||
|
||||
switch (data.Operation)
|
||||
{
|
||||
case DuplicatiOperation.Backup:
|
||||
{
|
||||
var filter = ApplyFilter(backup, GetCommonFilter());
|
||||
var filter = ApplyFilter(backup, GetCommonFilter(databaseConnection));
|
||||
var sources =
|
||||
(from n in backup.Sources
|
||||
let p = SpecialFolders.ExpandEnvironmentVariables(n)
|
||||
@@ -565,62 +606,62 @@ namespace Duplicati.Server
|
||||
select p).ToArray();
|
||||
|
||||
var r = controller.Backup(sources, filter);
|
||||
UpdateMetadata(backup, r);
|
||||
UpdateMetadataBase(databaseConnection, eventPollNotify, notificationUpdateService, backup, r);
|
||||
return r;
|
||||
}
|
||||
case DuplicatiOperation.List:
|
||||
{
|
||||
var r = controller.List(data.FilterStrings, null);
|
||||
UpdateMetadata(backup, r);
|
||||
UpdateMetadataBase(databaseConnection, eventPollNotify, notificationUpdateService, backup, r);
|
||||
return r;
|
||||
}
|
||||
case DuplicatiOperation.Repair:
|
||||
{
|
||||
var r = controller.Repair(data.FilterStrings == null ? null : new Library.Utility.FilterExpression(data.FilterStrings));
|
||||
UpdateMetadata(backup, r);
|
||||
UpdateMetadataBase(databaseConnection, eventPollNotify, notificationUpdateService, backup, r);
|
||||
return r;
|
||||
}
|
||||
case DuplicatiOperation.RepairUpdate:
|
||||
{
|
||||
var r = controller.UpdateDatabaseWithVersions();
|
||||
UpdateMetadata(backup, r);
|
||||
UpdateMetadataBase(databaseConnection, eventPollNotify, notificationUpdateService, backup, r);
|
||||
return r;
|
||||
}
|
||||
case DuplicatiOperation.Remove:
|
||||
{
|
||||
var r = controller.Delete();
|
||||
UpdateMetadata(backup, r);
|
||||
UpdateMetadataBase(databaseConnection, eventPollNotify, notificationUpdateService, backup, r);
|
||||
return r;
|
||||
}
|
||||
case DuplicatiOperation.Restore:
|
||||
{
|
||||
var r = controller.Restore(data.FilterStrings);
|
||||
UpdateMetadata(backup, r);
|
||||
UpdateMetadataBase(databaseConnection, eventPollNotify, notificationUpdateService, backup, r);
|
||||
return r;
|
||||
}
|
||||
case DuplicatiOperation.Verify:
|
||||
{
|
||||
var r = controller.Test();
|
||||
UpdateMetadata(backup, r);
|
||||
UpdateMetadataBase(databaseConnection, eventPollNotify, notificationUpdateService, backup, r);
|
||||
return r;
|
||||
}
|
||||
case DuplicatiOperation.Compact:
|
||||
{
|
||||
var r = controller.Compact();
|
||||
UpdateMetadata(backup, r);
|
||||
UpdateMetadataBase(databaseConnection, eventPollNotify, notificationUpdateService, backup, r);
|
||||
return r;
|
||||
}
|
||||
case DuplicatiOperation.CreateReport:
|
||||
{
|
||||
using (var tf = new Duplicati.Library.Utility.TempFile())
|
||||
using (var tf = new Library.Utility.TempFile())
|
||||
{
|
||||
var r = controller.CreateLogDatabase(tf);
|
||||
var tempid = FIXMEGlobal.DataConnection.RegisterTempFile("create-bug-report", r.TargetPath, DateTime.Now.AddDays(3));
|
||||
var tempid = databaseConnection.RegisterTempFile("create-bug-report", r.TargetPath, DateTime.Now.AddDays(3));
|
||||
|
||||
if (string.Equals(tf, r.TargetPath, Library.Utility.Utility.ClientFilenameStringComparison))
|
||||
if (string.Equals(tf, r.TargetPath, Utility.ClientFilenameStringComparison))
|
||||
tf.Protected = true;
|
||||
|
||||
FIXMEGlobal.DataConnection.RegisterNotification(
|
||||
databaseConnection.RegisterNotification(
|
||||
NotificationType.Information,
|
||||
"Bugreport ready",
|
||||
"Bugreport is ready for download",
|
||||
@@ -640,38 +681,40 @@ namespace Duplicati.Server
|
||||
case DuplicatiOperation.ListRemote:
|
||||
{
|
||||
var r = controller.ListRemote();
|
||||
UpdateMetadata(backup, r);
|
||||
UpdateMetadataBase(databaseConnection, eventPollNotify, notificationUpdateService, backup, r);
|
||||
return r;
|
||||
}
|
||||
|
||||
case DuplicatiOperation.Delete:
|
||||
{
|
||||
if (Library.Utility.Utility.ParseBoolOption(data.ExtraOptions.AsReadOnly(), "delete-remote-files"))
|
||||
controller.DeleteAllRemoteFiles();
|
||||
|
||||
if (Library.Utility.Utility.ParseBoolOption(data.ExtraOptions.AsReadOnly(), "delete-local-db"))
|
||||
if (data.ExtraOptions != null)
|
||||
{
|
||||
string dbpath;
|
||||
options.TryGetValue("dbpath", out dbpath);
|
||||
if (Utility.ParseBoolOption(data.ExtraOptions.AsReadOnly(), "delete-remote-files"))
|
||||
controller.DeleteAllRemoteFiles();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(dbpath) && System.IO.File.Exists(dbpath))
|
||||
System.IO.File.Delete(dbpath);
|
||||
if (Utility.ParseBoolOption(data.ExtraOptions.AsReadOnly(), "delete-local-db"))
|
||||
{
|
||||
options.TryGetValue("dbpath", out var dbpath);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(dbpath) && System.IO.File.Exists(dbpath))
|
||||
System.IO.File.Delete(dbpath);
|
||||
}
|
||||
}
|
||||
FIXMEGlobal.DataConnection.DeleteBackup(backup);
|
||||
FIXMEGlobal.Scheduler.Reschedule();
|
||||
databaseConnection.DeleteBackup(backup);
|
||||
(data as RunnerData)?.AfterTaskFinished?.Invoke(data);
|
||||
return null;
|
||||
}
|
||||
case DuplicatiOperation.Vacuum:
|
||||
{
|
||||
var r = controller.Vacuum();
|
||||
UpdateMetadata(backup, r);
|
||||
UpdateMetadataBase(databaseConnection, eventPollNotify, notificationUpdateService, backup, r);
|
||||
return r;
|
||||
}
|
||||
|
||||
case DuplicatiOperation.ListFilesets:
|
||||
{
|
||||
var r = controller.ListFilesets();
|
||||
UpdateMetadata(backup, r);
|
||||
UpdateMetadataBase(databaseConnection, eventPollNotify, notificationUpdateService, backup, r);
|
||||
return r;
|
||||
}
|
||||
case DuplicatiOperation.ListFolderContents:
|
||||
@@ -696,8 +739,9 @@ namespace Duplicati.Server
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FIXMEGlobal.DataConnection.LogError(data.Backup.ID, string.Format("Failed while executing {0} \"{1}\" (id: {2})", data.Operation, data.Backup.Name, data.Backup.ID), ex);
|
||||
UpdateMetadataError(data.Backup, ex);
|
||||
databaseConnection.LogError(data.Backup?.ID, string.Format("Failed while executing {0} \"{1}\" (id: {2})", data.Operation, data.Backup?.Name, data.Backup?.ID), ex);
|
||||
if (data.Backup != null)
|
||||
UpdateMetadataError(databaseConnection, notificationUpdateService, data.Backup, ex);
|
||||
Library.UsageReporter.Reporter.Report(ex);
|
||||
|
||||
if (!fromQueue)
|
||||
@@ -707,29 +751,33 @@ namespace Duplicati.Server
|
||||
}
|
||||
finally
|
||||
{
|
||||
((RunnerData)data).Controller = null;
|
||||
data.SetController(null);
|
||||
data.TaskFinished = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
private static Duplicati.Library.Utility.TempFolder StoreTaskConfigAndGetTempFolder(IRunnerData data, Dictionary<string, string> options)
|
||||
private static TempFolder? StoreTaskConfigAndGetTempFolder(Connection databaseConnection, IRunnerData data, Dictionary<string, string?> options)
|
||||
{
|
||||
if (data.Backup == null)
|
||||
throw new ArgumentNullException(nameof(data.Backup));
|
||||
|
||||
var all_tasks = string.Equals(options["store-task-config"], "all", StringComparison.OrdinalIgnoreCase) || string.Equals(options["store-task-config"], "*", StringComparison.OrdinalIgnoreCase);
|
||||
var this_task = Duplicati.Library.Utility.Utility.ParseBool(options["store-task-config"], false);
|
||||
var this_task = Utility.ParseBool(options["store-task-config"], false);
|
||||
|
||||
options.Remove("store-task-config");
|
||||
|
||||
Duplicati.Library.Utility.TempFolder tempfolder = null;
|
||||
TempFolder? tempfolder = null;
|
||||
if (all_tasks || this_task)
|
||||
{
|
||||
tempfolder = new Duplicati.Library.Utility.TempFolder();
|
||||
tempfolder = new TempFolder();
|
||||
var temppath = System.IO.Path.Combine(tempfolder, "task-setup.json");
|
||||
using (var tempfile = Duplicati.Library.Utility.TempFile.WrapExistingFile(temppath))
|
||||
using (var tempfile = Library.Utility.TempFile.WrapExistingFile(temppath))
|
||||
{
|
||||
object taskdata = null;
|
||||
object? taskdata = null;
|
||||
if (all_tasks)
|
||||
taskdata = FIXMEGlobal.DataConnection.Backups.Where(x => !x.IsTemporary).Select(x => FIXMEGlobal.DataConnection.PrepareBackupForExport(FIXMEGlobal.DataConnection.GetBackup(x.ID)));
|
||||
taskdata = databaseConnection.Backups.Where(x => !x.IsTemporary).Select(x => databaseConnection.PrepareBackupForExport(databaseConnection.GetBackup(x.ID)!));
|
||||
else
|
||||
taskdata = new[] { FIXMEGlobal.DataConnection.PrepareBackupForExport(data.Backup) };
|
||||
taskdata = new[] { databaseConnection.PrepareBackupForExport(data.Backup) };
|
||||
|
||||
using (var fs = System.IO.File.OpenWrite(tempfile))
|
||||
using (var sw = new System.IO.StreamWriter(fs, System.Text.Encoding.UTF8))
|
||||
@@ -737,7 +785,7 @@ namespace Duplicati.Server
|
||||
|
||||
tempfile.Protected = true;
|
||||
|
||||
options.TryGetValue("control-files", out string controlfiles);
|
||||
options.TryGetValue("control-files", out var controlfiles);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(controlfiles))
|
||||
controlfiles = tempfile;
|
||||
@@ -750,20 +798,20 @@ namespace Duplicati.Server
|
||||
return tempfolder;
|
||||
}
|
||||
|
||||
private static void UpdateMetadataError(Duplicati.Server.Serialization.Interface.IBackup backup, Exception ex)
|
||||
private static void UpdateMetadataError(Connection databaseConnection, INotificationUpdateService notificationUpdateService, IBackup backup, Exception ex)
|
||||
{
|
||||
backup.Metadata["LastErrorDate"] = Library.Utility.Utility.SerializeDateTime(DateTime.UtcNow);
|
||||
backup.Metadata["LastErrorDate"] = Utility.SerializeDateTime(DateTime.UtcNow);
|
||||
backup.Metadata["LastErrorMessage"] = ex.Message;
|
||||
|
||||
if (!backup.IsTemporary)
|
||||
FIXMEGlobal.DataConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null);
|
||||
databaseConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null);
|
||||
|
||||
string messageid = null;
|
||||
string? messageid = null;
|
||||
if (ex is UserInformationException exception)
|
||||
messageid = exception.HelpID;
|
||||
|
||||
FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
|
||||
FIXMEGlobal.DataConnection.RegisterNotification(
|
||||
notificationUpdateService.IncrementLastDataUpdateId();
|
||||
databaseConnection.RegisterNotification(
|
||||
NotificationType.Error,
|
||||
backup.IsTemporary ?
|
||||
"Error" : string.Format("Error while running {0}", backup.Name),
|
||||
@@ -781,31 +829,31 @@ namespace Duplicati.Server
|
||||
);
|
||||
}
|
||||
|
||||
private static void UpdateMetadataLastCompact(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.ICompactResults r)
|
||||
private static void UpdateMetadataLastCompact(IBackup backup, ICompactResults r)
|
||||
{
|
||||
if (r != null)
|
||||
{
|
||||
backup.Metadata["LastCompactDuration"] = r.Duration.ToString();
|
||||
backup.Metadata["LastCompactStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime());
|
||||
backup.Metadata["LastCompactFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime());
|
||||
backup.Metadata["LastCompactStarted"] = Utility.SerializeDateTime(r.BeginTime.ToUniversalTime());
|
||||
backup.Metadata["LastCompactFinished"] = Utility.SerializeDateTime(r.EndTime.ToUniversalTime());
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateMetadataLastVacuum(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IVacuumResults r)
|
||||
private static void UpdateMetadataLastVacuum(IBackup backup, IVacuumResults r)
|
||||
{
|
||||
if (r != null)
|
||||
{
|
||||
backup.Metadata["LastVacuumDuration"] = r.Duration.ToString();
|
||||
backup.Metadata["LastVacuumStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime());
|
||||
backup.Metadata["LastVacuumFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime());
|
||||
backup.Metadata["LastVacuumStarted"] = Utility.SerializeDateTime(r.BeginTime.ToUniversalTime());
|
||||
backup.Metadata["LastVacuumFinished"] = Utility.SerializeDateTime(r.EndTime.ToUniversalTime());
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateMetadata(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IParsedBackendStatistics r)
|
||||
private static void UpdateMetadataStatistics(IBackup backup, IParsedBackendStatistics r)
|
||||
{
|
||||
if (r != null)
|
||||
{
|
||||
backup.Metadata["LastBackupDate"] = Library.Utility.Utility.SerializeDateTime(r.LastBackupDate.ToUniversalTime());
|
||||
backup.Metadata["LastBackupDate"] = Utility.SerializeDateTime(r.LastBackupDate.ToUniversalTime());
|
||||
backup.Metadata["BackupListCount"] = r.BackupListCount.ToString();
|
||||
backup.Metadata["TotalQuotaSpace"] = r.TotalQuotaSpace.ToString();
|
||||
backup.Metadata["FreeQuotaSpace"] = r.FreeQuotaSpace.ToString();
|
||||
@@ -814,28 +862,28 @@ namespace Duplicati.Server
|
||||
backup.Metadata["TargetFilesSize"] = r.KnownFileSize.ToString();
|
||||
backup.Metadata["TargetFilesCount"] = r.KnownFileCount.ToString();
|
||||
backup.Metadata["TargetFilesetsCount"] = r.KnownFilesets.ToString();
|
||||
backup.Metadata["TargetSizeString"] = Duplicati.Library.Utility.Utility.FormatSizeString(r.KnownFileSize);
|
||||
backup.Metadata["TargetSizeString"] = Utility.FormatSizeString(r.KnownFileSize);
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateMetadata(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IBasicResults result)
|
||||
private static void UpdateMetadataBase(Connection databaseConnection, EventPollNotify eventPollNotify, INotificationUpdateService notificationUpdateService, IBackup backup, IBasicResults result)
|
||||
{
|
||||
if (result is IRestoreResults r1)
|
||||
{
|
||||
backup.Metadata["LastRestoreDuration"] = r1.Duration.ToString();
|
||||
backup.Metadata["LastRestoreStarted"] = Library.Utility.Utility.SerializeDateTime(result.BeginTime.ToUniversalTime());
|
||||
backup.Metadata["LastRestoreFinished"] = Library.Utility.Utility.SerializeDateTime(result.EndTime.ToUniversalTime());
|
||||
backup.Metadata["LastRestoreStarted"] = Utility.SerializeDateTime(result.BeginTime.ToUniversalTime());
|
||||
backup.Metadata["LastRestoreFinished"] = Utility.SerializeDateTime(result.EndTime.ToUniversalTime());
|
||||
}
|
||||
|
||||
if (result is IParsedBackendStatistics r2 && !result.Interrupted)
|
||||
{
|
||||
UpdateMetadata(backup, r2);
|
||||
UpdateMetadataStatistics(backup, r2);
|
||||
}
|
||||
|
||||
if (result is IBackendStatsticsReporter r3 && !result.Interrupted)
|
||||
{
|
||||
if (r3.BackendStatistics is IParsedBackendStatistics statistics)
|
||||
UpdateMetadata(backup, statistics);
|
||||
UpdateMetadataStatistics(backup, statistics);
|
||||
}
|
||||
|
||||
if (result is ICompactResults r4 && !result.Interrupted)
|
||||
@@ -858,8 +906,8 @@ namespace Duplicati.Server
|
||||
backup.Metadata["SourceFilesSize"] = r.SizeOfExaminedFiles.ToString();
|
||||
backup.Metadata["SourceFilesCount"] = r.ExaminedFiles.ToString();
|
||||
backup.Metadata["SourceSizeString"] = Duplicati.Library.Utility.Utility.FormatSizeString(r.SizeOfExaminedFiles);
|
||||
backup.Metadata["LastBackupStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime());
|
||||
backup.Metadata["LastBackupFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime());
|
||||
backup.Metadata["LastBackupStarted"] = Utility.SerializeDateTime(r.BeginTime.ToUniversalTime());
|
||||
backup.Metadata["LastBackupFinished"] = Utility.SerializeDateTime(r.EndTime.ToUniversalTime());
|
||||
backup.Metadata["LastBackupDuration"] = r.Duration.ToString();
|
||||
|
||||
if (r.CompactResults != null)
|
||||
@@ -889,7 +937,7 @@ namespace Duplicati.Server
|
||||
titleType = "Warning";
|
||||
}
|
||||
|
||||
FIXMEGlobal.DataConnection.RegisterNotification(
|
||||
databaseConnection.RegisterNotification(
|
||||
r.FilesWithError == 0 && !r.Errors.Any() ? NotificationType.Warning : NotificationType.Error,
|
||||
backup.IsTemporary ? "Warning" : $"{titleType} while running {backup.Name}",
|
||||
message,
|
||||
@@ -935,7 +983,7 @@ namespace Duplicati.Server
|
||||
else if (result.ParsedResult == ParsedResultType.Error && result.Errors.Count() == 1)
|
||||
message = $"Error: {result.Errors.Single()}";
|
||||
|
||||
FIXMEGlobal.DataConnection.RegisterNotification(
|
||||
databaseConnection.RegisterNotification(
|
||||
type,
|
||||
title,
|
||||
message,
|
||||
@@ -950,10 +998,10 @@ namespace Duplicati.Server
|
||||
}
|
||||
|
||||
if (!backup.IsTemporary)
|
||||
FIXMEGlobal.DataConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null);
|
||||
databaseConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null);
|
||||
|
||||
FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
notificationUpdateService.IncrementLastDataUpdateId();
|
||||
eventPollNotify.SignalNewEvent();
|
||||
}
|
||||
|
||||
private static bool TestIfOptionApplies()
|
||||
@@ -962,23 +1010,20 @@ namespace Duplicati.Server
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void DisableModule(string module, Dictionary<string, string> options)
|
||||
private static void DisableModule(string module, Dictionary<string, string?> options)
|
||||
{
|
||||
string disabledModules;
|
||||
string enabledModules;
|
||||
|
||||
if (options.TryGetValue("enable-module", out enabledModules))
|
||||
if (options.TryGetValue("enable-module", out var enabledModules))
|
||||
{
|
||||
var emods = (enabledModules ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
options["enable-module"] = string.Join(",", emods.Where(x => module.Equals(x, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
options.TryGetValue("disable-module", out disabledModules);
|
||||
options.TryGetValue("disable-module", out var disabledModules);
|
||||
var mods = (disabledModules ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
options["disable-module"] = string.Join(",", mods.Union(new string[] { module }).Distinct(StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
internal static Dictionary<string, string> ApplyOptions(Duplicati.Server.Serialization.Interface.IBackup backup, Dictionary<string, string> options)
|
||||
internal static Dictionary<string, string?> ApplyOptions(Connection databaseConnection, Serialization.Interface.IBackup backup, Dictionary<string, string?> options)
|
||||
{
|
||||
options["backup-name"] = backup.Name;
|
||||
options["dbpath"] = backup.DBPath;
|
||||
@@ -998,7 +1043,7 @@ namespace Duplicati.Server
|
||||
DisableModule("console-password-input", options);
|
||||
|
||||
// Patch in additional report urls
|
||||
var additionalReportUrl = FIXMEGlobal.DataConnection.ApplicationSettings.AdditionalReportUrl;
|
||||
var additionalReportUrl = databaseConnection.ApplicationSettings.AdditionalReportUrl;
|
||||
if (!string.IsNullOrWhiteSpace(additionalReportUrl))
|
||||
{
|
||||
options["send-http-json-urls"] = string.Join(";",
|
||||
@@ -1012,7 +1057,7 @@ namespace Duplicati.Server
|
||||
return options;
|
||||
}
|
||||
|
||||
private static Library.Utility.IFilter ApplyFilter(Serialization.Interface.IBackup backup, Library.Utility.IFilter filter)
|
||||
private static Library.Utility.IFilter? ApplyFilter(IBackup backup, Library.Utility.IFilter? filter)
|
||||
{
|
||||
var f2 = backup.Filters;
|
||||
if (f2 != null && f2.Length > 0)
|
||||
@@ -1024,26 +1069,26 @@ namespace Duplicati.Server
|
||||
? SpecialFolders.ExpandEnvironmentVariablesRegexp(n.Expression)
|
||||
: SpecialFolders.ExpandEnvironmentVariables(n.Expression)
|
||||
orderby n.Order
|
||||
select (Library.Utility.IFilter)(new Library.Utility.FilterExpression(exp, n.Include)))
|
||||
.Aggregate((a, b) => Library.Utility.FilterExpression.Combine(a, b));
|
||||
select (Library.Utility.IFilter)new FilterExpression(exp, n.Include))
|
||||
.Aggregate((a, b) => FilterExpression.Combine(a, b));
|
||||
|
||||
filter = Library.Utility.FilterExpression.Combine(filter, nf);
|
||||
filter = FilterExpression.Combine(filter, nf);
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
public static Dictionary<string, string> GetCommonOptions()
|
||||
public static Dictionary<string, string?> GetCommonOptions(Connection databaseConnection)
|
||||
{
|
||||
return
|
||||
(from n in FIXMEGlobal.DataConnection.Settings
|
||||
(from n in databaseConnection.Settings
|
||||
where TestIfOptionApplies()
|
||||
select n).ToDictionary(k => k.Name.StartsWith("--", StringComparison.Ordinal) ? k.Name.Substring(2) : k.Name, k => k.Value);
|
||||
select n).ToDictionary(k => k.Name.StartsWith("--", StringComparison.Ordinal) ? k.Name.Substring(2) : k.Name, k => (string?)k.Value);
|
||||
}
|
||||
|
||||
private static Duplicati.Library.Utility.IFilter GetCommonFilter()
|
||||
private static Library.Utility.IFilter? GetCommonFilter(Connection databaseConnection)
|
||||
{
|
||||
var filters = FIXMEGlobal.DataConnection.Filters;
|
||||
var filters = databaseConnection.Filters;
|
||||
if (filters == null || filters.Length == 0)
|
||||
return null;
|
||||
|
||||
@@ -1051,8 +1096,8 @@ namespace Duplicati.Server
|
||||
(from n in filters
|
||||
orderby n.Order
|
||||
let exp = Environment.ExpandEnvironmentVariables(n.Expression)
|
||||
select (Duplicati.Library.Utility.IFilter)(new Duplicati.Library.Utility.FilterExpression(exp, n.Include)))
|
||||
.Aggregate((a, b) => Duplicati.Library.Utility.FilterExpression.Combine(a, b));
|
||||
select (Library.Utility.IFilter)new FilterExpression(exp, n.Include))
|
||||
.Aggregate((a, b) => FilterExpression.Combine(a, b));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using Duplicati.Server.Serialization.Interface;
|
||||
|
||||
using System;
|
||||
@@ -27,7 +29,9 @@ using System.Text;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using System.Threading.Tasks;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
using Duplicati.Server.Database;
|
||||
|
||||
// TODO: Rewrite this class.
|
||||
// It should just signal what new backups to run, and not mix with the worker thread.
|
||||
@@ -39,7 +43,7 @@ namespace Duplicati.Server
|
||||
/// </summary>
|
||||
public class Scheduler
|
||||
{
|
||||
private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType<Scheduler>();
|
||||
private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType<Scheduler>();
|
||||
|
||||
/// <summary>
|
||||
/// The thread that runs the scheduler
|
||||
@@ -51,11 +55,6 @@ namespace Duplicati.Server
|
||||
/// </summary>
|
||||
private volatile bool m_terminate;
|
||||
|
||||
/// <summary>
|
||||
/// The worker thread that is invoked to do work
|
||||
/// </summary>
|
||||
private WorkerThread<Runner.IRunnerData> m_worker;
|
||||
|
||||
/// <summary>
|
||||
/// The wait event
|
||||
/// </summary>
|
||||
@@ -67,9 +66,13 @@ namespace Duplicati.Server
|
||||
private readonly object m_lock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// An event that is raised when the schedule changes
|
||||
/// The queue runner service
|
||||
/// </summary>
|
||||
public event EventHandler NewSchedule;
|
||||
private readonly IQueueRunnerService m_queueRunnerService;
|
||||
/// <summary>
|
||||
/// The data connection
|
||||
/// </summary>
|
||||
private readonly Connection m_dataConnection;
|
||||
|
||||
/// <summary>
|
||||
/// The currently scheduled items
|
||||
@@ -79,45 +82,31 @@ namespace Duplicati.Server
|
||||
/// <summary>
|
||||
/// List of update tasks, used to set the timestamp on the schedule once completed
|
||||
/// </summary>
|
||||
private Dictionary<Server.Runner.IRunnerData, Tuple<ISchedule, DateTime, DateTime>> m_updateTasks;
|
||||
private Dictionary<Runner.IRunnerData, Tuple<ISchedule, DateTime, DateTime>> m_updateTasks;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new scheduler
|
||||
/// </summary>
|
||||
public Scheduler()
|
||||
/// <param name="connection">The data connection</param>
|
||||
/// <param name="queueRunnerService">The queue runner service</param>
|
||||
public Scheduler(Connection connection, IQueueRunnerService queueRunnerService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes scheduler
|
||||
/// </summary>
|
||||
/// <param name="worker">The worker thread</param>
|
||||
public void Init(WorkerThread<Runner.IRunnerData> worker)
|
||||
{
|
||||
m_worker = worker;
|
||||
m_dataConnection = connection;
|
||||
m_queueRunnerService = queueRunnerService;
|
||||
m_thread = new Thread(new ThreadStart(Runner));
|
||||
m_worker.CompletedWork += OnCompleted;
|
||||
m_worker.StartingWork += OnStartingWork;
|
||||
m_schedule = new KeyValuePair<DateTime, ISchedule>[0];
|
||||
m_schedule = [];
|
||||
m_terminate = false;
|
||||
m_event = new AutoResetEvent(false);
|
||||
m_updateTasks = new Dictionary<Server.Runner.IRunnerData, Tuple<ISchedule, DateTime, DateTime>>();
|
||||
m_updateTasks = new Dictionary<Runner.IRunnerData, Tuple<ISchedule, DateTime, DateTime>>();
|
||||
m_thread.IsBackground = true;
|
||||
m_thread.Name = "TaskScheduler";
|
||||
m_thread.Start();
|
||||
}
|
||||
|
||||
public IList<Tuple<long, string>> GetSchedulerQueueIds()
|
||||
{
|
||||
return (from n in WorkerQueue
|
||||
where n.Backup != null
|
||||
select new Tuple<long, string>(n.TaskID, n.Backup.ID)).ToList();
|
||||
}
|
||||
|
||||
public IList<Tuple<string, DateTime>> GetProposedSchedule()
|
||||
{
|
||||
return (
|
||||
from n in FIXMEGlobal.Scheduler.Schedule
|
||||
from n in this.Schedule
|
||||
let backupid = (from t in n.Value.Tags
|
||||
where t != null && t.StartsWith("ID=", StringComparison.Ordinal)
|
||||
select t.Substring("ID=".Length)).FirstOrDefault()
|
||||
@@ -147,14 +136,6 @@ namespace Duplicati.Server
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A snapshot copy of the current worker queue, that is items that are scheduled, but waiting for execution
|
||||
/// </summary>
|
||||
public List<Runner.IRunnerData> WorkerQueue
|
||||
{
|
||||
get { return m_worker?.CurrentTasks?.Where(t => t != null)?.ToList() ?? []; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Terminates the thread. Any items still in queue will be removed
|
||||
/// </summary>
|
||||
@@ -233,9 +214,9 @@ namespace Duplicati.Server
|
||||
return res;
|
||||
}
|
||||
|
||||
private void OnCompleted(WorkerThread<Runner.IRunnerData> worker, Runner.IRunnerData task)
|
||||
private Task OnCompleted(Runner.IRunnerData task)
|
||||
{
|
||||
Tuple<ISchedule, DateTime, DateTime> t = null;
|
||||
Tuple<ISchedule, DateTime, DateTime>? t = null;
|
||||
lock (m_lock)
|
||||
{
|
||||
if (task != null && m_updateTasks.TryGetValue(task, out t))
|
||||
@@ -246,26 +227,28 @@ namespace Duplicati.Server
|
||||
{
|
||||
t.Item1.Time = t.Item2;
|
||||
t.Item1.LastRun = t.Item3;
|
||||
FIXMEGlobal.DataConnection.AddOrUpdateSchedule(t.Item1);
|
||||
m_dataConnection.AddOrUpdateSchedule(t.Item1);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnStartingWork(WorkerThread<Runner.IRunnerData> worker, Runner.IRunnerData task)
|
||||
private Task OnStartingWork(Runner.IRunnerData task)
|
||||
{
|
||||
if (task is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
|
||||
lock (m_lock)
|
||||
{
|
||||
if (m_updateTasks.TryGetValue(task, out Tuple<ISchedule, DateTime, DateTime> scheduleInfo))
|
||||
if (m_updateTasks.TryGetValue(task, out var scheduleInfo))
|
||||
{
|
||||
// Item2 is the scheduled start time (Time in the Schedule table).
|
||||
// Item3 is the actual start time (LastRun in the Schedule table).
|
||||
m_updateTasks[task] = Tuple.Create(scheduleInfo.Item1, scheduleInfo.Item2, DateTime.UtcNow);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -280,8 +263,8 @@ namespace Duplicati.Server
|
||||
// to avoid frequent db lookups
|
||||
|
||||
//Determine schedule list
|
||||
var timeZoneInfo = FIXMEGlobal.DataConnection.ApplicationSettings.Timezone;
|
||||
var lst = FIXMEGlobal.DataConnection.Schedules;
|
||||
var timeZoneInfo = m_dataConnection.ApplicationSettings.Timezone;
|
||||
var lst = m_dataConnection.Schedules;
|
||||
foreach (var sc in lst)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(sc.Repeat))
|
||||
@@ -315,7 +298,7 @@ namespace Duplicati.Server
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FIXMEGlobal.DataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date",
|
||||
m_dataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date",
|
||||
ex);
|
||||
}
|
||||
|
||||
@@ -324,37 +307,37 @@ namespace Duplicati.Server
|
||||
{
|
||||
var jobsToRun = new List<Server.Runner.IRunnerData>();
|
||||
//TODO: Cache this to avoid frequent lookups
|
||||
foreach (var id in FIXMEGlobal.DataConnection.GetBackupIDsForTags(sc.Tags).Distinct()
|
||||
foreach (var id in m_dataConnection.GetBackupIDsForTags(sc.Tags).Distinct()
|
||||
.Select(x => x.ToString()))
|
||||
{
|
||||
//See if it is already queued
|
||||
var tmplst = from n in m_worker.CurrentTasks
|
||||
where n.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup
|
||||
select n.Backup;
|
||||
var tastTemp = m_worker.CurrentTask;
|
||||
var tmplst = from n in m_queueRunnerService.GetCurrentTasks()
|
||||
where n.Operation == Serialization.DuplicatiOperation.Backup
|
||||
select n.BackupID;
|
||||
var tastTemp = m_queueRunnerService.GetCurrentTask();
|
||||
if (tastTemp != null && tastTemp.Operation ==
|
||||
Duplicati.Server.Serialization.DuplicatiOperation.Backup)
|
||||
tmplst = tmplst.Union(new[] { tastTemp.Backup });
|
||||
Serialization.DuplicatiOperation.Backup)
|
||||
tmplst = tmplst.Union(new[] { tastTemp.BackupID });
|
||||
|
||||
//If it is not already in queue, put it there
|
||||
if (!tmplst.Any(x => x.ID == id))
|
||||
if (!tmplst.Any(x => x == id))
|
||||
{
|
||||
var entry = FIXMEGlobal.DataConnection.GetBackup(id);
|
||||
var entry = m_dataConnection.GetBackup(id);
|
||||
if (entry != null)
|
||||
{
|
||||
Dictionary<string, string> options = Duplicati.Server.Runner.GetCommonOptions();
|
||||
Duplicati.Server.Runner.ApplyOptions(entry, options);
|
||||
if ((new Duplicati.Library.Main.Options(options)).DisableOnBattery &&
|
||||
(Duplicati.Library.Utility.Power.PowerSupply.GetSource() ==
|
||||
Duplicati.Library.Utility.Power.PowerSupply.Source.Battery))
|
||||
var options = Server.Runner.GetCommonOptions(m_dataConnection);
|
||||
Server.Runner.ApplyOptions(m_dataConnection, entry, options);
|
||||
if (new Library.Main.Options(options).DisableOnBattery &&
|
||||
(Library.Utility.Power.PowerSupply.GetSource() ==
|
||||
Library.Utility.Power.PowerSupply.Source.Battery))
|
||||
{
|
||||
Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG,
|
||||
Library.Logging.Log.WriteInformationMessage(LOGTAG,
|
||||
"BackupDisabledOnBattery",
|
||||
"Scheduled backup disabled while on battery power.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Dictionary<string, string> taskOptions = null;
|
||||
Dictionary<string, string?>? taskOptions = null;
|
||||
try
|
||||
{
|
||||
var nextRun = GetNextValidTime(start,
|
||||
@@ -362,15 +345,17 @@ namespace Duplicati.Server
|
||||
Math.Max(DateTime.UtcNow.AddSeconds(1).Ticks, start.AddSeconds(1).Ticks),
|
||||
DateTimeKind.Utc), sc.Repeat, sc.AllowedDays, timeZoneInfo);
|
||||
|
||||
taskOptions = new Dictionary<string, string>()
|
||||
taskOptions = new Dictionary<string, string?>()
|
||||
{ { "next-scheduled-run", Utility.SerializeDateTime(nextRun.ToUniversalTime()) } };
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
jobsToRun.Add(Server.Runner.CreateTask(
|
||||
Serialization.DuplicatiOperation.Backup, entry, taskOptions));
|
||||
var job = Server.Runner.CreateTask(Serialization.DuplicatiOperation.Backup, entry, taskOptions);
|
||||
job.OnStarting = () => OnStartingWork(job);
|
||||
job.OnFinished = (_) => OnCompleted(job);
|
||||
jobsToRun.Add(job);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -386,12 +371,12 @@ namespace Duplicati.Server
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FIXMEGlobal.DataConnection.LogError(sc.ID.ToString(),
|
||||
m_dataConnection.LogError(sc.ID.ToString(),
|
||||
"Scheduler failed to find next date", ex);
|
||||
continue;
|
||||
}
|
||||
|
||||
Server.Runner.IRunnerData lastJob = jobsToRun.LastOrDefault();
|
||||
var lastJob = jobsToRun.LastOrDefault();
|
||||
if (lastJob != null)
|
||||
{
|
||||
lock (m_lock)
|
||||
@@ -403,7 +388,7 @@ namespace Duplicati.Server
|
||||
}
|
||||
|
||||
foreach (var job in jobsToRun)
|
||||
m_worker.AddTask(job);
|
||||
m_queueRunnerService.AddTask(job);
|
||||
|
||||
if (start < DateTime.UtcNow)
|
||||
{
|
||||
@@ -428,11 +413,6 @@ namespace Duplicati.Server
|
||||
foreach (var c in (from n in scheduled where !existing.ContainsKey(n.Key) select n.Key).ToArray())
|
||||
scheduled.Remove(c);
|
||||
|
||||
//Raise event if needed
|
||||
// TODO: This triggers a new data event and a reconnect with long-poll
|
||||
if (NewSchedule != null)
|
||||
NewSchedule(this, null);
|
||||
|
||||
int waittime = 0;
|
||||
|
||||
//Figure out a sensible amount of time to sleep the thread
|
||||
|
||||
@@ -18,9 +18,8 @@
|
||||
// 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.Linq;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Server.Serialization.Interface;
|
||||
|
||||
namespace Duplicati.Server.Serializable
|
||||
@@ -38,7 +37,7 @@ namespace Duplicati.Server.Serializable
|
||||
/// <summary>
|
||||
/// Constructor for backend interface
|
||||
/// </summary>
|
||||
public DynamicModule(Duplicati.Library.Interface.IBackend backend)
|
||||
public DynamicModule(Library.Interface.IBackend backend)
|
||||
{
|
||||
this.Key = backend.ProtocolKey;
|
||||
this.Description = backend.Description;
|
||||
@@ -49,7 +48,7 @@ namespace Duplicati.Server.Serializable
|
||||
/// <summary>
|
||||
/// Constructor for compression module interface
|
||||
/// </summary>
|
||||
public DynamicModule(Duplicati.Library.Interface.ICompression module)
|
||||
public DynamicModule(Library.Interface.ICompression module)
|
||||
{
|
||||
this.Key = module.FilenameExtension;
|
||||
this.Description = module.Description;
|
||||
@@ -60,7 +59,7 @@ namespace Duplicati.Server.Serializable
|
||||
/// <summary>
|
||||
/// Constructor for encryption module interface
|
||||
/// </summary>
|
||||
public DynamicModule(Duplicati.Library.Interface.IEncryption module)
|
||||
public DynamicModule(Library.Interface.IEncryption module)
|
||||
{
|
||||
this.Key = module.FilenameExtension;
|
||||
this.Description = module.Description;
|
||||
@@ -71,7 +70,7 @@ namespace Duplicati.Server.Serializable
|
||||
/// <summary>
|
||||
/// Constructor for generic module interface
|
||||
/// </summary>
|
||||
public DynamicModule(Duplicati.Library.Interface.IGenericModule module)
|
||||
public DynamicModule(Library.Interface.IGenericModule module)
|
||||
{
|
||||
this.Key = module.Key;
|
||||
this.Description = module.Description;
|
||||
@@ -82,7 +81,7 @@ namespace Duplicati.Server.Serializable
|
||||
/// <summary>
|
||||
/// Constructor for webmodule interface
|
||||
/// </summary>
|
||||
public DynamicModule(Duplicati.Library.Interface.IWebModule module)
|
||||
public DynamicModule(Library.Interface.IWebModule module)
|
||||
{
|
||||
this.Key = module.Key;
|
||||
this.Description = module.Description;
|
||||
@@ -93,7 +92,7 @@ namespace Duplicati.Server.Serializable
|
||||
/// <summary>
|
||||
/// Constructor for sercretprovider interface
|
||||
/// </summary>
|
||||
public DynamicModule(Duplicati.Library.Interface.ISecretProvider module)
|
||||
public DynamicModule(Library.Interface.ISecretProvider module)
|
||||
{
|
||||
this.Key = module.Key;
|
||||
this.Description = module.Description;
|
||||
@@ -116,13 +115,13 @@ namespace Duplicati.Server.Serializable
|
||||
/// <summary>
|
||||
/// The options supported by the module
|
||||
/// </summary>
|
||||
public Duplicati.Library.Interface.ICommandLineArgument[] Options { get; private set; }
|
||||
public Library.Interface.ICommandLineArgument[] Options { get; private set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all supported options
|
||||
/// </summary>
|
||||
public static Duplicati.Library.Interface.ICommandLineArgument[] Options
|
||||
public static Library.Interface.ICommandLineArgument[] Options
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -242,21 +241,5 @@ namespace Duplicati.Server.Serializable
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The filters that are applied to all backups
|
||||
/// </summary>
|
||||
public static IFilter[] Filters
|
||||
{
|
||||
get { return FIXMEGlobal.DataConnection.Filters; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The settings applied to all backups by default
|
||||
/// </summary>
|
||||
public static ISetting[] Settings
|
||||
{
|
||||
get { return FIXMEGlobal.DataConnection.Settings; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Duplicati.StreamUtil" Version="1.0.3" />
|
||||
<PackageReference Include="Duplicati.StreamUtil" Version="1.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageReference Include="System.Management" Version="8.0.0" />
|
||||
|
||||
@@ -44,7 +44,7 @@ public static class AuthIdOptionsHelper
|
||||
/// <returns>The authentication options</returns>
|
||||
public static CommandLineArgument[] GetOptions(string tokenurl, string? prefix = null) =>
|
||||
[
|
||||
new CommandLineArgument($"{prefix}{AuthIdOption}", CommandLineArgument.ArgumentType.String, Strings.AuthIdSettingsHelper.AuthidShort, Strings.AuthIdSettingsHelper.AuthidLong(tokenurl))
|
||||
new CommandLineArgument($"{prefix}{AuthIdOption}", CommandLineArgument.ArgumentType.Password, Strings.AuthIdSettingsHelper.AuthidShort, Strings.AuthIdSettingsHelper.AuthidLong(tokenurl))
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -37,6 +37,11 @@ namespace Duplicati.Library.Utility
|
||||
/// </summary>
|
||||
protected Stream m_basestream;
|
||||
|
||||
/// <summary>
|
||||
/// The base stream that is wrapped
|
||||
/// </summary>
|
||||
public Stream BaseStream => m_basestream;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="OverrideableStream"/> instance
|
||||
/// </summary>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -1397,7 +1398,8 @@ namespace Duplicati.Library.Utility
|
||||
/// <returns>The wrapped commandline element.</returns>
|
||||
/// <param name="arg">The argument to wrap.</param>
|
||||
/// <param name="allowEnvExpansion">A flag indicating if environment variables are allowed to be expanded</param>
|
||||
public static string WrapCommandLineElement(string arg, bool allowEnvExpansion)
|
||||
[return: NotNullIfNotNull("arg")]
|
||||
public static string? WrapCommandLineElement(string? arg, bool allowEnvExpansion)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(arg))
|
||||
return arg;
|
||||
@@ -1847,5 +1849,90 @@ namespace Duplicati.Library.Utility
|
||||
: Timeout.Infinite
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The types of streams that are considered basic (i.e. not wrapped)
|
||||
/// </summary>
|
||||
private static readonly IReadOnlySet<Type> _basicStreamTypes = new HashSet<Type>
|
||||
{
|
||||
typeof(FileStream),
|
||||
typeof(MemoryStream),
|
||||
typeof(NetworkStream),
|
||||
typeof(BufferedStream),
|
||||
typeof(System.IO.Compression.DeflateStream),
|
||||
typeof(System.IO.Compression.GZipStream),
|
||||
typeof(System.IO.Compression.ZLibStream)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Unwraps a stream from layers of wrapping streams
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to unwrap</param>
|
||||
/// <returns>The unwrapped stream</returns>
|
||||
public static Stream UnwrapThrottledStream(this Stream stream)
|
||||
{
|
||||
if (stream == null)
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
|
||||
var previousStream = stream;
|
||||
|
||||
do
|
||||
{
|
||||
previousStream = stream;
|
||||
|
||||
while (stream is WrappingStream wrappingStream)
|
||||
stream = wrappingStream.BaseStream;
|
||||
while (stream is OverrideableStream overrideableStream)
|
||||
stream = overrideableStream.BaseStream;
|
||||
|
||||
} while (stream != previousStream);
|
||||
|
||||
#if DEBUG
|
||||
if (!_basicStreamTypes.Contains(stream.GetType()))
|
||||
throw new InvalidOperationException($"The unwrapped stream is not a basic stream, but a {stream.GetType()}");
|
||||
#endif
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the hash of a throttled stream and returns the stream to read
|
||||
/// </summary>
|
||||
/// <param name="stream">The source stream
|
||||
/// <param name="hashalgorithm">The hash algorithm to use</param>
|
||||
/// <param name="cancelToken">The cancellation token to observe</param>
|
||||
/// <returns>A tuple with the stream, the hash and a temporary file if used</returns>
|
||||
public static async Task<(Stream content, string hash, TempFile? tmpfile)> CalculateThrottledStreamHash(Stream stream, string hashalgorithm, CancellationToken cancelToken)
|
||||
{
|
||||
TempFile? tmp = null;
|
||||
string contentHash;
|
||||
var measure = stream.UnwrapThrottledStream();
|
||||
if (measure.CanSeek)
|
||||
{
|
||||
var p = measure.Position;
|
||||
|
||||
// Compute the hash
|
||||
using (var hashalg = HashFactory.CreateHasher(hashalgorithm))
|
||||
contentHash = ByteArrayAsHexString(hashalg.ComputeHash(measure));
|
||||
|
||||
measure.Position = p;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No seeking possible, use a temp file
|
||||
tmp = new TempFile();
|
||||
await using (var sr = File.OpenWrite(tmp))
|
||||
using (var hasher = HashFactory.CreateHasher(hashalgorithm))
|
||||
await using (var hc = new HashCalculatingStream(measure, hasher))
|
||||
{
|
||||
await CopyStreamAsync(hc, sr, cancelToken).ConfigureAwait(false);
|
||||
contentHash = hc.GetFinalHashString();
|
||||
}
|
||||
|
||||
stream = File.OpenRead(tmp);
|
||||
}
|
||||
|
||||
return (stream, contentHash, tmp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,401 +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;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
|
||||
// TODO: Delete this class.
|
||||
// It is essentially a queue that is processed by a worker thread, and can be implemented using a BlockingCollection or similar.
|
||||
|
||||
namespace Duplicati.Library.Utility
|
||||
{
|
||||
/// <summary>
|
||||
/// Class to encapsulate a thread that runs a list of queued operations
|
||||
/// </summary>
|
||||
/// <typeparam name="Tx">The type to operate on</typeparam>
|
||||
public class WorkerThread<Tx> where Tx : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Locking object for shared data
|
||||
/// </summary>
|
||||
private readonly object m_lock = new object();
|
||||
/// <summary>
|
||||
/// The wait event
|
||||
/// </summary>
|
||||
private readonly AutoResetEvent m_event;
|
||||
/// <summary>
|
||||
/// The internal list of tasks to perform
|
||||
/// </summary>
|
||||
private Queue<Tx> m_tasks;
|
||||
/// <summary>
|
||||
/// A flag used to terminate the thread
|
||||
/// </summary>
|
||||
private volatile bool m_terminate;
|
||||
/// <summary>
|
||||
/// The coordinating thread
|
||||
/// </summary>
|
||||
private Thread m_thread;
|
||||
|
||||
/// <summary>
|
||||
/// A value indicating if the coordinating thread is running
|
||||
/// </summary>
|
||||
private volatile bool m_active;
|
||||
|
||||
/// <summary>
|
||||
/// The current task being processed
|
||||
/// </summary>
|
||||
private Tx m_currentTask;
|
||||
/// <summary>
|
||||
/// A callback that performs the actual work on the item
|
||||
/// </summary>
|
||||
private readonly Action<Tx> m_delegate;
|
||||
|
||||
/// <summary>
|
||||
/// An event that is raised when the runner state changes
|
||||
/// </summary>
|
||||
public event Action<WorkerThread<Tx>, RunState> WorkerStateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Event that occurs when a new operation is being processed
|
||||
/// </summary>
|
||||
public event Action<WorkerThread<Tx>, Tx> StartingWork;
|
||||
/// <summary>
|
||||
/// Event that occurs when an operation has completed
|
||||
/// </summary>
|
||||
public event Action<WorkerThread<Tx>, Tx> CompletedWork;
|
||||
/// <summary>
|
||||
/// Event that occurs when an error is detected
|
||||
/// </summary>
|
||||
public event Action<WorkerThread<Tx>, Tx, Exception> OnError;
|
||||
/// <summary>
|
||||
/// An event that occurs when a new task is added to the queue or an existing one is removed
|
||||
/// </summary>
|
||||
public event Action<WorkerThread<Tx>> WorkQueueChanged;
|
||||
|
||||
/// <summary>
|
||||
/// The internal state
|
||||
/// </summary>
|
||||
private volatile RunState m_state;
|
||||
|
||||
/// <summary>
|
||||
/// The states the scheduler can take
|
||||
/// </summary>
|
||||
public enum RunState
|
||||
{
|
||||
/// <summary>
|
||||
/// The program is running as normal
|
||||
/// </summary>
|
||||
Run,
|
||||
/// <summary>
|
||||
/// The program is suspended by the user
|
||||
/// </summary>
|
||||
Paused
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new WorkerThread
|
||||
/// </summary>
|
||||
/// <param name="item">The callback that performs the work</param>
|
||||
public WorkerThread(Action<Tx> item, bool paused)
|
||||
{
|
||||
m_delegate = item;
|
||||
m_event = new AutoResetEvent(paused);
|
||||
m_terminate = false;
|
||||
m_tasks = new Queue<Tx>();
|
||||
m_state = paused ? WorkerThread<Tx>.RunState.Paused : WorkerThread<Tx>.RunState.Run;
|
||||
|
||||
m_thread = new Thread(new ThreadStart(Runner));
|
||||
m_thread.IsBackground = true;
|
||||
m_thread.Name = "WorkerThread<" + typeof(Tx).Name + ">";
|
||||
m_thread.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a copy of the current queue
|
||||
/// </summary>
|
||||
public List<Tx> CurrentTasks
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (m_lock)
|
||||
return new List<Tx>(m_tasks);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating if the worker is running
|
||||
/// </summary>
|
||||
public bool Active
|
||||
{
|
||||
get { return m_active; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a task to the queue
|
||||
/// </summary>
|
||||
/// <param name="task">The task to add</param>
|
||||
public void AddTask(Tx task)
|
||||
{
|
||||
lock (m_lock)
|
||||
{
|
||||
m_tasks.Enqueue(task);
|
||||
m_event.Set();
|
||||
}
|
||||
|
||||
if (WorkQueueChanged != null)
|
||||
WorkQueueChanged(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An overloaded AddTask method that allows a task to skip to the front of a queue
|
||||
/// It does this by creating a new queue, adding the new task first, and then adding
|
||||
/// all the old tasks to the new queue. It's cleaner to use a linked list,
|
||||
/// but the performance difference is negligible on such a small queue.
|
||||
/// </summary>
|
||||
/// <param name="task">Task.</param>
|
||||
/// <param name="skipQueue">If set to <c>true</c> skip queue.</param>
|
||||
public void AddTask(Tx task, bool skipQueue)
|
||||
{
|
||||
if (!skipQueue)
|
||||
{
|
||||
// Fall back to default AddTask method
|
||||
AddTask(task);
|
||||
return;
|
||||
}
|
||||
|
||||
lock (m_lock)
|
||||
{
|
||||
Queue<Tx> newQueue = new Queue<Tx>();
|
||||
newQueue.Enqueue(task);
|
||||
while (m_tasks.Count > 0)
|
||||
{
|
||||
Tx n = m_tasks.Dequeue();
|
||||
newQueue.Enqueue(n);
|
||||
}
|
||||
m_tasks = newQueue;
|
||||
m_event.Set();
|
||||
}
|
||||
|
||||
if (WorkQueueChanged != null)
|
||||
WorkQueueChanged(this);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Removes a task from the queue, does not remove the task if it is currently running
|
||||
/// </summary>
|
||||
/// <param name="task">The task to remove</param>
|
||||
public void RemoveTask(Tx task)
|
||||
{
|
||||
lock (m_lock)
|
||||
{
|
||||
Queue<Tx> tmp = new Queue<Tx>();
|
||||
while (m_tasks.Count > 0)
|
||||
{
|
||||
Tx n = m_tasks.Dequeue();
|
||||
if (n != task)
|
||||
tmp.Enqueue(n);
|
||||
}
|
||||
|
||||
m_tasks = tmp;
|
||||
}
|
||||
|
||||
if (WorkQueueChanged != null)
|
||||
WorkQueueChanged(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will clear the pending queue
|
||||
/// <param name="abortThread">True if the current running thread should be aborted</param>
|
||||
/// </summary>
|
||||
public void ClearQueue(bool abortThread)
|
||||
{
|
||||
lock (m_lock)
|
||||
m_tasks.Clear();
|
||||
|
||||
if (abortThread)
|
||||
{
|
||||
try
|
||||
{
|
||||
m_thread.Interrupt();
|
||||
m_thread.Join(500);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
m_thread = new Thread(new ThreadStart(Runner));
|
||||
m_thread.Start();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a reference to the currently executing task.
|
||||
/// BEWARE: This is not protected by a mutex, DO NOT MODIFY IT!!!!
|
||||
/// </summary>
|
||||
public Tx CurrentTask
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_currentTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Terminates the thread. Any items still in queue will be removed
|
||||
/// </summary>
|
||||
/// <param name="wait">True if the call should block until the thread has exited, false otherwise</param>
|
||||
public void Terminate(bool wait)
|
||||
{
|
||||
m_terminate = true;
|
||||
m_event.Set();
|
||||
|
||||
if (wait)
|
||||
m_thread.Join();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is the thread entry point
|
||||
/// </summary>
|
||||
private void Runner()
|
||||
{
|
||||
while (!m_terminate)
|
||||
{
|
||||
m_currentTask = null;
|
||||
|
||||
lock (m_lock)
|
||||
if (m_state == WorkerThread<Tx>.RunState.Run && m_tasks.Count > 0)
|
||||
m_currentTask = m_tasks.Dequeue();
|
||||
|
||||
if (m_currentTask == null && !m_terminate)
|
||||
{
|
||||
if (m_state == WorkerThread<Tx>.RunState.Run)
|
||||
m_event.WaitOne(); //Sleep until signaled
|
||||
else
|
||||
{
|
||||
if (WorkerStateChanged != null)
|
||||
WorkerStateChanged(this, m_state);
|
||||
|
||||
//Sleep for brief periods, until signaled
|
||||
while (!m_terminate && m_state != WorkerThread<Tx>.RunState.Run)
|
||||
m_event.WaitOne(1000 * 60 * 5, false);
|
||||
|
||||
//If we were not terminated, we are now ready to run
|
||||
if (!m_terminate)
|
||||
{
|
||||
m_state = WorkerThread<Tx>.RunState.Run;
|
||||
if (WorkerStateChanged != null)
|
||||
WorkerStateChanged(this, m_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_terminate)
|
||||
return;
|
||||
|
||||
if (m_currentTask == null && m_state == WorkerThread<Tx>.RunState.Run)
|
||||
lock (m_lock)
|
||||
if (m_tasks.Count > 0)
|
||||
m_currentTask = m_tasks.Dequeue();
|
||||
|
||||
if (m_currentTask == null)
|
||||
continue;
|
||||
|
||||
if (StartingWork != null)
|
||||
StartingWork(this, m_currentTask);
|
||||
|
||||
try
|
||||
{
|
||||
m_active = true;
|
||||
m_delegate(m_currentTask);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//TODO: Here where Thread.ResetAbort() was called we shall integrate the CancelationToken pattern.
|
||||
if (OnError != null)
|
||||
try { OnError(this, m_currentTask, ex); }
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
//TODO: Here where Thread.ResetAbort() was called we shall integrate the CancelationToken pattern.
|
||||
m_active = false;
|
||||
}
|
||||
|
||||
var task = m_currentTask;
|
||||
m_currentTask = null;
|
||||
|
||||
if (CompletedWork != null)
|
||||
try { CompletedWork(this, task); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { OnError(this, task, ex); }
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current run state
|
||||
/// </summary>
|
||||
public RunState State { get { return m_state; } }
|
||||
|
||||
/// <summary>
|
||||
/// Instructs Duplicati to run scheduled backups
|
||||
/// </summary>
|
||||
public void Resume()
|
||||
{
|
||||
m_state = RunState.Run;
|
||||
m_event.Set();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instructs Duplicati to pause scheduled backups
|
||||
/// </summary>
|
||||
public void Pause()
|
||||
{
|
||||
m_state = RunState.Paused;
|
||||
m_event.Set();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits the specified number of milliseconds for the thread to terminate
|
||||
/// </summary>
|
||||
/// <param name="millisecondTimeout">The number of milliseconds to wait</param>
|
||||
/// <returns>True if the thread is terminated, false if a timeout occured</returns>
|
||||
public bool Join(int millisecondTimeout)
|
||||
{
|
||||
if (m_thread != null)
|
||||
return m_thread.Join(millisecondTimeout);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,16 +17,71 @@
|
||||
// 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.
|
||||
|
||||
namespace Duplicati.Server.Serialization.Interface
|
||||
{
|
||||
public interface IQueuedTask
|
||||
{
|
||||
long TaskID { get; }
|
||||
string BackupID { get; }
|
||||
Duplicati.Server.Serialization.DuplicatiOperation Operation { get; }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Duplicati.Server.Serialization.Interface;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a queued task.
|
||||
/// </summary>
|
||||
public interface IQueuedTask
|
||||
{
|
||||
/// <summary>
|
||||
/// The task ID.
|
||||
/// </summary>
|
||||
long TaskID { get; }
|
||||
/// <summary>
|
||||
/// The backup ID, if applicable.
|
||||
/// </summary>
|
||||
string? BackupID { get; }
|
||||
/// <summary>
|
||||
/// The operation type of the task.
|
||||
/// </summary>
|
||||
DuplicatiOperation Operation { get; }
|
||||
/// <summary>
|
||||
/// Callback to be executed when the task is starting.
|
||||
/// </summary>
|
||||
Func<Task>? OnStarting { get; set; }
|
||||
/// <summary>
|
||||
/// Callback to be executed when the task is finished.
|
||||
/// If the task completes successfully, the exception parameter will be null.
|
||||
/// </summary>
|
||||
Func<Exception?, Task>? OnFinished { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Updates the throttle speeds for the task.
|
||||
/// </summary>
|
||||
/// <param name="uploadSpeed">The upload speed to set.</param>
|
||||
/// <param name="downloadSpeed">The download speed to set.</param>
|
||||
void UpdateThrottleSpeeds(string? uploadSpeed, string? downloadSpeed);
|
||||
/// <summary>
|
||||
/// The time when the task was starting to execute.
|
||||
/// </summary>
|
||||
DateTime? TaskStarted { get; set; }
|
||||
/// <summary>
|
||||
/// The time when the task was finished executing.
|
||||
/// </summary>
|
||||
DateTime? TaskFinished { get; set; }
|
||||
/// <summary>
|
||||
/// Stops the task.
|
||||
/// </summary>
|
||||
void Stop();
|
||||
/// <summary>
|
||||
/// Aborts the task.
|
||||
/// </summary>
|
||||
void Abort();
|
||||
/// <summary>
|
||||
/// Pauses the task.
|
||||
/// </summary>
|
||||
/// <param name="alsoTransfers">If true, also pauses transfers.</param>
|
||||
void Pause(bool alsoTransfers);
|
||||
/// <summary>
|
||||
/// Resumes the task.
|
||||
/// </summary>
|
||||
void Resume();
|
||||
}
|
||||
|
||||
+135
-247
@@ -32,11 +32,11 @@ using Duplicati.Library.Interface;
|
||||
using Duplicati.Library.Logging;
|
||||
using Duplicati.Library.Main;
|
||||
using Duplicati.Library.Main.Database;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.WebserverCore;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
using Duplicati.WebserverCore.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Duplicati.Server
|
||||
@@ -84,61 +84,21 @@ namespace Duplicati.Server
|
||||
/// </summary>
|
||||
private static readonly string ENV_NAME_PREFIX = AutoUpdateSettings.AppName.ToUpperInvariant();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the folder where Duplicati data is stored
|
||||
/// </summary>
|
||||
public static string DataFolder { get => FIXMEGlobal.DataFolder; private set => FIXMEGlobal.DataFolder = value; }
|
||||
|
||||
/// <summary>
|
||||
/// The single instance
|
||||
/// </summary>
|
||||
public static SingleInstance ApplicationInstance = null;
|
||||
|
||||
/// <summary>
|
||||
/// This is the only access to the database
|
||||
/// </summary>
|
||||
public static Database.Connection DataConnection { get => FIXMEGlobal.DataConnection; set => FIXMEGlobal.DataConnection = value; }
|
||||
|
||||
/// <summary>
|
||||
/// This is the lock to be used before manipulating the shared resources
|
||||
/// </summary>
|
||||
public static object MainLock { get => FIXMEGlobal.MainLock; }
|
||||
|
||||
/// <summary>
|
||||
/// This is the scheduling thread
|
||||
/// </summary>
|
||||
public static IScheduler Scheduler { get => FIXMEGlobal.Scheduler; }
|
||||
|
||||
/// <summary>
|
||||
/// List of completed task results
|
||||
/// </summary>
|
||||
public static List<KeyValuePair<long, Exception>> TaskResultCache { get => FIXMEGlobal.TaskResultCache; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of completed task results to keep in memory
|
||||
/// </summary>
|
||||
private static readonly int MAX_TASK_RESULT_CACHE_SIZE = 100;
|
||||
|
||||
/// <summary>
|
||||
/// The thread running the ping-pong handler
|
||||
/// </summary>
|
||||
private static System.Threading.Thread PingPongThread;
|
||||
|
||||
/// <summary>
|
||||
/// The path to the file that contains the current database
|
||||
/// </summary>
|
||||
private static string DatabasePath;
|
||||
private static Thread PingPongThread;
|
||||
|
||||
/// <summary>
|
||||
/// The controller interface for pause/resume and throttle options
|
||||
/// </summary>
|
||||
public static LiveControls LiveControl { get => DuplicatiWebserver.Provider.GetRequiredService<LiveControls>(); }
|
||||
|
||||
/// <summary>
|
||||
/// The application exit event
|
||||
/// </summary>
|
||||
public static System.Threading.ManualResetEvent ApplicationExitEvent { get => FIXMEGlobal.ApplicationExitEvent; set => FIXMEGlobal.ApplicationExitEvent = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Duplicati webserver instance
|
||||
/// </summary>
|
||||
@@ -152,68 +112,38 @@ namespace Duplicati.Server
|
||||
DuplicatiWebserver.Stop().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The update poll thread.
|
||||
/// </summary>
|
||||
public static UpdatePollThread UpdatePoller => FIXMEGlobal.UpdatePoller;
|
||||
|
||||
/// <summary>
|
||||
/// An event that is set once the server is ready to respond to requests
|
||||
/// </summary>
|
||||
public static readonly System.Threading.ManualResetEvent ServerStartedEvent = new System.Threading.ManualResetEvent(false);
|
||||
public static readonly ManualResetEvent ServerStartedEvent = new ManualResetEvent(false);
|
||||
|
||||
/// <summary>
|
||||
/// The status event signaler, used to control long polling of status updates
|
||||
/// Timer for purging temp files and log data
|
||||
/// </summary>
|
||||
public static EventPollNotify StatusEventNotifyer => FIXMEGlobal.Provider.GetRequiredService<EventPollNotify>();
|
||||
|
||||
/// <summary>
|
||||
/// A delegate method for creating a copy of the current progress state
|
||||
/// </summary>
|
||||
public static Func<Duplicati.Server.Serialization.Interface.IProgressEventData> GenerateProgressState { get => FIXMEGlobal.GenerateProgressState; set => FIXMEGlobal.GenerateProgressState = value; }
|
||||
|
||||
/// <summary>
|
||||
/// The log redirect handler
|
||||
/// </summary>
|
||||
public static LogWriteHandler LogHandler { get => FIXMEGlobal.LogHandler; }
|
||||
|
||||
private static System.Threading.Timer PurgeTempFilesTimer = null;
|
||||
|
||||
public static int ServerPort
|
||||
{
|
||||
get
|
||||
{
|
||||
return DuplicatiWebserver.Port;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsFirstRun
|
||||
{
|
||||
get { return DataConnection.ApplicationSettings.IsFirstRun; }
|
||||
set { DataConnection.ApplicationSettings.IsFirstRun = value; }
|
||||
}
|
||||
|
||||
public static bool ServerPortChanged
|
||||
{
|
||||
get { return DataConnection.ApplicationSettings.ServerPortChanged; }
|
||||
set { DataConnection.ApplicationSettings.ServerPortChanged = value; }
|
||||
}
|
||||
|
||||
static Program()
|
||||
{
|
||||
FIXMEGlobal.StartOrStopUsageReporter = Program.StartOrStopUsageReporter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The main entry point for the application.
|
||||
/// </summary>
|
||||
/// <param name="_args"> The command line arguments</param>
|
||||
[STAThread]
|
||||
public static int Main(string[] _args)
|
||||
=> Main(null, _args);
|
||||
|
||||
/// <summary>
|
||||
/// The main entry point for the application.
|
||||
/// </summary>
|
||||
/// <param name="applicationSettings"> The application settings</param>
|
||||
/// <param name="_args"> The command line arguments</param>
|
||||
[STAThread]
|
||||
public static int Main(IApplicationSettings applicationSettings, string[] _args)
|
||||
{
|
||||
PreloadSettingsLoader.ConfigurePreloadSettings(ref _args, PackageHelper.NamedExecutable.Server, out var preloadDbSettings);
|
||||
|
||||
applicationSettings ??= new ApplicationSettings();
|
||||
|
||||
//If this executable is invoked directly, write to console, otherwise throw exceptions
|
||||
var writeToConsoleOnException = FIXMEGlobal.Origin == "Server";
|
||||
var writeToConsoleOnException = applicationSettings.Origin == "Server";
|
||||
|
||||
// Prepared for the future, where we might want to have a silent console mode
|
||||
var silentConsole = false;
|
||||
@@ -238,7 +168,7 @@ namespace Duplicati.Server
|
||||
SystemContextSettings.StartSession();
|
||||
|
||||
ApplyEnvironmentVariables(commandlineOptions);
|
||||
ApplySecretProvider(commandlineOptions, CancellationToken.None).Await();
|
||||
ApplySecretProvider(applicationSettings, commandlineOptions, CancellationToken.None).Await();
|
||||
|
||||
var parameterFileOption = PARAMETERS_FILE_OPTION_EXTRAS.Prepend(PARAMETERS_FILE_OPTION)
|
||||
.FirstOrDefault(x => commandlineOptions.ContainsKey(x));
|
||||
@@ -251,62 +181,70 @@ namespace Duplicati.Server
|
||||
return 100;
|
||||
}
|
||||
|
||||
ConfigureLogging(commandlineOptions);
|
||||
var logHandler = new LogWriteHandler();
|
||||
using var logScope = ConfigureLogging(logHandler, commandlineOptions);
|
||||
|
||||
// Validate after logging is configured
|
||||
CommandLineArgumentValidator.ValidateArguments(SupportedCommands, commandlineOptions, KnownDuplicateOptions, ValidationIgnoredOptions);
|
||||
|
||||
var crashed = false;
|
||||
var terminated = false;
|
||||
IQueueRunnerService queueRunner = null;
|
||||
UpdatePollThread updatePollThread = null;
|
||||
EventPollNotify eventPollNotify = null;
|
||||
ISchedulerService scheduler = null;
|
||||
try
|
||||
{
|
||||
DataConnection = GetDatabaseConnection(commandlineOptions, silentConsole);
|
||||
var connection = GetDatabaseConnection(applicationSettings, commandlineOptions, silentConsole);
|
||||
|
||||
if (!DataConnection.ApplicationSettings.FixedInvalidBackupId)
|
||||
DataConnection.FixInvalidBackupId();
|
||||
if (!connection.ApplicationSettings.FixedInvalidBackupId)
|
||||
connection.FixInvalidBackupId();
|
||||
|
||||
DataConnection.ApplicationSettings.UpgradePasswordToKBDF();
|
||||
CreateApplicationInstance(writeToConsoleOnException);
|
||||
connection.ApplicationSettings.UpgradePasswordToKBDF();
|
||||
CreateApplicationInstance(applicationSettings.DataFolder, writeToConsoleOnException);
|
||||
|
||||
StartOrStopUsageReporter();
|
||||
applicationSettings.StartOrStopUsageReporter = () => StartOrStopUsageReporter(connection);
|
||||
applicationSettings.StartOrStopUsageReporter?.Invoke();
|
||||
|
||||
AdjustApplicationSettings(commandlineOptions);
|
||||
|
||||
ApplicationExitEvent = new System.Threading.ManualResetEvent(false);
|
||||
AdjustApplicationSettings(connection, commandlineOptions);
|
||||
|
||||
UpdaterManager.OnError += obj =>
|
||||
{
|
||||
DataConnection.LogError(null, "Error in updater", obj);
|
||||
connection.LogError(null, "Error in updater", obj);
|
||||
};
|
||||
|
||||
DuplicatiWebserver = StartWebServer(commandlineOptions, DataConnection).Await();
|
||||
DuplicatiWebserver = StartWebServer(commandlineOptions, connection, logHandler, applicationSettings).Await();
|
||||
|
||||
UpdatePoller.Init(Library.Utility.Utility.ParseBoolOption(commandlineOptions, DISABLE_UPDATE_CHECK_OPTION));
|
||||
connection.SetServiceProvider(DuplicatiWebserver.Provider);
|
||||
queueRunner = DuplicatiWebserver.Provider.GetRequiredService<IQueueRunnerService>();
|
||||
updatePollThread = DuplicatiWebserver.Provider.GetRequiredService<UpdatePollThread>();
|
||||
eventPollNotify = DuplicatiWebserver.Provider.GetRequiredService<EventPollNotify>();
|
||||
scheduler = DuplicatiWebserver.Provider.GetRequiredService<ISchedulerService>();
|
||||
|
||||
SetPurgeTempFilesTimer(commandlineOptions);
|
||||
updatePollThread.Init(Library.Utility.Utility.ParseBoolOption(commandlineOptions, DISABLE_UPDATE_CHECK_OPTION));
|
||||
|
||||
LiveControl.StateChanged = LiveControl_StateChanged;
|
||||
SetPurgeTempFilesTimer(connection, commandlineOptions);
|
||||
|
||||
SetWorkerThread();
|
||||
LiveControl.StateChanged = (e) => { LiveControl_StateChanged(queueRunner, connection, eventPollNotify, e); };
|
||||
|
||||
if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, PING_PONG_KEEPALIVE_OPTION))
|
||||
{
|
||||
PingPongThread = new System.Threading.Thread(PingPongMethod) { IsBackground = true };
|
||||
PingPongThread = new Thread(() => PingPongMethod(applicationSettings)) { IsBackground = true };
|
||||
PingPongThread.Start();
|
||||
}
|
||||
|
||||
DataConnection.ReWriteAllFieldsIfEncryptionChanged();
|
||||
DataConnection.SetPreloadSettingsIfChanged(preloadDbSettings);
|
||||
EmitWarningsForConfigurationIssues(commandlineOptions);
|
||||
connection.ReWriteAllFieldsIfEncryptionChanged();
|
||||
connection.SetPreloadSettingsIfChanged(preloadDbSettings);
|
||||
EmitWarningsForConfigurationIssues(connection, applicationSettings, commandlineOptions);
|
||||
|
||||
Log.WriteInformationMessage(LOGTAG, "ServerStarted", Strings.Program.ServerStarted(DuplicatiWebserver.Interface, DuplicatiWebserver.Port));
|
||||
logMessageToConsole(Strings.Program.ServerStarted(DuplicatiWebserver.Interface, DuplicatiWebserver.Port));
|
||||
|
||||
if (FIXMEGlobal.Origin == "Server" && DataConnection.ApplicationSettings.AutogeneratedPassphrase)
|
||||
if (applicationSettings.Origin == "Server" && connection.ApplicationSettings.AutogeneratedPassphrase)
|
||||
{
|
||||
var signinToken = DuplicatiWebserver.Provider.GetRequiredService<IJWTTokenProvider>().CreateSigninToken("server-cli");
|
||||
var hostname = (DataConnection.ApplicationSettings.AllowedHostnames ?? string.Empty).Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(x => x != "*") ?? "localhost";
|
||||
var scheme = DataConnection.ApplicationSettings.UseHTTPS ? "https" : "http";
|
||||
var hostname = (connection.ApplicationSettings.AllowedHostnames ?? string.Empty).Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(x => x != "*") ?? "localhost";
|
||||
var scheme = connection.ApplicationSettings.UseHTTPS ? "https" : "http";
|
||||
|
||||
var url = $"{scheme}://{hostname}:{DuplicatiWebserver.Port}/signin.html?token={signinToken}";
|
||||
Log.WriteWarningMessage(LOGTAG, "ServerStartedSignin", null, Strings.Program.ServerStartedSignin(url));
|
||||
@@ -322,7 +260,7 @@ namespace Duplicati.Server
|
||||
}
|
||||
|
||||
terminated = true;
|
||||
ApplicationExitEvent.Set();
|
||||
applicationSettings.ApplicationExitEvent.Set();
|
||||
});
|
||||
|
||||
var stopCounter = 0;
|
||||
@@ -344,7 +282,7 @@ namespace Duplicati.Server
|
||||
};
|
||||
|
||||
ServerStartedEvent.Set();
|
||||
ApplicationExitEvent.WaitOne();
|
||||
applicationSettings.ApplicationExitEvent.WaitOne();
|
||||
}
|
||||
catch (SingleInstance.MultipleInstanceException mex)
|
||||
{
|
||||
@@ -374,19 +312,19 @@ namespace Duplicati.Server
|
||||
Log.WriteInformationMessage(LOGTAG, "ServerStopping", Strings.Program.ServerStopping);
|
||||
|
||||
var steps = new Action[] {
|
||||
() => StatusEventNotifyer.SignalNewEvent(),
|
||||
() => { if (ShutdownModernWebserver != null) ShutdownModernWebserver(); },
|
||||
() => UpdatePoller?.Terminate(),
|
||||
() => Scheduler?.Terminate(true),
|
||||
() => FIXMEGlobal.WorkThread?.Terminate(true),
|
||||
() => eventPollNotify?.SignalNewEvent(),
|
||||
() => ShutdownModernWebserver(),
|
||||
() => updatePollThread?.Terminate(),
|
||||
() => scheduler?.Terminate(true),
|
||||
() => queueRunner?.Terminate(true),
|
||||
() => ApplicationInstance?.Dispose(),
|
||||
() => PurgeTempFilesTimer?.Dispose(),
|
||||
() => Library.UsageReporter.Reporter.ShutDown(),
|
||||
() => PingPongThread?.Interrupt(),
|
||||
() =>
|
||||
{
|
||||
Library.Logging.Log.WriteInformationMessage(LOGTAG, "ServerStopped", Strings.Program.ServerStopped);
|
||||
LogHandler?.Dispose();
|
||||
Log.WriteInformationMessage(LOGTAG, "ServerStopped", Strings.Program.ServerStopped);
|
||||
logHandler?.Dispose();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -412,7 +350,7 @@ namespace Duplicati.Server
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static async Task<DuplicatiWebserver> StartWebServer(IReadOnlyDictionary<string, string> options, Connection connection)
|
||||
private static async Task<DuplicatiWebserver> StartWebServer(IReadOnlyDictionary<string, string> options, Connection connection, ILogWriteHandler logWriteHandler, IApplicationSettings applicationSettings)
|
||||
{
|
||||
var server = await WebServerLoader.TryRunServer(options, connection, async parsedOptions =>
|
||||
{
|
||||
@@ -421,7 +359,6 @@ namespace Duplicati.Server
|
||||
parsedOptions.Port,
|
||||
parsedOptions.Interface,
|
||||
parsedOptions.Certificate,
|
||||
parsedOptions.Servername,
|
||||
parsedOptions.AllowedHostnames,
|
||||
parsedOptions.DisableStaticFiles,
|
||||
parsedOptions.SPAPaths,
|
||||
@@ -429,7 +366,7 @@ namespace Duplicati.Server
|
||||
parsedOptions.PreAuthTokens
|
||||
);
|
||||
|
||||
var server = DuplicatiWebserver.CreateWebServer(mappedSettings, connection);
|
||||
var server = DuplicatiWebserver.CreateWebServer(mappedSettings, connection, logWriteHandler, applicationSettings);
|
||||
|
||||
// Start the server, but catch any configuration issues
|
||||
var task = server.Start();
|
||||
@@ -440,57 +377,13 @@ namespace Duplicati.Server
|
||||
return server;
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
FIXMEGlobal.Provider = server.Provider;
|
||||
ServerPortChanged |= server.Port != DataConnection.ApplicationSettings.LastWebserverPort;
|
||||
DataConnection.ApplicationSettings.LastWebserverPort = server.Port;
|
||||
connection.ApplicationSettings.ServerPortChanged |= server.Port != connection.ApplicationSettings.LastWebserverPort;
|
||||
connection.ApplicationSettings.LastWebserverPort = server.Port;
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
private static void SetWorkerThread()
|
||||
{
|
||||
FIXMEGlobal.WorkerThreadsManager.Spawn(x => { Runner.Run(x, true); });
|
||||
FIXMEGlobal.WorkThread.StartingWork += (worker, task) => { SignalNewEvent(null, null); };
|
||||
FIXMEGlobal.WorkThread.CompletedWork += (worker, task) => { SignalNewEvent(null, null); };
|
||||
FIXMEGlobal.WorkThread.WorkQueueChanged += (worker) => { SignalNewEvent(null, null); };
|
||||
FIXMEGlobal.Scheduler.SubScribeToNewSchedule(() => SignalNewEvent(null, null));
|
||||
FIXMEGlobal.WorkThread.OnError += (worker, task, exception) =>
|
||||
{
|
||||
DataConnection.LogError(task?.BackupID, "Error in worker", exception);
|
||||
};
|
||||
|
||||
var lastScheduleId = FIXMEGlobal.NotificationUpdateService.LastDataUpdateId;
|
||||
StatusEventNotifyer.NewEvent += (sender, e) =>
|
||||
{
|
||||
if (lastScheduleId == FIXMEGlobal.NotificationUpdateService.LastDataUpdateId) return;
|
||||
lastScheduleId = FIXMEGlobal.NotificationUpdateService.LastDataUpdateId;
|
||||
Scheduler.Reschedule();
|
||||
};
|
||||
|
||||
void RegisterTaskResult(long id, Exception ex)
|
||||
{
|
||||
lock (MainLock)
|
||||
{
|
||||
// If the new results says it crashed, we store that instead of success
|
||||
if (TaskResultCache.Count > 0 && TaskResultCache.Last().Key == id)
|
||||
{
|
||||
if (ex != null && TaskResultCache.Last().Value == null)
|
||||
TaskResultCache.RemoveAt(TaskResultCache.Count - 1);
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
TaskResultCache.Add(new KeyValuePair<long, Exception>(id, ex));
|
||||
while (TaskResultCache.Count > MAX_TASK_RESULT_CACHE_SIZE)
|
||||
TaskResultCache.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
FIXMEGlobal.WorkThread.CompletedWork += (worker, task) => { RegisterTaskResult(task.TaskID, null); };
|
||||
FIXMEGlobal.WorkThread.OnError += (worker, task, exception) => { RegisterTaskResult(task.TaskID, exception); };
|
||||
}
|
||||
|
||||
private static void SetPurgeTempFilesTimer(Dictionary<string, string> commandlineOptions)
|
||||
private static void SetPurgeTempFilesTimer(Connection connection, Dictionary<string, string> commandlineOptions)
|
||||
{
|
||||
var lastPurge = new DateTime(0);
|
||||
|
||||
@@ -505,7 +398,7 @@ namespace Duplicati.Server
|
||||
|
||||
lastPurge = DateTime.Now;
|
||||
|
||||
foreach (var e in DataConnection.GetTempFiles().Where((f) => f.Expires < DateTime.Now))
|
||||
foreach (var e in connection.GetTempFiles().Where((f) => f.Expires < DateTime.Now))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -514,16 +407,16 @@ namespace Duplicati.Server
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DataConnection.LogError(null, $"Failed to delete temp file: {e.Path}", ex);
|
||||
connection.LogError(null, $"Failed to delete temp file: {e.Path}", ex);
|
||||
}
|
||||
|
||||
DataConnection.DeleteTempFile(e.ID);
|
||||
connection.DeleteTempFile(e.ID);
|
||||
}
|
||||
|
||||
|
||||
Library.Utility.TempFile.RemoveOldApplicationTempFiles((path, ex) =>
|
||||
{
|
||||
DataConnection.LogError(null, $"Failed to delete temp file: {path}", ex);
|
||||
connection.LogError(null, $"Failed to delete temp file: {path}", ex);
|
||||
});
|
||||
|
||||
if (!commandlineOptions.TryGetValue("log-retention", out string pts))
|
||||
@@ -531,11 +424,11 @@ namespace Duplicati.Server
|
||||
pts = DEFAULT_LOG_RETENTION;
|
||||
}
|
||||
|
||||
DataConnection.PurgeLogData(Library.Utility.Timeparser.ParseTimeInterval(pts, DateTime.Now, true));
|
||||
connection.PurgeLogData(Library.Utility.Timeparser.ParseTimeInterval(pts, DateTime.Now, true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DataConnection.LogError(null, "Failed during temp file cleanup", ex);
|
||||
connection.LogError(null, "Failed during temp file cleanup", ex);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -545,37 +438,37 @@ namespace Duplicati.Server
|
||||
DEBUG_MODE ? TimeSpan.FromHours(1) : TimeSpan.FromDays(1));
|
||||
}
|
||||
|
||||
private static void AdjustApplicationSettings(Dictionary<string, string> commandlineOptions)
|
||||
private static void AdjustApplicationSettings(Connection connection, Dictionary<string, string> commandlineOptions)
|
||||
{
|
||||
// This clears the JWT config, and a new will be generated, invalidating all existing tokens
|
||||
if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, WebServerLoader.OPTION_WEBSERVICE_RESET_JWT_CONFIG))
|
||||
{
|
||||
DataConnection.ApplicationSettings.JWTConfig = null;
|
||||
connection.ApplicationSettings.JWTConfig = null;
|
||||
// Clean up stored tokens as they are now invalid
|
||||
DataConnection.ExecuteWithCommand((con) => con.ExecuteNonQuery("DELETE FROM TokenFamily"));
|
||||
connection.ExecuteWithCommand((con) => con.ExecuteNonQuery("DELETE FROM TokenFamily"));
|
||||
}
|
||||
|
||||
if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, WebServerLoader.OPTION_WEBSERVICE_ENABLE_FOREVER_TOKEN))
|
||||
DataConnection.ApplicationSettings.EnableForeverTokens();
|
||||
connection.ApplicationSettings.EnableForeverTokens();
|
||||
|
||||
if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_DISABLE_VISUAL_CAPTCHA))
|
||||
DataConnection.ApplicationSettings.DisableVisualCaptcha = Library.Utility.Utility.ParseBool(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_DISABLE_VISUAL_CAPTCHA], true);
|
||||
connection.ApplicationSettings.DisableVisualCaptcha = Library.Utility.Utility.ParseBool(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_DISABLE_VISUAL_CAPTCHA], true);
|
||||
|
||||
if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_DISABLE_SIGNIN_TOKENS))
|
||||
DataConnection.ApplicationSettings.DisableSigninTokens = Library.Utility.Utility.ParseBool(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_DISABLE_SIGNIN_TOKENS], true);
|
||||
connection.ApplicationSettings.DisableSigninTokens = Library.Utility.Utility.ParseBool(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_DISABLE_SIGNIN_TOKENS], true);
|
||||
|
||||
if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_PASSWORD))
|
||||
DataConnection.ApplicationSettings.SetWebserverPassword(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_PASSWORD]);
|
||||
connection.ApplicationSettings.SetWebserverPassword(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_PASSWORD]);
|
||||
|
||||
if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES))
|
||||
DataConnection.ApplicationSettings.SetAllowedHostnames(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES]);
|
||||
connection.ApplicationSettings.SetAllowedHostnames(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES]);
|
||||
else if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES_ALT))
|
||||
DataConnection.ApplicationSettings.SetAllowedHostnames(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES_ALT]);
|
||||
connection.ApplicationSettings.SetAllowedHostnames(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES_ALT]);
|
||||
|
||||
if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_TIMEZONE) && !string.IsNullOrEmpty(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_TIMEZONE]))
|
||||
try
|
||||
{
|
||||
DataConnection.ApplicationSettings.Timezone = TimeZoneHelper.FindTimeZone(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_TIMEZONE]);
|
||||
connection.ApplicationSettings.Timezone = TimeZoneHelper.FindTimeZone(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_TIMEZONE]);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -583,33 +476,33 @@ namespace Duplicati.Server
|
||||
}
|
||||
|
||||
// The database has recorded a new version
|
||||
if (DataConnection.ApplicationSettings.UpdatedVersion != null)
|
||||
if (connection.ApplicationSettings.UpdatedVersion != null)
|
||||
{
|
||||
// Check if the running version is newer than the recorded version
|
||||
if (UpdaterManager.TryParseVersion(DataConnection.ApplicationSettings.UpdatedVersion.Version) <= UpdaterManager.TryParseVersion(UpdaterManager.SelfVersion.Version))
|
||||
if (UpdaterManager.TryParseVersion(connection.ApplicationSettings.UpdatedVersion.Version) <= UpdaterManager.TryParseVersion(UpdaterManager.SelfVersion.Version))
|
||||
{
|
||||
// Clean up lingering update notifications
|
||||
var updateNotifications = DataConnection.GetNotifications().Where(x => x.Action == "update:new").ToList();
|
||||
var updateNotifications = connection.GetNotifications().Where(x => x.Action == "update:new").ToList();
|
||||
foreach (var n in updateNotifications)
|
||||
DataConnection.DismissNotification(n.ID);
|
||||
connection.DismissNotification(n.ID);
|
||||
|
||||
// Clear up the recorded version
|
||||
DataConnection.ApplicationSettings.UpdatedVersion = null;
|
||||
connection.ApplicationSettings.UpdatedVersion = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void EmitWarningsForConfigurationIssues(Dictionary<string, string> commandlineOptions)
|
||||
private static void EmitWarningsForConfigurationIssues(Connection connection, IApplicationSettings applicationSettings, Dictionary<string, string> commandlineOptions)
|
||||
{
|
||||
if (DataConnection.ApplicationSettings.LastConfigIssueCheckVersion != UpdaterManager.SelfVersion.Version)
|
||||
if (connection.ApplicationSettings.LastConfigIssueCheckVersion != UpdaterManager.SelfVersion.Version)
|
||||
{
|
||||
var updateNotifications = DataConnection.GetNotifications().Where(x => x.Action.StartsWith("config:issue:")).ToList();
|
||||
var updateNotifications = connection.GetNotifications().Where(x => x.Action.StartsWith("config:issue:")).ToList();
|
||||
foreach (var n in updateNotifications)
|
||||
DataConnection.DismissNotification(n.ID);
|
||||
connection.DismissNotification(n.ID);
|
||||
|
||||
if (!DataConnection.IsEncryptingFields && !Library.Utility.Utility.ParseBoolOption(commandlineOptions, DISABLE_DB_ENCRYPTION_OPTION))
|
||||
if (!connection.IsEncryptingFields && !Library.Utility.Utility.ParseBoolOption(commandlineOptions, DISABLE_DB_ENCRYPTION_OPTION))
|
||||
{
|
||||
DataConnection.RegisterNotification(
|
||||
connection.RegisterNotification(
|
||||
Serialization.NotificationType.Warning,
|
||||
"Unencrypted database",
|
||||
"The database is not encrypted. This is a security risk and should be fixed as soon as possible.",
|
||||
@@ -626,9 +519,9 @@ namespace Duplicati.Server
|
||||
);
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsWindows() && DataFolder.StartsWith(Util.AppendDirSeparator(Environment.GetFolderPath(Environment.SpecialFolder.Windows)), StringComparison.OrdinalIgnoreCase))
|
||||
if (OperatingSystem.IsWindows() && applicationSettings.DataFolder.StartsWith(Util.AppendDirSeparator(Environment.GetFolderPath(Environment.SpecialFolder.Windows)), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
DataConnection.RegisterNotification(
|
||||
connection.RegisterNotification(
|
||||
Serialization.NotificationType.Warning,
|
||||
"Incorrect storage folder",
|
||||
"The server configuraion is stored inside the Windows folder. Please move the configuration to a different location, or it may be deleted on Windows version upgrades.",
|
||||
@@ -645,16 +538,16 @@ namespace Duplicati.Server
|
||||
);
|
||||
}
|
||||
|
||||
DataConnection.ApplicationSettings.LastConfigIssueCheckVersion = UpdaterManager.SelfVersion.Version;
|
||||
connection.ApplicationSettings.LastConfigIssueCheckVersion = UpdaterManager.SelfVersion.Version;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateApplicationInstance(bool writeToConsoleOnExceptionw)
|
||||
private static void CreateApplicationInstance(string dataFolder, bool writeToConsoleOnExceptionw)
|
||||
{
|
||||
try
|
||||
{
|
||||
//This will also create DATAFOLDER if it does not exist
|
||||
ApplicationInstance = new SingleInstance(DataFolder);
|
||||
ApplicationInstance = new SingleInstance(dataFolder);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -698,8 +591,8 @@ namespace Duplicati.Server
|
||||
commandlineOptions[SETTINGS_ENCRYPTION_KEY_OPTION] = Environment.GetEnvironmentVariable(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME);
|
||||
}
|
||||
|
||||
private static async Task ApplySecretProvider(Dictionary<string, string> commandlineOptions, CancellationToken cancellationToken)
|
||||
=> FIXMEGlobal.SecretProvider = await SecretProviderHelper.ApplySecretProviderAsync([], [], commandlineOptions, TempFolder.SystemTempPath, FIXMEGlobal.SecretProvider, cancellationToken).ConfigureAwait(false);
|
||||
private static async Task ApplySecretProvider(IApplicationSettings applicationSettings, Dictionary<string, string> commandlineOptions, CancellationToken cancellationToken)
|
||||
=> applicationSettings.SecretProvider = await SecretProviderHelper.ApplySecretProviderAsync([], [], commandlineOptions, TempFolder.SystemTempPath, applicationSettings.SecretProvider, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
private class ConsoleLogDestination(LogMessageType level) : ILogDestination
|
||||
{
|
||||
@@ -710,8 +603,9 @@ namespace Duplicati.Server
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConfigureLogging(Dictionary<string, string> commandlineOptions)
|
||||
private static IDisposable ConfigureLogging(ILogWriteHandler logWriteHandler, Dictionary<string, string> commandlineOptions)
|
||||
{
|
||||
IDisposable logScope;
|
||||
//Log various information in the logfile
|
||||
if (DEBUG_MODE && !commandlineOptions.ContainsKey(LOG_FILE_OPTION))
|
||||
{
|
||||
@@ -722,7 +616,7 @@ namespace Duplicati.Server
|
||||
System.IO.File.Delete(commandlineOptions[LOG_FILE_OPTION]);
|
||||
}
|
||||
|
||||
Log.StartScope(LogHandler, null);
|
||||
logScope = Log.StartScope(logWriteHandler, null);
|
||||
|
||||
if (commandlineOptions.ContainsKey(LOG_FILE_OPTION))
|
||||
{
|
||||
@@ -730,7 +624,7 @@ namespace Duplicati.Server
|
||||
if (commandlineOptions.ContainsKey(LOG_LEVEL_OPTION))
|
||||
Enum.TryParse(commandlineOptions[LOG_LEVEL_OPTION], true, out loglevel);
|
||||
|
||||
LogHandler.SetServerFile(commandlineOptions[LOG_FILE_OPTION], loglevel);
|
||||
logWriteHandler.SetServerFile(commandlineOptions[LOG_FILE_OPTION], loglevel);
|
||||
}
|
||||
|
||||
if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, LOG_CONSOLE_OPTION))
|
||||
@@ -739,7 +633,7 @@ namespace Duplicati.Server
|
||||
if (commandlineOptions.ContainsKey(LOG_LEVEL_OPTION))
|
||||
Enum.TryParse(commandlineOptions[LOG_LEVEL_OPTION], true, out loglevel);
|
||||
|
||||
LogHandler.AppendLogDestination(new ConsoleLogDestination(loglevel), loglevel);
|
||||
logWriteHandler.AppendLogDestination(new ConsoleLogDestination(loglevel), loglevel);
|
||||
}
|
||||
|
||||
if (commandlineOptions.TryGetValue(WINDOWS_EVENTLOG_OPTION, out var source) && !string.IsNullOrEmpty(source))
|
||||
@@ -765,16 +659,17 @@ namespace Duplicati.Server
|
||||
|
||||
if (WindowsEventLogSource.SourceExists(source))
|
||||
{
|
||||
var loglevel = Library.Logging.LogMessageType.Information;
|
||||
var loglevel = LogMessageType.Information;
|
||||
if (commandlineOptions.ContainsKey(WINDOWS_EVENTLOG_LEVEL_OPTION))
|
||||
Enum.TryParse(commandlineOptions[WINDOWS_EVENTLOG_LEVEL_OPTION], true, out loglevel);
|
||||
|
||||
LogHandler.AppendLogDestination(new WindowsEventLogSource(source), loglevel);
|
||||
logWriteHandler.AppendLogDestination(new WindowsEventLogSource(source), loglevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CrashlogHelper.OnUnobservedTaskException += (ex) => LogHandler.WriteMessage(new Library.Logging.LogEntry(ex.Message, null, Library.Logging.LogMessageType.Error, LOGTAG, "UnobservedTaskException", ex));
|
||||
CrashlogHelper.OnUnobservedTaskException += (ex) => logWriteHandler.WriteMessage(new LogEntry(ex.Message, null, Library.Logging.LogMessageType.Error, LOGTAG, "UnobservedTaskException", ex));
|
||||
return logScope;
|
||||
}
|
||||
|
||||
private static int ShowHelp(bool writeToConsoleOnExceptionw)
|
||||
@@ -792,15 +687,13 @@ namespace Duplicati.Server
|
||||
throw new Exception("Server invoked with --help");
|
||||
}
|
||||
|
||||
public static Connection GetDatabaseConnection(Dictionary<string, string> commandlineOptions, bool silentConsole)
|
||||
public static Connection GetDatabaseConnection(IApplicationSettings applicationSettings, Dictionary<string, string> commandlineOptions, bool silentConsole)
|
||||
{
|
||||
DataFolder = DataFolderManager.GetDataFolder(DataFolderManager.AccessMode.ReadWritePermissionSet);
|
||||
|
||||
// Emit a warning if the database is stored in the Windows folder
|
||||
if (Util.IsPathUnderWindowsFolder(DataFolder))
|
||||
if (Util.IsPathUnderWindowsFolder(applicationSettings.DataFolder))
|
||||
Log.WriteWarningMessage(LOGTAG, "DatabaseInWindowsFolder", null, "The database is stored in the Windows folder, this is not recommended as it will be deleted on Windows upgrades.");
|
||||
|
||||
CrashlogHelper.DefaultLogDir = DataFolder;
|
||||
CrashlogHelper.DefaultLogDir = applicationSettings.DataFolder;
|
||||
|
||||
var sqliteVersion = new Version(Duplicati.Library.SQLiteHelper.SQLiteLoader.SQLiteVersion);
|
||||
if (sqliteVersion < new Version(3, 6, 3))
|
||||
@@ -814,15 +707,15 @@ namespace Duplicati.Server
|
||||
|
||||
try
|
||||
{
|
||||
DatabasePath = System.IO.Path.Combine(DataFolder, DataFolderManager.SERVER_DATABASE_FILENAME);
|
||||
var databasePath = System.IO.Path.Combine(applicationSettings.DataFolder, DataFolderManager.SERVER_DATABASE_FILENAME);
|
||||
|
||||
if (!System.IO.Directory.Exists(System.IO.Path.GetDirectoryName(DatabasePath)))
|
||||
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(DatabasePath));
|
||||
if (!System.IO.Directory.Exists(System.IO.Path.GetDirectoryName(databasePath)))
|
||||
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(databasePath));
|
||||
|
||||
// Attempt to open the database, removing any encryption present
|
||||
Library.SQLiteHelper.SQLiteLoader.OpenDatabase(con, DatabasePath, Library.SQLiteHelper.SQLiteRC4Decrypter.GetEncryptionPassword(commandlineOptions));
|
||||
Library.SQLiteHelper.SQLiteLoader.OpenDatabase(con, databasePath, Library.SQLiteHelper.SQLiteRC4Decrypter.GetEncryptionPassword(commandlineOptions));
|
||||
|
||||
Library.SQLiteHelper.DatabaseUpgrader.UpgradeDatabase(con, DatabasePath, typeof(Library.RestAPI.Database.DatabaseSchemaMarker));
|
||||
Library.SQLiteHelper.DatabaseUpgrader.UpgradeDatabase(con, databasePath, typeof(Library.RestAPI.Database.DatabaseSchemaMarker));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -839,7 +732,7 @@ namespace Duplicati.Server
|
||||
var usingBlacklistedKey = encKey?.IsBlacklisted ?? false;
|
||||
var hasValidEncryptionKey = encKey != null;
|
||||
|
||||
FIXMEGlobal.SettingsEncryptionKeyProvidedExternally = hasValidEncryptionKey;
|
||||
applicationSettings.SettingsEncryptionKeyProvidedExternally = hasValidEncryptionKey;
|
||||
|
||||
if (requireDbEncryptionKey && !(hasValidEncryptionKey || disableDbEncryption))
|
||||
throw new UserInformationException(Strings.Program.DatabaseEncryptionKeyRequired(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME, DISABLE_DB_ENCRYPTION_OPTION), "RequireDbEncryptionKey");
|
||||
@@ -856,7 +749,7 @@ namespace Duplicati.Server
|
||||
|
||||
if (hasEncryptedFields)
|
||||
{
|
||||
Library.Logging.Log.WriteWarningMessage(LOGTAG, "EncryptionKeyMissing", null, Strings.Program.EncryptionKeyMissing(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME));
|
||||
Log.WriteWarningMessage(LOGTAG, "EncryptionKeyMissing", null, Strings.Program.EncryptionKeyMissing(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME));
|
||||
if (!silentConsole)
|
||||
Console.WriteLine(Strings.Program.EncryptionKeyMissing(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME));
|
||||
}
|
||||
@@ -871,61 +764,55 @@ namespace Duplicati.Server
|
||||
if (!hasValidEncryptionKey && !disableDbEncryption)
|
||||
{
|
||||
disableDbEncryption = true;
|
||||
Log.WriteWarningMessage(LOGTAG, "MissingEncryptionKey", null, Strings.Program.NoEncryptionKeySpecified(Library.Encryption.EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME, DISABLE_DB_ENCRYPTION_OPTION));
|
||||
Log.WriteWarningMessage(LOGTAG, "MissingEncryptionKey", null, Strings.Program.NoEncryptionKeySpecified(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME, DISABLE_DB_ENCRYPTION_OPTION));
|
||||
if (!silentConsole)
|
||||
Console.WriteLine(Strings.Program.NoEncryptionKeySpecified(Library.Encryption.EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME, DISABLE_DB_ENCRYPTION_OPTION));
|
||||
Console.WriteLine(Strings.Program.NoEncryptionKeySpecified(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME, DISABLE_DB_ENCRYPTION_OPTION));
|
||||
}
|
||||
|
||||
if (usingBlacklistedKey && !disableDbEncryption)
|
||||
{
|
||||
disableDbEncryption = true;
|
||||
Log.WriteErrorMessage(LOGTAG, "BlacklistedEncryptionKey", null, Strings.Program.BlacklistedEncryptionKey(Library.Encryption.EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME, DISABLE_DB_ENCRYPTION_OPTION));
|
||||
Log.WriteErrorMessage(LOGTAG, "BlacklistedEncryptionKey", null, Strings.Program.BlacklistedEncryptionKey(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME, DISABLE_DB_ENCRYPTION_OPTION));
|
||||
if (!silentConsole)
|
||||
Console.WriteLine(Strings.Program.BlacklistedEncryptionKey(Library.Encryption.EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME, DISABLE_DB_ENCRYPTION_OPTION));
|
||||
Console.WriteLine(Strings.Program.BlacklistedEncryptionKey(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME, DISABLE_DB_ENCRYPTION_OPTION));
|
||||
}
|
||||
|
||||
return new Connection(con, disableDbEncryption, encKey);
|
||||
return new Connection(con, disableDbEncryption, encKey, applicationSettings.DataFolder, applicationSettings.StartOrStopUsageReporter);
|
||||
}
|
||||
|
||||
public static void StartOrStopUsageReporter()
|
||||
private static void StartOrStopUsageReporter(Connection connection)
|
||||
{
|
||||
var disableUsageReporter =
|
||||
string.Equals(DataConnection.ApplicationSettings.UsageReporterLevel, "none", StringComparison.OrdinalIgnoreCase)
|
||||
string.Equals(connection.ApplicationSettings.UsageReporterLevel, "none", StringComparison.OrdinalIgnoreCase)
|
||||
||
|
||||
string.Equals(DataConnection.ApplicationSettings.UsageReporterLevel, "disabled", StringComparison.OrdinalIgnoreCase);
|
||||
string.Equals(connection.ApplicationSettings.UsageReporterLevel, "disabled", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!Enum.TryParse<Library.UsageReporter.ReportType>(DataConnection.ApplicationSettings.UsageReporterLevel, true, out var reportLevel))
|
||||
if (!Enum.TryParse<Library.UsageReporter.ReportType>(connection.ApplicationSettings.UsageReporterLevel, true, out var reportLevel))
|
||||
Library.UsageReporter.Reporter.SetReportLevel(null, disableUsageReporter);
|
||||
else
|
||||
Library.UsageReporter.Reporter.SetReportLevel(reportLevel, disableUsageReporter);
|
||||
}
|
||||
|
||||
private static void SignalNewEvent(object sender, EventArgs e)
|
||||
{
|
||||
StatusEventNotifyer.SignalNewEvent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This event handler updates the trayicon menu with the current state of the runner.
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
private static void LiveControl_StateChanged(LiveControls.LiveControlEvent e)
|
||||
private static void LiveControl_StateChanged(IQueueRunnerService queueRunnerService, Connection connection, EventPollNotify eventPollNotify, LiveControls.LiveControlEvent e)
|
||||
{
|
||||
var worker = FIXMEGlobal.WorkThread;
|
||||
var appSettings = FIXMEGlobal.DataConnection.ApplicationSettings;
|
||||
var appSettings = connection.ApplicationSettings;
|
||||
switch (e.State)
|
||||
{
|
||||
case LiveControls.LiveControlState.Paused:
|
||||
{
|
||||
worker.Pause();
|
||||
worker.CurrentTask?.Pause(e.TransfersPaused);
|
||||
queueRunnerService.Pause();
|
||||
queueRunnerService.GetCurrentTask()?.Pause(e.TransfersPaused);
|
||||
appSettings.PausedUntil = e.WaitTimeExpiration;
|
||||
break;
|
||||
}
|
||||
case LiveControls.LiveControlState.Running:
|
||||
{
|
||||
worker.Resume();
|
||||
worker.CurrentTask?.Resume();
|
||||
queueRunnerService.Resume();
|
||||
queueRunnerService.GetCurrentTask()?.Resume();
|
||||
appSettings.PausedUntil = null;
|
||||
break;
|
||||
}
|
||||
@@ -934,14 +821,15 @@ namespace Duplicati.Server
|
||||
break;
|
||||
}
|
||||
|
||||
StatusEventNotifyer.SignalNewEvent();
|
||||
eventPollNotify.SignalNewEvent();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple method for tracking if the server has crashed
|
||||
/// </summary>
|
||||
private static void PingPongMethod()
|
||||
/// <param name="applicationSettings">The application settings</param>
|
||||
private static void PingPongMethod(IApplicationSettings applicationSettings)
|
||||
{
|
||||
var rd = new System.IO.StreamReader(Console.OpenStandardInput());
|
||||
var wr = new System.IO.StreamWriter(Console.OpenStandardOutput());
|
||||
@@ -952,7 +840,7 @@ namespace Duplicati.Server
|
||||
{
|
||||
// TODO: All calls to ApplicationExitEvent and TrayIcon->Quit
|
||||
// should check if we are running something
|
||||
ApplicationExitEvent.Set();
|
||||
applicationSettings.ApplicationExitEvent.Set();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -158,7 +158,6 @@ public static class WebServerLoader
|
||||
/// <param name="Port">The listining port</param>
|
||||
/// <param name="Interface">The listening interface</param>
|
||||
/// <param name="Certificate">SSL certificate, if any</param>
|
||||
/// <param name="Servername">The servername to report</param>
|
||||
/// <param name="AllowedHostnames">The allowed hostnames</param>
|
||||
/// <param name="DisableStaticFiles">If static files should be disabled</param>
|
||||
/// <param name="SPAPaths">The paths to serve as SPAs</param>
|
||||
@@ -168,7 +167,6 @@ public static class WebServerLoader
|
||||
int Port,
|
||||
System.Net.IPAddress Interface,
|
||||
X509Certificate2Collection? Certificate,
|
||||
string Servername,
|
||||
IEnumerable<string> AllowedHostnames,
|
||||
bool DisableStaticFiles,
|
||||
IEnumerable<string> SPAPaths,
|
||||
@@ -264,7 +262,6 @@ public static class WebServerLoader
|
||||
-1,
|
||||
listenInterface,
|
||||
connection.ApplicationSettings.UseHTTPS ? connection.ApplicationSettings.ServerSSLCertificate : null,
|
||||
string.Format("{0} v{1}", Library.AutoUpdater.AutoUpdateSettings.AppName, Library.AutoUpdater.UpdaterManager.SelfVersion.Version),
|
||||
(connection.ApplicationSettings.AllowedHostnames ?? string.Empty).Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries),
|
||||
Utility.ParseBoolOption(options, OPTION_WEBSERVICE_API_ONLY),
|
||||
spaPathsString.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries),
|
||||
|
||||
@@ -29,7 +29,7 @@ using Duplicati.Server.Database;
|
||||
using Duplicati.Server.Serializable;
|
||||
using Duplicati.Server.Serialization;
|
||||
using Duplicati.Server.Serialization.Interface;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Duplicati.WebserverCore.Services;
|
||||
using NUnit.Framework;
|
||||
using Backup = Duplicati.Server.Database.Backup;
|
||||
|
||||
@@ -107,10 +107,8 @@ namespace Duplicati.UnitTest
|
||||
}
|
||||
|
||||
byte[] jsonByteArray;
|
||||
using (Program.DataConnection = Program.GetDatabaseConnection(advancedOptions, true))
|
||||
{
|
||||
jsonByteArray = BackupImportExportHandler.ExportToJSON(Program.DataConnection, backup, null);
|
||||
}
|
||||
using (var con = Program.GetDatabaseConnection(new ApplicationSettings(), advancedOptions, true))
|
||||
jsonByteArray = BackupImportExportHandler.ExportToJSON(con, backup, null);
|
||||
|
||||
// The username should not have the '%40' converted to '@' since the import code
|
||||
// cannot handle it (see issue #3619).
|
||||
@@ -130,13 +128,6 @@ namespace Duplicati.UnitTest
|
||||
var metadata = new Dictionary<string, string> { { "SourceFilesCount", "1" } };
|
||||
var advancedOptions = new Dictionary<string, string> { { "server-datafolder", this.serverDatafolder } };
|
||||
|
||||
// Mock the setup enough to get the import/export working
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddSingleton<INotificationUpdateService, NotificationUpdateService>();
|
||||
serviceCollection.AddSingleton(new EventPollNotify());
|
||||
FIXMEGlobal.Provider = new DefaultServiceProviderFactory().CreateServiceProvider(serviceCollection);
|
||||
FIXMEGlobal.DataFolder = this.serverDatafolder;
|
||||
|
||||
var dbpath = Path.Combine(this.serverDatafolder, Library.AutoUpdater.DataFolderManager.SERVER_DATABASE_FILENAME);
|
||||
if (File.Exists(dbpath))
|
||||
File.Delete(dbpath);
|
||||
@@ -145,7 +136,7 @@ namespace Duplicati.UnitTest
|
||||
SQLiteLoader.OpenDatabase(con, dbpath, null);
|
||||
DatabaseUpgrader.UpgradeDatabase(con, dbpath, typeof(Library.RestAPI.Database.DatabaseSchemaMarker));
|
||||
|
||||
using (var connection = new Connection(con, true, null))
|
||||
using (var connection = new Connection(con, true, null, this.serverDatafolder, null))
|
||||
{
|
||||
// Unencrypted file, don't import metadata.
|
||||
string unencryptedWithoutMetadata = Path.Combine(this.serverDatafolder, Path.GetRandomFileName());
|
||||
|
||||
@@ -25,6 +25,8 @@ namespace Duplicati.WebserverCore.Dto;
|
||||
/// </summary>
|
||||
/// <param name="Status">The status</param>
|
||||
/// <param name="ID">The ID</param>
|
||||
/// <param name="TaskStarted">When the task started</param>
|
||||
/// <param name="TaskFinished">When the task finished</param>
|
||||
/// <param name="ErrorMessage">The error message</param>
|
||||
/// <param name="Exception">The exception</param>
|
||||
public record GetTaskStateDto(string Status, long ID, string? ErrorMessage = null, string? Exception = null);
|
||||
public record GetTaskStateDto(string Status, long ID, DateTime? TaskStarted, DateTime? TaskFinished, string? ErrorMessage = null, string? Exception = null);
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using Duplicati.Library.Logging;
|
||||
using Duplicati.Server;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
|
||||
namespace Duplicati.WebserverCore.Dto;
|
||||
|
||||
@@ -56,7 +56,7 @@ public sealed record LogEntry
|
||||
/// <summary>
|
||||
/// The message ID
|
||||
/// </summary>
|
||||
public required string ExceptionID { get; init; }
|
||||
public required string? ExceptionID { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The message type
|
||||
@@ -71,7 +71,7 @@ public sealed record LogEntry
|
||||
/// <summary>
|
||||
/// The backup ID, if any
|
||||
/// </summary>
|
||||
public required string BackupID { get; init; }
|
||||
public required string? BackupID { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The task ID, if any
|
||||
@@ -83,7 +83,7 @@ public sealed record LogEntry
|
||||
/// </summary>
|
||||
/// <param name="entry">The internal record</param>
|
||||
/// <returns>The DTO record</returns>
|
||||
public static LogEntry FromInternalEntry(LogWriteHandler.LogEntry entry)
|
||||
public static LogEntry FromInternalEntry(ILogWriteHandler.LiveLogEntry entry)
|
||||
{
|
||||
return new LogEntry
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ public sealed record ServerStatusDto
|
||||
/// <summary>
|
||||
/// Gets or sets the active task.
|
||||
/// </summary>
|
||||
public required Tuple<long, string>? ActiveTask { get; init; }
|
||||
public required Tuple<long, string?>? ActiveTask { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the state of the program.
|
||||
@@ -40,7 +40,7 @@ public sealed record ServerStatusDto
|
||||
/// <summary>
|
||||
/// Gets the IDs of the tasks in the scheduler queue.
|
||||
/// </summary>
|
||||
public required IList<Tuple<long, string>> SchedulerQueueIds { get; init; } = [];
|
||||
public required IList<Tuple<long, string?>> SchedulerQueueIds { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the proposed schedule.
|
||||
|
||||
@@ -108,7 +108,6 @@ public class DuplicatiWebserver
|
||||
/// <param name="Port">The listining port</param>
|
||||
/// <param name="Interface">The listening interface</param>
|
||||
/// <param name="Certificate">The certificate, if using SSL</param>
|
||||
/// <param name="Servername">The servername to report</param>
|
||||
/// <param name="AllowedHostnames">The allowed hostnames</param>
|
||||
/// <param name="DisableStaticFiles">If static files should be disabled</param>
|
||||
/// <param name="SPAPaths">The paths to serve as SPAs</param>
|
||||
@@ -119,7 +118,6 @@ public class DuplicatiWebserver
|
||||
int Port,
|
||||
System.Net.IPAddress Interface,
|
||||
X509Certificate2Collection? Certificate,
|
||||
string Servername,
|
||||
IEnumerable<string> AllowedHostnames,
|
||||
bool DisableStaticFiles,
|
||||
IEnumerable<string> SPAPaths,
|
||||
@@ -132,8 +130,10 @@ public class DuplicatiWebserver
|
||||
/// </summary>
|
||||
/// <param name="settings">The settings for the server</param>
|
||||
/// <param name="connection">The connection to the database</param>
|
||||
/// <param name="logWriteHandler">The log write handler</param>
|
||||
/// <param name="applicationSettings">The application settings</param>
|
||||
/// <returns>The new webserver instance</returns>
|
||||
public static DuplicatiWebserver CreateWebServer(InitSettings settings, Connection connection)
|
||||
public static DuplicatiWebserver CreateWebServer(InitSettings settings, Connection connection, ILogWriteHandler logWriteHandler, IApplicationSettings applicationSettings)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(new WebApplicationOptions()
|
||||
{
|
||||
@@ -279,7 +279,7 @@ public class DuplicatiWebserver
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddCheck("Basic", () => HealthCheckResult.Healthy("Service is running"));
|
||||
|
||||
builder.Services.AddDuplicati(connection);
|
||||
builder.Services.AddDuplicati(connection, logWriteHandler, applicationSettings);
|
||||
|
||||
// Prevent logs from spamming the console, but allow enabling for debugging
|
||||
if (Environment.GetEnvironmentVariable("DUPLICATI_WEBSERVER_LOGGING") != "1")
|
||||
@@ -331,6 +331,13 @@ public class DuplicatiWebserver
|
||||
if (!settings.DisableStaticFiles)
|
||||
app.UseDefaultStaticFiles(settings.WebRoot, settings.SPAPaths);
|
||||
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
// Set up the log scope to capture any log messages for the livelog / server log
|
||||
using (var scope = Library.Logging.Log.StartScope(logWriteHandler))
|
||||
await next();
|
||||
});
|
||||
|
||||
app.UseExceptionHandler(app =>
|
||||
{
|
||||
app.Run(async context =>
|
||||
@@ -387,9 +394,9 @@ public class DuplicatiWebserver
|
||||
var _ = Task.Run(() => app.Services.GetRequiredService<ISystemInfoProvider>().GetSystemInfo(null));
|
||||
|
||||
// Get a string description of the listen interface used
|
||||
var listenInterface = settings.Interface == System.Net.IPAddress.Any
|
||||
var listenInterface = settings.Interface == IPAddress.Any
|
||||
? "*"
|
||||
: settings.Interface == System.Net.IPAddress.Loopback
|
||||
: settings.Interface == IPAddress.Loopback
|
||||
? "localhost"
|
||||
: settings.Interface.ToString();
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Duplicati.Library.Interface;
|
||||
using Duplicati.Library.RestAPI.Abstractions;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Server;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.Server.Serialization;
|
||||
@@ -42,8 +42,8 @@ public class BackupGet : IEndpointV1
|
||||
=> ExecuteGet(connection, GetBackup(connection, id)))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapGet("/backup/{id}/files", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery] string? filter, [FromQuery] string? time, [FromQuery(Name = "all-versions")] bool? allVersions, [FromQuery(Name = "prefix-only")] bool? prefixOnly, [FromQuery(Name = "folder-contents")] bool? folderContents)
|
||||
=> ExecuteGetFiles(GetBackup(connection, id), filter, time, allVersions ?? false, prefixOnly ?? false, folderContents ?? false, new Dictionary<string, string>()))
|
||||
group.MapGet("/backup/{id}/files", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id, [FromQuery] string? filter, [FromQuery] string? time, [FromQuery(Name = "all-versions")] bool? allVersions, [FromQuery(Name = "prefix-only")] bool? prefixOnly, [FromQuery(Name = "folder-contents")] bool? folderContents)
|
||||
=> ExecuteGetFiles(queueRunnerService, GetBackup(connection, id), filter, time, allVersions ?? false, prefixOnly ?? false, folderContents ?? false, new Dictionary<string, string>()))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapGet("/backup/{id}/log", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery] long? offset, [FromQuery] long? pagesize)
|
||||
@@ -54,16 +54,16 @@ public class BackupGet : IEndpointV1
|
||||
=> ExecuteGetRemotelog(connection, GetBackup(connection, id), offset, pagesize ?? 100))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapGet("/backup/{id}/filesets", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery(Name = "include-metadata")] bool? includeMetadata, [FromQuery(Name = "from-remote-only")] bool? fromRemoteOnly)
|
||||
=> ExecuteGetFilesets(GetBackup(connection, id), includeMetadata ?? false, fromRemoteOnly ?? false))
|
||||
group.MapGet("/backup/{id}/filesets", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id, [FromQuery(Name = "include-metadata")] bool? includeMetadata, [FromQuery(Name = "from-remote-only")] bool? fromRemoteOnly)
|
||||
=> ExecuteGetFilesets(queueRunnerService, GetBackup(connection, id), includeMetadata ?? false, fromRemoteOnly ?? false))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapGet("/backup/{id}/export-argsonly", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery(Name = "export-passwords")] bool? exportPasswords, [FromQuery] string? passphrase)
|
||||
=> ExecuteGetExportArgsOnly(GetBackup(connection, id), exportPasswords ?? false))
|
||||
=> ExecuteGetExportArgsOnly(connection, GetBackup(connection, id), exportPasswords ?? false))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapGet("/backup/{id}/export-cmdline", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery(Name = "export-passwords")] bool? exportPasswords, [FromQuery] string? passphrase)
|
||||
=> ExecuteGetExportCmdline(GetBackup(connection, id), exportPasswords ?? false))
|
||||
=> ExecuteGetExportCmdline(connection, GetBackup(connection, id), exportPasswords ?? false))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapGet("/backup/{id}/export", ([FromServices] Connection connection, [FromServices] IHttpContextAccessor httpContextAccessor, [FromServices] IJWTTokenProvider jWTTokenProvider, [FromRoute] string id, [FromQuery(Name = "export-passwords")] bool? exportPasswords, [FromQuery] string? passphrase, [FromQuery] string token, CancellationToken ct) =>
|
||||
@@ -86,8 +86,8 @@ public class BackupGet : IEndpointV1
|
||||
=> ExecuteGetIsdbUsedElsewhere(GetBackup(connection, id)))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapGet("/backup/{id}/isactive", ([FromServices] Connection connection, [FromServices] IWorkerThreadsManager workerThreadsManager, [FromRoute] string id)
|
||||
=> ExecuteGetIsActive(workerThreadsManager, GetBackup(connection, id)))
|
||||
group.MapGet("/backup/{id}/isactive", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id)
|
||||
=> ExecuteGetIsActive(queueRunnerService, GetBackup(connection, id)))
|
||||
.RequireAuthorization();
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ public class BackupGet : IEndpointV1
|
||||
);
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> SearchFiles(IBackup backup, string? filter, string? timestring, bool allVersions, bool prefixOnly, bool folderContents, Dictionary<string, string> extraValues)
|
||||
private static Dictionary<string, object> SearchFiles(IQueueRunnerService queueRunnerService, IBackup backup, string? filter, string? timestring, bool allVersions, bool prefixOnly, bool folderContents, Dictionary<string, string> extraValues)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(timestring) && !allVersions)
|
||||
throw new BadRequestException("Invalid or missing time");
|
||||
@@ -152,7 +152,7 @@ public class BackupGet : IEndpointV1
|
||||
if (!allVersions)
|
||||
time = Library.Utility.Timeparser.ParseTimeInterval(timestring, DateTime.Now);
|
||||
|
||||
var r = Runner.Run(Runner.CreateListTask(backup, [filter], prefixOnly, allVersions, folderContents, time), false) as Duplicati.Library.Interface.IListResults;
|
||||
var r = queueRunnerService.RunImmediately(Runner.CreateListTask(backup, filter == null ? null : [filter], prefixOnly, allVersions, folderContents, time)) as IListResults;
|
||||
if (r == null)
|
||||
throw new ServerErrorException("No result from list operation");
|
||||
|
||||
@@ -171,8 +171,8 @@ public class BackupGet : IEndpointV1
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> ExecuteGetFiles(IBackup bk, string? filter, string? timestring, bool allVersions, bool prefixOnly, bool folderContents, Dictionary<string, string> extraValues)
|
||||
=> SearchFiles(bk, filter, timestring, allVersions, prefixOnly, folderContents, extraValues);
|
||||
private static Dictionary<string, object> ExecuteGetFiles(IQueueRunnerService queueRunnerService, IBackup bk, string? filter, string? timestring, bool allVersions, bool prefixOnly, bool folderContents, Dictionary<string, string> extraValues)
|
||||
=> SearchFiles(queueRunnerService, bk, filter, timestring, allVersions, prefixOnly, folderContents, extraValues);
|
||||
|
||||
private static List<Dictionary<string, object>> ExecuteGetLog(Connection connection, IBackup bk, long? offset, long pagesize)
|
||||
{
|
||||
@@ -203,9 +203,9 @@ public class BackupGet : IEndpointV1
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<IListResultFileset> ExecuteGetFilesets(IBackup bk, bool includeMetadata, bool fromRemoteOnly)
|
||||
private static IEnumerable<IListResultFileset> ExecuteGetFilesets(IQueueRunnerService queueRunnerService, IBackup bk, bool includeMetadata, bool fromRemoteOnly)
|
||||
{
|
||||
var extra = new Dictionary<string, string>
|
||||
var extra = new Dictionary<string, string?>
|
||||
{
|
||||
["list-sets-only"] = "true"
|
||||
};
|
||||
@@ -219,7 +219,7 @@ public class BackupGet : IEndpointV1
|
||||
|
||||
try
|
||||
{
|
||||
var r = Runner.Run(Runner.CreateTask(DuplicatiOperation.List, bk, extra), false) as IListResults;
|
||||
var r = queueRunnerService.RunImmediately(Runner.CreateTask(DuplicatiOperation.List, bk, extra)) as IListResults;
|
||||
if (r == null)
|
||||
throw new ServerErrorException("No result from list operation");
|
||||
|
||||
@@ -240,20 +240,20 @@ public class BackupGet : IEndpointV1
|
||||
backup.SanitizeTargetUrl();
|
||||
}
|
||||
|
||||
private static Dto.ExportCommandlineDto ExecuteGetExportCmdline(IBackup backup, bool exportPasswords)
|
||||
private static Dto.ExportCommandlineDto ExecuteGetExportCmdline(Connection connection, IBackup backup, bool exportPasswords)
|
||||
{
|
||||
if (!exportPasswords)
|
||||
RemovePasswords(backup);
|
||||
|
||||
return new Dto.ExportCommandlineDto(Runner.GetCommandLine(Runner.CreateTask(DuplicatiOperation.Backup, backup)));
|
||||
return new Dto.ExportCommandlineDto(Runner.GetCommandLine(connection, Runner.CreateTask(DuplicatiOperation.Backup, backup)));
|
||||
}
|
||||
|
||||
private static Dto.ExportArgsOnlyDto ExecuteGetExportArgsOnly(IBackup backup, bool exportPasswords)
|
||||
private static Dto.ExportArgsOnlyDto ExecuteGetExportArgsOnly(Connection connection, IBackup backup, bool exportPasswords)
|
||||
{
|
||||
if (!exportPasswords)
|
||||
RemovePasswords(backup);
|
||||
|
||||
var parts = Runner.GetCommandLineParts(Runner.CreateTask(DuplicatiOperation.Backup, backup));
|
||||
var parts = Runner.GetCommandLineParts(connection, Runner.CreateTask(DuplicatiOperation.Backup, backup));
|
||||
return new Dto.ExportArgsOnlyDto(
|
||||
parts.First(),
|
||||
parts.Skip(1).Where(x => !x.StartsWith("--", StringComparison.Ordinal)),
|
||||
@@ -304,17 +304,14 @@ public class BackupGet : IEndpointV1
|
||||
private static Dto.IsDbUsedElsewhereDto ExecuteGetIsdbUsedElsewhere(IBackup bk)
|
||||
=> new Dto.IsDbUsedElsewhereDto(Library.Main.CLIDatabaseLocator.IsDatabasePathInUse(bk.DBPath));
|
||||
|
||||
private static Dto.IsBackupActiveDto ExecuteGetIsActive(IWorkerThreadsManager workerThreadsManager, IBackup bk)
|
||||
private static Dto.IsBackupActiveDto ExecuteGetIsActive(IQueueRunnerService queueRunnerService, IBackup bk)
|
||||
{
|
||||
if (workerThreadsManager.WorkerThread == null)
|
||||
throw new InvalidOperationException("Worker thread not available");
|
||||
|
||||
var t = workerThreadsManager.WorkerThread.CurrentTask;
|
||||
var bt = t?.Backup;
|
||||
if (bt != null && bk.ID == bt.ID)
|
||||
var t = queueRunnerService.GetCurrentTask();
|
||||
var bt = t?.BackupID;
|
||||
if (bt != null && bk.ID == bt)
|
||||
return new Dto.IsBackupActiveDto("OK", true);
|
||||
|
||||
if (workerThreadsManager.WorkerThread.CurrentTasks.Any(x => x?.Backup == null || x.Backup.ID == bk.ID))
|
||||
if (queueRunnerService.GetCurrentTasks().Any(x => x?.BackupID == null || x.BackupID == bk.ID))
|
||||
return new Dto.IsBackupActiveDto("OK", true);
|
||||
|
||||
return new Dto.IsBackupActiveDto("OK", false);
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
// 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.RestAPI.Abstractions;
|
||||
using Duplicati.Server;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.Server.Serialization;
|
||||
@@ -45,44 +44,44 @@ public class BackupPost : IEndpointV1
|
||||
=> UpdateDatabasePath(connection, GetBackup(connection, id), input.path, false))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapPost("/backup/{id}/restore", ([FromServices] Connection connection, [FromServices] IWorkerThreadsManager workerThreadsManager, [FromRoute] string id, [FromBody] Dto.RestoreInputDto input)
|
||||
=> ExecuteRestore(GetBackup(connection, id), workerThreadsManager, input))
|
||||
group.MapPost("/backup/{id}/restore", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id, [FromBody] Dto.RestoreInputDto input)
|
||||
=> ExecuteRestore(GetBackup(connection, id), queueRunnerService, input))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapPost("/backup/{id}/createreport", ([FromServices] Connection connection, [FromServices] IWorkerThreadsManager workerThreadsManager, [FromRoute] string id)
|
||||
=> ExecuteCreateReport(GetBackup(connection, id), workerThreadsManager))
|
||||
group.MapPost("/backup/{id}/createreport", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id)
|
||||
=> ExecuteCreateReport(GetBackup(connection, id), queueRunnerService))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapPost("/backup/{id}/repair", ([FromServices] Connection connection, [FromServices] IWorkerThreadsManager workerThreadsManager, [FromRoute] string id, Dto.RepairInputDto? input)
|
||||
=> ExecuteRepair(GetBackup(connection, id), workerThreadsManager, input))
|
||||
group.MapPost("/backup/{id}/repair", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id, Dto.RepairInputDto? input)
|
||||
=> ExecuteRepair(GetBackup(connection, id), queueRunnerService, input))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapPost("/backup/{id}/repairupdate", ([FromServices] Connection connection, [FromServices] IWorkerThreadsManager workerThreadsManager, [FromRoute] string id, Dto.RepairInputDto? input)
|
||||
=> ExecuteRepairUpdate(GetBackup(connection, id), workerThreadsManager, input))
|
||||
group.MapPost("/backup/{id}/repairupdate", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id, Dto.RepairInputDto? input)
|
||||
=> ExecuteRepairUpdate(GetBackup(connection, id), queueRunnerService, input))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapPost("/backup/{id}/vacuum", ([FromServices] Connection connection, [FromServices] IWorkerThreadsManager workerThreadsManager, [FromRoute] string id)
|
||||
=> ExecuteVacuum(GetBackup(connection, id), workerThreadsManager))
|
||||
group.MapPost("/backup/{id}/vacuum", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id)
|
||||
=> ExecuteVacuum(GetBackup(connection, id), queueRunnerService))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapPost("/backup/{id}/verify", ([FromServices] Connection connection, [FromServices] IWorkerThreadsManager workerThreadsManager, [FromRoute] string id)
|
||||
=> ExecuteVerify(GetBackup(connection, id), workerThreadsManager))
|
||||
group.MapPost("/backup/{id}/verify", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id)
|
||||
=> ExecuteVerify(GetBackup(connection, id), queueRunnerService))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapPost("/backup/{id}/compact", ([FromServices] Connection connection, [FromServices] IWorkerThreadsManager workerThreadsManager, [FromRoute] string id)
|
||||
=> ExecuteCompact(GetBackup(connection, id), workerThreadsManager))
|
||||
group.MapPost("/backup/{id}/compact", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id)
|
||||
=> ExecuteCompact(GetBackup(connection, id), queueRunnerService))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapPost("/backup/{id}/start", ([FromServices] Connection connection, [FromServices] IWorkerThreadsManager workerThreadsManager, [FromRoute] string id)
|
||||
=> ExecuteRunBackup(GetBackup(connection, id), workerThreadsManager))
|
||||
group.MapPost("/backup/{id}/start", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id)
|
||||
=> ExecuteRunBackup(GetBackup(connection, id), queueRunnerService))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapPost("/backup/{id}/run", ([FromServices] Connection connection, [FromServices] IWorkerThreadsManager workerThreadsManager, [FromRoute] string id)
|
||||
=> ExecuteRunBackup(GetBackup(connection, id), workerThreadsManager))
|
||||
group.MapPost("/backup/{id}/run", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id)
|
||||
=> ExecuteRunBackup(GetBackup(connection, id), queueRunnerService))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapPost("/backup/{id}/report-remote-size", ([FromServices] Connection connection, [FromServices] IWorkerThreadsManager workerThreadsManager, [FromRoute] string id)
|
||||
=> ExecuteReportRemoteSize(GetBackup(connection, id), workerThreadsManager))
|
||||
group.MapPost("/backup/{id}/report-remote-size", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id)
|
||||
=> ExecuteReportRemoteSize(GetBackup(connection, id), queueRunnerService))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapPost("/backup/{id}/copytotemp", ([FromServices] Connection connection, [FromRoute] string id)
|
||||
@@ -114,8 +113,8 @@ public class BackupPost : IEndpointV1
|
||||
|
||||
|
||||
|
||||
private static Dto.TaskStartedDto ExecuteRestore(IBackup backup, IWorkerThreadsManager workerThreadsManager, Dto.RestoreInputDto input)
|
||||
=> new Dto.TaskStartedDto("OK", workerThreadsManager.AddTask(Runner.CreateRestoreTask(
|
||||
private static Dto.TaskStartedDto ExecuteRestore(IBackup backup, IQueueRunnerService queueRunnerService, Dto.RestoreInputDto input)
|
||||
=> new Dto.TaskStartedDto("OK", queueRunnerService.AddTask(Runner.CreateRestoreTask(
|
||||
backup,
|
||||
input.paths ?? [],
|
||||
Library.Utility.Timeparser.ParseTimeInterval(input.time, DateTime.Now),
|
||||
@@ -125,31 +124,31 @@ public class BackupPost : IEndpointV1
|
||||
input.skip_metadata ?? false,
|
||||
string.IsNullOrWhiteSpace(input.passphrase) ? null : input.passphrase)));
|
||||
|
||||
private static Dto.TaskStartedDto ExecuteCreateReport(IBackup backup, IWorkerThreadsManager workerThreadsManager)
|
||||
=> new Dto.TaskStartedDto("OK", workerThreadsManager.AddTask(Runner.CreateTask(DuplicatiOperation.CreateReport, backup)));
|
||||
private static Dto.TaskStartedDto ExecuteCreateReport(IBackup backup, IQueueRunnerService queueRunnerService)
|
||||
=> new Dto.TaskStartedDto("OK", queueRunnerService.AddTask(Runner.CreateTask(DuplicatiOperation.CreateReport, backup)));
|
||||
|
||||
private static Dto.TaskStartedDto ExecuteReportRemoteSize(IBackup backup, IWorkerThreadsManager workerThreadsManager)
|
||||
=> new Dto.TaskStartedDto("OK", workerThreadsManager.AddTask(Runner.CreateTask(DuplicatiOperation.ListRemote, backup)));
|
||||
private static Dto.TaskStartedDto ExecuteReportRemoteSize(IBackup backup, IQueueRunnerService queueRunnerService)
|
||||
=> new Dto.TaskStartedDto("OK", queueRunnerService.AddTask(Runner.CreateTask(DuplicatiOperation.ListRemote, backup)));
|
||||
|
||||
private static Dto.TaskStartedDto ExecuteRepair(IBackup backup, IWorkerThreadsManager workerThreadsManager, Dto.RepairInputDto? input)
|
||||
=> DoRepair(backup, false, workerThreadsManager, input);
|
||||
private static Dto.TaskStartedDto ExecuteRepair(IBackup backup, IQueueRunnerService queueRunnerService, Dto.RepairInputDto? input)
|
||||
=> DoRepair(backup, false, queueRunnerService, input);
|
||||
|
||||
private static Dto.TaskStartedDto ExecuteRepairUpdate(IBackup backup, IWorkerThreadsManager workerThreadsManager, Dto.RepairInputDto? input)
|
||||
=> DoRepair(backup, true, workerThreadsManager, input);
|
||||
private static Dto.TaskStartedDto ExecuteRepairUpdate(IBackup backup, IQueueRunnerService queueRunnerService, Dto.RepairInputDto? input)
|
||||
=> DoRepair(backup, true, queueRunnerService, input);
|
||||
|
||||
private static Dto.TaskStartedDto ExecuteVacuum(IBackup backup, IWorkerThreadsManager workerThreadsManager)
|
||||
=> new Dto.TaskStartedDto("OK", workerThreadsManager.AddTask(Runner.CreateTask(DuplicatiOperation.Vacuum, backup)));
|
||||
private static Dto.TaskStartedDto ExecuteVacuum(IBackup backup, IQueueRunnerService queueRunnerService)
|
||||
=> new Dto.TaskStartedDto("OK", queueRunnerService.AddTask(Runner.CreateTask(DuplicatiOperation.Vacuum, backup)));
|
||||
|
||||
private static Dto.TaskStartedDto ExecuteVerify(IBackup backup, IWorkerThreadsManager workerThreadsManager)
|
||||
=> new Dto.TaskStartedDto("OK", workerThreadsManager.AddTask(Runner.CreateTask(DuplicatiOperation.Verify, backup)));
|
||||
private static Dto.TaskStartedDto ExecuteVerify(IBackup backup, IQueueRunnerService queueRunnerService)
|
||||
=> new Dto.TaskStartedDto("OK", queueRunnerService.AddTask(Runner.CreateTask(DuplicatiOperation.Verify, backup)));
|
||||
|
||||
private static Dto.TaskStartedDto ExecuteCompact(IBackup backup, IWorkerThreadsManager workerThreadsManager)
|
||||
=> new Dto.TaskStartedDto("OK", workerThreadsManager.AddTask(Runner.CreateTask(DuplicatiOperation.Compact, backup)));
|
||||
private static Dto.TaskStartedDto ExecuteCompact(IBackup backup, IQueueRunnerService queueRunnerService)
|
||||
=> new Dto.TaskStartedDto("OK", queueRunnerService.AddTask(Runner.CreateTask(DuplicatiOperation.Compact, backup)));
|
||||
|
||||
private static Dto.TaskStartedDto DoRepair(IBackup backup, bool repairUpdate, IWorkerThreadsManager workerThreadsManager, Dto.RepairInputDto? input)
|
||||
private static Dto.TaskStartedDto DoRepair(IBackup backup, bool repairUpdate, IQueueRunnerService queueRunnerService, Dto.RepairInputDto? input)
|
||||
{
|
||||
// These are all props on the input object
|
||||
var extra = new Dictionary<string, string>();
|
||||
var extra = new Dictionary<string, string?>();
|
||||
if (input != null)
|
||||
{
|
||||
if (input.only_paths.HasValue)
|
||||
@@ -168,23 +167,23 @@ public class BackupPost : IEndpointV1
|
||||
|
||||
var filters = input?.paths ?? [];
|
||||
|
||||
return new Dto.TaskStartedDto("OK", workerThreadsManager.AddTask(Runner.CreateTask(repairUpdate ? DuplicatiOperation.RepairUpdate : DuplicatiOperation.Repair, backup, extra, filters)));
|
||||
return new Dto.TaskStartedDto("OK", queueRunnerService.AddTask(Runner.CreateTask(repairUpdate ? DuplicatiOperation.RepairUpdate : DuplicatiOperation.Repair, backup, extra, filters)));
|
||||
}
|
||||
|
||||
private static Dto.TaskStartedDto ExecuteRunBackup(IBackup backup, IWorkerThreadsManager workerThreadsManager)
|
||||
private static Dto.TaskStartedDto ExecuteRunBackup(IBackup backup, IQueueRunnerService queueRunnerService)
|
||||
{
|
||||
var t = workerThreadsManager.WorkerThread?.CurrentTask;
|
||||
var bt = t?.Backup;
|
||||
var t = queueRunnerService.GetCurrentTask();
|
||||
var bt = t?.BackupID;
|
||||
|
||||
// Already running
|
||||
if (bt != null && backup.ID == bt.ID)
|
||||
if (bt != null && backup.ID == bt)
|
||||
return new Dto.TaskStartedDto("OK", t!.TaskID);
|
||||
|
||||
t = workerThreadsManager.WorkerThread?.CurrentTasks.FirstOrDefault(x => x?.Backup != null && x.Backup.ID == backup.ID);
|
||||
t = queueRunnerService.GetCurrentTasks().FirstOrDefault(x => x.BackupID == backup.ID);
|
||||
if (t != null)
|
||||
return new Dto.TaskStartedDto("OK", t.TaskID);
|
||||
|
||||
return new Dto.TaskStartedDto("OK", workerThreadsManager.AddTask(Runner.CreateTask(DuplicatiOperation.Backup, backup), true));
|
||||
return new Dto.TaskStartedDto("OK", queueRunnerService.AddTask(Runner.CreateTask(DuplicatiOperation.Backup, backup), true));
|
||||
}
|
||||
|
||||
private class WrappedBackup : Server.Database.Backup
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
// 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.Text.Json;
|
||||
using Duplicati.Library.RestAPI.Abstractions;
|
||||
using Duplicati.Server;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.Server.Serialization;
|
||||
@@ -38,9 +36,9 @@ public class BackupPutDelete : IEndpointV1
|
||||
=> ExecutePut(GetBackup(connection, id), connection, input))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapDelete("/backup/{id}", ([FromServices] Connection connection, [FromServices] IWorkerThreadsManager workerThreadsManager, [FromServices] LiveControls liveControls, [FromServices] IHttpContextAccessor httpContextAccessor, [FromRoute] string id, [FromQuery(Name = "delete-remote-files")] bool? delete_remote_files, [FromQuery(Name = "delete-local-db")] bool? delete_local_db, [FromQuery] bool? force) =>
|
||||
group.MapDelete("/backup/{id}", ([FromServices] Connection connection, [FromServices] ISchedulerService schedulerService, [FromServices] IQueueRunnerService queueRunnerService, [FromServices] LiveControls liveControls, [FromServices] IHttpContextAccessor httpContextAccessor, [FromRoute] string id, [FromQuery(Name = "delete-remote-files")] bool? delete_remote_files, [FromQuery(Name = "delete-local-db")] bool? delete_local_db, [FromQuery] bool? force) =>
|
||||
{
|
||||
var res = ExecuteDelete(GetBackup(connection, id), workerThreadsManager, liveControls, delete_remote_files ?? false, delete_local_db, force ?? false);
|
||||
var res = ExecuteDelete(GetBackup(connection, id), schedulerService, queueRunnerService, liveControls, delete_remote_files ?? false, delete_local_db, force ?? false);
|
||||
if (res.Status != "OK" && httpContextAccessor.HttpContext != null)
|
||||
httpContextAccessor.HttpContext.Response.StatusCode = 500;
|
||||
return res;
|
||||
@@ -132,67 +130,56 @@ public class BackupPutDelete : IEndpointV1
|
||||
}
|
||||
}
|
||||
|
||||
private static Dto.DeleteBackupOutputDto ExecuteDelete(IBackup backup, IWorkerThreadsManager workerThreadsManager, LiveControls liveControls, bool delete_remote_files, bool? delete_local_db, bool force)
|
||||
private static Dto.DeleteBackupOutputDto ExecuteDelete(IBackup backup, ISchedulerService schedulerService, IQueueRunnerService queueRunnerService, LiveControls liveControls, bool delete_remote_files, bool? delete_local_db, bool force)
|
||||
{
|
||||
if (workerThreadsManager.WorkerThread!.Active)
|
||||
try
|
||||
{
|
||||
try
|
||||
var nt = queueRunnerService.GetCurrentTask();
|
||||
if (backup.ID == nt?.BackupID)
|
||||
{
|
||||
//TODO: It's not safe to access the values like this,
|
||||
//because the runner thread might interfere
|
||||
var nt = workerThreadsManager.WorkerThread.CurrentTask;
|
||||
if (backup.Equals(nt?.Backup))
|
||||
if (!force)
|
||||
return new Dto.DeleteBackupOutputDto("failed", "backup-in-progress", nt?.TaskID);
|
||||
|
||||
|
||||
bool hasPaused = liveControls.State != LiveControls.LiveControlState.Paused;
|
||||
if (hasPaused)
|
||||
liveControls.Pause(true);
|
||||
nt.Abort();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
if (!force)
|
||||
return new Dto.DeleteBackupOutputDto("failed", "backup-in-progress", nt?.TaskID);
|
||||
|
||||
|
||||
bool hasPaused = liveControls.State != LiveControls.LiveControlState.Paused;
|
||||
if (hasPaused)
|
||||
liveControls.Pause(true);
|
||||
nt.Abort();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
if (workerThreadsManager.WorkerThread.Active)
|
||||
{
|
||||
var t = workerThreadsManager.WorkerThread.CurrentTask;
|
||||
if (backup.Equals(t == null ? null : t.Backup))
|
||||
Thread.Sleep(1000);
|
||||
else
|
||||
break;
|
||||
}
|
||||
else
|
||||
break;
|
||||
|
||||
if (workerThreadsManager.WorkerThread.Active)
|
||||
{
|
||||
var t = workerThreadsManager.WorkerThread.CurrentTask;
|
||||
if (backup.Equals(t == null ? null : t.Backup))
|
||||
{
|
||||
if (hasPaused)
|
||||
liveControls.Resume();
|
||||
|
||||
return new Dto.DeleteBackupOutputDto("failed", "backup-unstoppable", t?.TaskID);
|
||||
}
|
||||
}
|
||||
var tt = queueRunnerService.GetCurrentTask();
|
||||
if (backup.ID == tt?.BackupID)
|
||||
Thread.Sleep(1000);
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
var t = queueRunnerService.GetCurrentTask();
|
||||
if (backup.ID == t?.BackupID)
|
||||
{
|
||||
if (hasPaused)
|
||||
liveControls.Resume();
|
||||
|
||||
return new Dto.DeleteBackupOutputDto("failed", "backup-unstoppable", t?.TaskID);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new Dto.DeleteBackupOutputDto("error", ex.Message, null);
|
||||
|
||||
if (hasPaused)
|
||||
liveControls.Resume();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new Dto.DeleteBackupOutputDto("error", ex.Message, null);
|
||||
}
|
||||
|
||||
var extra = new Dictionary<string, string>();
|
||||
var extra = new Dictionary<string, string?>();
|
||||
if (delete_local_db.HasValue)
|
||||
extra["delete-local-db"] = delete_local_db.Value.ToString();
|
||||
if (delete_remote_files)
|
||||
extra["delete-remote-files"] = "true";
|
||||
|
||||
return new Dto.DeleteBackupOutputDto("OK", null, workerThreadsManager.AddTask(Runner.CreateTask(DuplicatiOperation.Delete, backup, extra)));
|
||||
return new Dto.DeleteBackupOutputDto("OK", null, queueRunnerService.AddTask(Runner.CreateDeleteTask(backup, extra, (_) => schedulerService.Reschedule())));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ public class BackupDefaults : IEndpointV1
|
||||
{
|
||||
// Add built-in defaults
|
||||
Newtonsoft.Json.Linq.JObject? n;
|
||||
using (var s = new StreamReader(typeof(FIXMEGlobal).Assembly.GetManifestResourceStream(typeof(FIXMEGlobal), "newbackup.json")!))
|
||||
using (var s = new StreamReader(typeof(Server.Runner).Assembly.GetManifestResourceStream(typeof(Server.Runner), "newbackup.json")!))
|
||||
n = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(s.ReadToEnd())!;
|
||||
|
||||
o.Merge(n);
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using Duplicati.Library.Crashlog;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
using Duplicati.WebserverCore.Dto;
|
||||
@@ -31,8 +30,8 @@ public class LogData : IEndpointV1
|
||||
{
|
||||
public static void Map(RouteGroupBuilder group)
|
||||
{
|
||||
group.MapGet("/logdata/poll", ([FromServices] Connection connection, [FromQuery] Library.Logging.LogMessageType level, [FromQuery] long id, [FromQuery] long? offset, [FromQuery] int? pagesize)
|
||||
=> ExecuteLogPoll(level, id, offset ?? 0, pagesize ?? 100))
|
||||
group.MapGet("/logdata/poll", ([FromServices] ILogWriteHandler logWriteHandler, [FromQuery] Library.Logging.LogMessageType level, [FromQuery] long id, [FromQuery] int? pagesize)
|
||||
=> ExecuteLogPoll(logWriteHandler, level, id, pagesize ?? 100))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapGet("/logdata/log", ([FromServices] Connection connection, [FromQuery] long? offset, [FromQuery] int? pagesize)
|
||||
@@ -44,10 +43,10 @@ public class LogData : IEndpointV1
|
||||
.RequireAuthorization();
|
||||
}
|
||||
|
||||
private static LogEntry[] ExecuteLogPoll(Library.Logging.LogMessageType level, long id, long offset, int pagesize)
|
||||
private static LogEntry[] ExecuteLogPoll(ILogWriteHandler logWriteHandler, Library.Logging.LogMessageType level, long id, int pagesize)
|
||||
{
|
||||
pagesize = Math.Max(1, Math.Min(500, pagesize));
|
||||
return FIXMEGlobal.LogHandler.AfterID(id, level, pagesize).Select(x => Dto.LogEntry.FromInternalEntry(x)).ToArray();
|
||||
return logWriteHandler.AfterID(id, level, pagesize).Select(x => Dto.LogEntry.FromInternalEntry(x)).ToArray();
|
||||
}
|
||||
|
||||
private static List<Dictionary<string, object>>? ExecuteGetLog(Connection connection, long? offset, long pagesize)
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
// 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.RestAPI;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
using Duplicati.WebserverCore.Exceptions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Duplicati.WebserverCore.Endpoints.V1;
|
||||
|
||||
@@ -28,15 +28,16 @@ public class ProgressState : IEndpointV1
|
||||
{
|
||||
public static void Map(RouteGroupBuilder group)
|
||||
{
|
||||
group.MapGet("/progressstate", Execute)
|
||||
group.MapGet("/progressstate", ([FromServices] IProgressStateProviderService progressStateProviderService) => Execute(progressStateProviderService))
|
||||
.RequireAuthorization();
|
||||
}
|
||||
|
||||
private static Server.Serialization.Interface.IProgressEventData Execute()
|
||||
private static Server.Serialization.Interface.IProgressEventData Execute(IProgressStateProviderService progressStateProviderService)
|
||||
{
|
||||
if (FIXMEGlobal.GenerateProgressState == null)
|
||||
var pgState = progressStateProviderService.GenerateProgressState;
|
||||
if (pgState == null)
|
||||
throw new NotFoundException("No active backup");
|
||||
|
||||
return FIXMEGlobal.GenerateProgressState();
|
||||
return pgState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
// 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.RestAPI;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
using Duplicati.WebserverCore.Exceptions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -32,9 +31,12 @@ public class RemoteControl : IEndpointV1
|
||||
group.MapGet("/remotecontrol/status", ([FromServices] IRemoteControllerRegistration registration, [FromServices] IRemoteController remoteController)
|
||||
=> GetStatus(registration, remoteController))
|
||||
.RequireAuthorization();
|
||||
}
|
||||
|
||||
public static void MapConditional(RouteGroupBuilder group, string origin)
|
||||
{
|
||||
// Don't allow these in agent-mode
|
||||
if (FIXMEGlobal.Origin == "Agent")
|
||||
if (origin == "Agent")
|
||||
return;
|
||||
|
||||
group.MapPost("/remotecontrol/enable", ([FromServices] IRemoteControllerRegistration registration, [FromServices] IRemoteController remoteController)
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using Duplicati.Library.Interface;
|
||||
using Duplicati.Library.Main;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Server;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
using Duplicati.WebserverCore.Exceptions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -38,8 +38,8 @@ namespace Duplicati.WebserverCore.Endpoints.V1
|
||||
=> ExecuteDbPath(input.path))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapPost("/remoteoperation/test", ([FromQuery] bool? autocreate, [FromBody] RemoteOperationInput input, CancellationToken cancelToken)
|
||||
=> ExecuteTest(input.path, autocreate ?? false, cancelToken))
|
||||
group.MapPost("/remoteoperation/test", ([FromServices] Connection connection, [FromServices] IApplicationSettings applicationSettings, [FromQuery] bool? autocreate, [FromBody] RemoteOperationInput input, CancellationToken cancelToken)
|
||||
=> ExecuteTest(connection, applicationSettings, input.path, autocreate ?? false, cancelToken))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapPost("/remoteoperation/create", ([FromBody] RemoteOperationInput input, CancellationToken cancelToken)
|
||||
@@ -53,11 +53,11 @@ namespace Duplicati.WebserverCore.Endpoints.V1
|
||||
return new Dto.GetDbPathDto(!string.IsNullOrWhiteSpace(path), path);
|
||||
}
|
||||
|
||||
private static Dictionary<string, string?> ParseUrlOptions(Library.Utility.Uri uri)
|
||||
private static Dictionary<string, string?> ParseUrlOptions(Connection connection, Library.Utility.Uri uri)
|
||||
{
|
||||
var qp = uri.QueryParameters;
|
||||
|
||||
var opts = Runner.GetCommonOptions();
|
||||
var opts = Runner.GetCommonOptions(connection);
|
||||
foreach (var k in qp.Keys.Cast<string>())
|
||||
opts[k] = qp[k];
|
||||
|
||||
@@ -87,13 +87,13 @@ namespace Duplicati.WebserverCore.Endpoints.V1
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<TupleDisposeWrapper> GetBackend(string url, CancellationToken cancelToken)
|
||||
private static async Task<TupleDisposeWrapper> GetBackend(Connection connection, IApplicationSettings applicationSettings, string url, CancellationToken cancelToken)
|
||||
{
|
||||
var uri = new Library.Utility.Uri(url);
|
||||
var opts = ParseUrlOptions(uri);
|
||||
var opts = ParseUrlOptions(connection, uri);
|
||||
|
||||
var tmp = new[] { uri };
|
||||
await SecretProviderHelper.ApplySecretProviderAsync([], tmp, opts, Library.Utility.TempFolder.SystemTempPath, FIXMEGlobal.SecretProvider, cancelToken);
|
||||
await SecretProviderHelper.ApplySecretProviderAsync([], tmp, opts, Library.Utility.TempFolder.SystemTempPath, applicationSettings.SecretProvider, cancelToken);
|
||||
url = tmp[0].ToString();
|
||||
|
||||
var modules = ConfigureModules(opts);
|
||||
@@ -101,13 +101,13 @@ namespace Duplicati.WebserverCore.Endpoints.V1
|
||||
return new TupleDisposeWrapper(backend, modules);
|
||||
}
|
||||
|
||||
private static async Task ExecuteTest(string url, bool autoCreate, CancellationToken cancelToken)
|
||||
private static async Task ExecuteTest(Connection connection, IApplicationSettings applicationSettings, string url, bool autoCreate, CancellationToken cancelToken)
|
||||
{
|
||||
TupleDisposeWrapper? wrapper = null;
|
||||
|
||||
try
|
||||
{
|
||||
wrapper = await GetBackend(url, cancelToken);
|
||||
wrapper = await GetBackend(connection, applicationSettings, url, cancelToken);
|
||||
|
||||
using (var b = wrapper.Backend)
|
||||
{
|
||||
|
||||
@@ -34,57 +34,55 @@ public class Tasks : IEndpointV1
|
||||
}
|
||||
public static void Map(RouteGroupBuilder group)
|
||||
{
|
||||
group.MapGet("/tasks", Execute).RequireAuthorization();
|
||||
group.MapGet("/task/{taskid}", ([FromRoute] long taskId) => ExecuteGet(taskId)).RequireAuthorization();
|
||||
group.MapPost("/task/{taskid}/stop", ([FromRoute] long taskId) => ExecutePost(taskId, TaskStopState.Stop)).RequireAuthorization();
|
||||
group.MapPost("/task/{taskid}/abort", ([FromRoute] long taskId) => ExecutePost(taskId, TaskStopState.Abort)).RequireAuthorization();
|
||||
group.MapGet("/tasks", ([FromServices] IQueueRunnerService queueRunnerService) => Execute(queueRunnerService)).RequireAuthorization();
|
||||
group.MapGet("/task/{taskid}", ([FromRoute] long taskId, [FromServices] IQueueRunnerService queueRunnerService) => ExecuteGet(queueRunnerService, taskId)).RequireAuthorization();
|
||||
group.MapPost("/task/{taskid}/stop", ([FromRoute] long taskId, [FromServices] IQueueRunnerService queueRunnerService) => ExecutePost(queueRunnerService, taskId, TaskStopState.Stop)).RequireAuthorization();
|
||||
group.MapPost("/task/{taskid}/abort", ([FromRoute] long taskId, [FromServices] IQueueRunnerService queueRunnerService) => ExecutePost(queueRunnerService, taskId, TaskStopState.Abort)).RequireAuthorization();
|
||||
|
||||
}
|
||||
|
||||
private static IEnumerable<Server.Runner.IRunnerData> Execute()
|
||||
private static IEnumerable<Server.Runner.IRunnerData> Execute(IQueueRunnerService queueRunnerService)
|
||||
{
|
||||
var cur = FIXMEGlobal.WorkThread.CurrentTask;
|
||||
var n = FIXMEGlobal.WorkThread.CurrentTasks;
|
||||
var cur = queueRunnerService.GetCurrentTask();
|
||||
var n = queueRunnerService.GetCurrentTasks();
|
||||
|
||||
if (cur != null)
|
||||
n.Insert(0, cur);
|
||||
|
||||
return n;
|
||||
return n.OfType<Server.Runner.IRunnerData>();
|
||||
}
|
||||
|
||||
private static Dto.GetTaskStateDto ExecuteGet(long taskid)
|
||||
private static Dto.GetTaskStateDto ExecuteGet(IQueueRunnerService queueRunnerService, long taskid)
|
||||
{
|
||||
var task = FIXMEGlobal.WorkThread.CurrentTask;
|
||||
var tasks = FIXMEGlobal.WorkThread.CurrentTasks;
|
||||
var task = queueRunnerService.GetCurrentTask();
|
||||
var tasks = queueRunnerService.GetCurrentTasks();
|
||||
|
||||
if (task != null && task.TaskID == taskid)
|
||||
return new Dto.GetTaskStateDto("Running", taskid);
|
||||
return new Dto.GetTaskStateDto("Running", taskid, task.TaskStarted, task.TaskFinished);
|
||||
|
||||
if (tasks.FirstOrDefault(x => x.TaskID == taskid) == null)
|
||||
{
|
||||
KeyValuePair<long, Exception>[] matches;
|
||||
lock (FIXMEGlobal.MainLock)
|
||||
matches = FIXMEGlobal.TaskResultCache.Where(x => x.Key == taskid).ToArray();
|
||||
|
||||
if (matches.Length == 0)
|
||||
var res = queueRunnerService.GetCachedTaskResults(taskid);
|
||||
if (res == null)
|
||||
throw new NotFoundException("No such task found");
|
||||
|
||||
|
||||
return new Dto.GetTaskStateDto(
|
||||
Status: matches[0].Value == null ? "Completed" : "Failed",
|
||||
Status: res.Exception == null ? "Completed" : "Failed",
|
||||
ID: taskid,
|
||||
ErrorMessage: matches[0].Value == null ? null : matches[0].Value.Message,
|
||||
Exception: matches[0].Value == null ? null : matches[0].Value.ToString()
|
||||
TaskStarted: res?.TaskStarted,
|
||||
TaskFinished: res?.TaskFinished,
|
||||
ErrorMessage: res?.Exception?.Message,
|
||||
Exception: res?.Exception?.ToString()
|
||||
);
|
||||
}
|
||||
|
||||
return new Dto.GetTaskStateDto("Waiting", taskid);
|
||||
return new Dto.GetTaskStateDto("Waiting", taskid, null, null);
|
||||
}
|
||||
|
||||
private static void ExecutePost(long taskid, TaskStopState stopState)
|
||||
private static void ExecutePost(IQueueRunnerService queueRunnerService, long taskid, TaskStopState stopState)
|
||||
{
|
||||
var task = FIXMEGlobal.WorkThread.CurrentTask;
|
||||
var tasks = FIXMEGlobal.WorkThread.CurrentTasks;
|
||||
var task = queueRunnerService.GetCurrentTask();
|
||||
var tasks = queueRunnerService.GetCurrentTasks();
|
||||
|
||||
if (task != null)
|
||||
tasks.Insert(0, task);
|
||||
|
||||
@@ -18,8 +18,9 @@
|
||||
// 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.RestAPI;
|
||||
using Duplicati.Server;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Duplicati.WebserverCore.Endpoints.V1;
|
||||
|
||||
@@ -27,9 +28,11 @@ public class Updates : IEndpointV1
|
||||
{
|
||||
public static void Map(RouteGroupBuilder group)
|
||||
{
|
||||
group.MapPost("/updates/check", Execute).RequireAuthorization();
|
||||
group.MapPost("/updates/check",
|
||||
([FromServices] UpdatePollThread updatePollThread) => Execute(updatePollThread))
|
||||
.RequireAuthorization();
|
||||
}
|
||||
|
||||
private static void Execute()
|
||||
=> FIXMEGlobal.UpdatePoller.CheckNow();
|
||||
private static void Execute(UpdatePollThread updatePollThread)
|
||||
=> updatePollThread.CheckNow();
|
||||
}
|
||||
@@ -20,8 +20,8 @@
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using Duplicati.Library.Interface;
|
||||
using Duplicati.Library.Main;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Server;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
using Duplicati.WebserverCore.Exceptions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -33,22 +33,24 @@ public record WebModules : IEndpointV1
|
||||
public static void Map(RouteGroupBuilder group)
|
||||
{
|
||||
group.MapGet("/webmodules", ExecuteGet).RequireAuthorization();
|
||||
group.MapPost("/webmodule/{modulekey}", ([FromRoute] string modulekey, [FromBody] Dictionary<string, string> options, CancellationToken cancellationToken) => ExecutePost(modulekey, options, cancellationToken)).RequireAuthorization();
|
||||
group.MapPost("/webmodule/{modulekey}", ([FromServices] Connection connection, [FromServices] IApplicationSettings applicationSettings, [FromRoute] string modulekey, [FromBody] Dictionary<string, string> options, CancellationToken cancellationToken)
|
||||
=> ExecutePost(connection, applicationSettings, modulekey, options, cancellationToken))
|
||||
.RequireAuthorization();
|
||||
}
|
||||
|
||||
private static IEnumerable<IWebModule> ExecuteGet()
|
||||
=> Library.DynamicLoader.WebLoader.Modules;
|
||||
|
||||
private static async Task<Dto.WebModuleOutputDto> ExecutePost(string modulekey, Dictionary<string, string> inputOptions, CancellationToken cancellationToken)
|
||||
private static async Task<Dto.WebModuleOutputDto> ExecutePost(Connection connection, IApplicationSettings applicationSettings, string modulekey, Dictionary<string, string> inputOptions, CancellationToken cancellationToken)
|
||||
{
|
||||
var m = Library.DynamicLoader.WebLoader.Modules.FirstOrDefault(x => x.Key.Equals(modulekey, StringComparison.OrdinalIgnoreCase))
|
||||
?? throw new NotFoundException("No such module found");
|
||||
|
||||
var options = Runner.GetCommonOptions();
|
||||
var options = Runner.GetCommonOptions(connection);
|
||||
foreach (var k in inputOptions.Keys)
|
||||
options[k] = inputOptions[k];
|
||||
|
||||
await SecretProviderHelper.ApplySecretProviderAsync([], [], options, Library.Utility.TempFolder.SystemTempPath, FIXMEGlobal.SecretProvider, cancellationToken);
|
||||
await SecretProviderHelper.ApplySecretProviderAsync([], [], options, Library.Utility.TempFolder.SystemTempPath, applicationSettings.SecretProvider, cancellationToken);
|
||||
|
||||
return new Dto.WebModuleOutputDto(
|
||||
Status: "OK",
|
||||
|
||||
@@ -20,11 +20,13 @@
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using Duplicati.Library.Interface;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Server;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.Server.Serialization.Interface;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
using Duplicati.WebserverCore.Exceptions;
|
||||
using Duplicati.WebserverCore.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Duplicati.WebserverCore.Endpoints.V2.Backup;
|
||||
@@ -33,33 +35,33 @@ public class BackupListing : IEndpointV2
|
||||
{
|
||||
public static void Map(RouteGroupBuilder group)
|
||||
{
|
||||
group.MapPost("/backup/list-filesets", ([FromServices] Connection connection, [FromBody] Dto.V2.ListFilesetsRequestDto input)
|
||||
=> ExecuteGetFilesets(GetBackup(connection, input.BackupId)))
|
||||
group.MapPost("/backup/list-filesets", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromBody] Dto.V2.ListFilesetsRequestDto input)
|
||||
=> ExecuteGetFilesets(queueRunnerService, GetBackup(connection, input.BackupId)))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapPost("/backup/list-folder", ([FromServices] Connection connection, [FromBody] Dto.V2.ListFolderContentRequestDto input)
|
||||
=> ExecuteListFolder(GetBackup(connection, input.BackupId), input))
|
||||
group.MapPost("/backup/list-folder", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromBody] Dto.V2.ListFolderContentRequestDto input)
|
||||
=> ExecuteListFolder(queueRunnerService, GetBackup(connection, input.BackupId), input))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapPost("/backup/list-versions", ([FromServices] Connection connection, [FromBody] Dto.V2.ListFileVersionsRequestDto input)
|
||||
=> ExecuteListVersions(GetBackup(connection, input.BackupId), input))
|
||||
group.MapPost("/backup/list-versions", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromBody] Dto.V2.ListFileVersionsRequestDto input)
|
||||
=> ExecuteListVersions(queueRunnerService, GetBackup(connection, input.BackupId), input))
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapPost("/backup/search", ([FromServices] Connection connection, [FromBody] Dto.V2.SearchEntriesRequestDto input)
|
||||
=> ExecuteSearch(GetBackup(connection, input.BackupId), input))
|
||||
group.MapPost("/backup/search", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromBody] Dto.V2.SearchEntriesRequestDto input)
|
||||
=> ExecuteSearch(queueRunnerService, GetBackup(connection, input.BackupId), input))
|
||||
.RequireAuthorization();
|
||||
}
|
||||
|
||||
private static IBackup GetBackup(Connection connection, string id)
|
||||
=> connection.GetBackup(id) ?? throw new NotFoundException("Backup not found");
|
||||
|
||||
private static Dto.V2.ListFolderContentResponseDto ExecuteListFolder(IBackup bk, Dto.V2.ListFolderContentRequestDto input)
|
||||
private static Dto.V2.ListFolderContentResponseDto ExecuteListFolder(IQueueRunnerService queueRunnerService, IBackup bk, Dto.V2.ListFolderContentRequestDto input)
|
||||
{
|
||||
var time = string.IsNullOrWhiteSpace(input.Time)
|
||||
? new DateTime(0)
|
||||
: Library.Utility.Timeparser.ParseTimeInterval(input.Time, DateTime.Now);
|
||||
|
||||
var r = Runner.Run(Runner.CreateListFolderContents(bk, input.Paths, time, input.PageSize ?? 1000, input.Page ?? 0), false) as IListFolderResults;
|
||||
var r = queueRunnerService.RunImmediately(Runner.CreateListFolderContents(bk, input.Paths, time, input.PageSize ?? 1000, input.Page ?? 0)) as IListFolderResults;
|
||||
if (r == null)
|
||||
throw new ServerErrorException("No result from list operation");
|
||||
|
||||
@@ -78,16 +80,16 @@ public class BackupListing : IEndpointV2
|
||||
r.Entries.TotalCount);
|
||||
}
|
||||
|
||||
private static Dto.V2.ListFilesetsResponseDto ExecuteGetFilesets(IBackup bk)
|
||||
private static Dto.V2.ListFilesetsResponseDto ExecuteGetFilesets(IQueueRunnerService queueRunnerService, IBackup bk)
|
||||
{
|
||||
var extra = new Dictionary<string, string>();
|
||||
var extra = new Dictionary<string, string?>();
|
||||
|
||||
// Retries will hang the http request
|
||||
extra["number-of-retries"] = "0";
|
||||
|
||||
try
|
||||
{
|
||||
var r = Runner.Run(Runner.CreateListFilesetsTask(bk, extra), false) as IListFilesetResults;
|
||||
var r = queueRunnerService.RunImmediately(Runner.CreateListFilesetsTask(bk, extra)) as IListFilesetResults;
|
||||
if (r == null)
|
||||
throw new ServerErrorException("No result from list operation");
|
||||
|
||||
@@ -112,9 +114,9 @@ public class BackupListing : IEndpointV2
|
||||
}
|
||||
}
|
||||
|
||||
private static Dto.V2.ListFileVersionsOutputDto ExecuteListVersions(IBackup bk, Dto.V2.ListFileVersionsRequestDto input)
|
||||
private static Dto.V2.ListFileVersionsOutputDto ExecuteListVersions(IQueueRunnerService queueRunnerService, IBackup bk, Dto.V2.ListFileVersionsRequestDto input)
|
||||
{
|
||||
var r = Runner.Run(Runner.ListFileVersionsTask(bk, input.Paths, input.PageSize ?? 1000, input.Page ?? 0), false) as IListFileVersionsResults;
|
||||
var r = queueRunnerService.RunImmediately(Runner.ListFileVersionsTask(bk, input.Paths, input.PageSize ?? 1000, input.Page ?? 0)) as IListFileVersionsResults;
|
||||
if (r == null)
|
||||
throw new ServerErrorException("No result from list operation");
|
||||
|
||||
@@ -135,13 +137,13 @@ public class BackupListing : IEndpointV2
|
||||
r.FileVersions.TotalCount);
|
||||
}
|
||||
|
||||
private static Dto.V2.SearchEntriesResponseDto ExecuteSearch(IBackup bk, Dto.V2.SearchEntriesRequestDto input)
|
||||
private static Dto.V2.SearchEntriesResponseDto ExecuteSearch(IQueueRunnerService queueRunnerService, IBackup bk, Dto.V2.SearchEntriesRequestDto input)
|
||||
{
|
||||
var time = string.IsNullOrWhiteSpace(input.Time)
|
||||
? new DateTime(0)
|
||||
: Library.Utility.Timeparser.ParseTimeInterval(input.Time, DateTime.Now);
|
||||
|
||||
var r = Runner.Run(Runner.CreateSearchEntriesTask(bk, input.Filters, input.Paths, time, input.PageSize ?? 1000, input.Page ?? 0), false) as ISearchFilesResults;
|
||||
var r = queueRunnerService.RunImmediately(Runner.CreateSearchEntriesTask(bk, input.Filters, input.Paths, time, input.PageSize ?? 1000, input.Page ?? 0)) as ISearchFilesResults;
|
||||
if (r == null)
|
||||
throw new ServerErrorException("No result from list operation");
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
using Duplicati.Library.Interface;
|
||||
using Duplicati.Library.Main;
|
||||
using Duplicati.Library.Main.Volumes;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Server;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
@@ -35,8 +34,8 @@ public class DestinationVerify : IEndpointV2
|
||||
{
|
||||
public static void Map(RouteGroupBuilder group)
|
||||
{
|
||||
group.MapPost("/destination/test", ([FromServices] Connection connection, [FromBody] Dto.V2.DestinationTestRequestDto input, CancellationToken cancelToken)
|
||||
=> ExecuteTest(input, cancelToken))
|
||||
group.MapPost("/destination/test", ([FromServices] Connection connection, [FromServices] IApplicationSettings applicationSettings, [FromBody] Dto.V2.DestinationTestRequestDto input, CancellationToken cancelToken)
|
||||
=> ExecuteTest(connection, applicationSettings, input, cancelToken))
|
||||
.RequireAuthorization();
|
||||
}
|
||||
|
||||
@@ -51,11 +50,11 @@ public class DestinationVerify : IEndpointV2
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, string?> ParseUrlOptions(Library.Utility.Uri uri)
|
||||
private static Dictionary<string, string?> ParseUrlOptions(Connection connection, Library.Utility.Uri uri)
|
||||
{
|
||||
var qp = uri.QueryParameters;
|
||||
|
||||
var opts = Runner.GetCommonOptions();
|
||||
var opts = Runner.GetCommonOptions(connection);
|
||||
foreach (var k in qp.Keys.Cast<string>())
|
||||
opts[k] = qp[k];
|
||||
|
||||
@@ -75,13 +74,13 @@ public class DestinationVerify : IEndpointV2
|
||||
return modules;
|
||||
}
|
||||
|
||||
private static async Task<TupleDisposeWrapper> GetBackend(string url, CancellationToken cancelToken)
|
||||
private static async Task<TupleDisposeWrapper> GetBackend(Connection connection, IApplicationSettings applicationSettings, string url, CancellationToken cancelToken)
|
||||
{
|
||||
var uri = new Library.Utility.Uri(url);
|
||||
var opts = ParseUrlOptions(uri);
|
||||
var opts = ParseUrlOptions(connection, uri);
|
||||
|
||||
var tmp = new[] { uri };
|
||||
await SecretProviderHelper.ApplySecretProviderAsync([], tmp, opts, Library.Utility.TempFolder.SystemTempPath, FIXMEGlobal.SecretProvider, cancelToken);
|
||||
await SecretProviderHelper.ApplySecretProviderAsync([], tmp, opts, Library.Utility.TempFolder.SystemTempPath, applicationSettings.SecretProvider, cancelToken);
|
||||
url = tmp[0].ToString();
|
||||
|
||||
var modules = ConfigureModules(opts);
|
||||
@@ -90,13 +89,13 @@ public class DestinationVerify : IEndpointV2
|
||||
}
|
||||
|
||||
|
||||
private static async Task<DestinationTestResponseDto> ExecuteTest(DestinationTestRequestDto input, CancellationToken cancelToken)
|
||||
private static async Task<DestinationTestResponseDto> ExecuteTest(Connection connection, IApplicationSettings applicationSettings, DestinationTestRequestDto input, CancellationToken cancelToken)
|
||||
{
|
||||
TupleDisposeWrapper? wrapper = null;
|
||||
|
||||
try
|
||||
{
|
||||
wrapper = await GetBackend(input.DestinationUrl, cancelToken);
|
||||
wrapper = await GetBackend(connection, applicationSettings, input.DestinationUrl, cancelToken);
|
||||
|
||||
using (var b = wrapper.Backend)
|
||||
{
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using Duplicati.Library.IO;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Library.RestAPI.Abstractions;
|
||||
using Duplicati.Server;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.Server.Serialization;
|
||||
@@ -36,7 +35,7 @@ namespace Duplicati.WebserverCore.Extensions;
|
||||
|
||||
public static class ServiceCollectionsExtensions
|
||||
{
|
||||
public static IServiceCollection AddDuplicati(this IServiceCollection services, Connection connection)
|
||||
public static IServiceCollection AddDuplicati(this IServiceCollection services, Connection connection, ILogWriteHandler logWriteHandler, IApplicationSettings applicationSettings)
|
||||
{
|
||||
//old part
|
||||
services
|
||||
@@ -44,7 +43,6 @@ public static class ServiceCollectionsExtensions
|
||||
.AddSingleton(Serializer.JsonSettings)
|
||||
.AddSingleton<UpdatePollThread>()
|
||||
.AddSingleton<EventPollNotify>()
|
||||
.AddSingleton<Scheduler>()
|
||||
.AddSingleton(connection);
|
||||
|
||||
|
||||
@@ -57,8 +55,7 @@ public static class ServiceCollectionsExtensions
|
||||
.AddTransient<IStatusService, StatusService>()
|
||||
.AddTransient<IUpdateService, UpdateService>()
|
||||
.AddSingleton<INotificationUpdateService, NotificationUpdateService>()
|
||||
.AddSingleton<IWorkerThreadsManager, WorkerThreadsManager>()
|
||||
.AddSingleton<IScheduler, SchedulerService>()
|
||||
.AddSingleton<ISchedulerService, SchedulerService>()
|
||||
.AddSingleton<IWebsocketAccessor, WebsocketAccessor>()
|
||||
.AddTransient<ILanguageService, LanguageService>()
|
||||
.AddSingleton<ICommandlineRunService, CommandlineRunService>()
|
||||
@@ -68,7 +65,11 @@ public static class ServiceCollectionsExtensions
|
||||
.AddSingleton<IRemoteController, RemoteControllerService>()
|
||||
.AddSingleton<IRemoteControllerHandler, RemoteControllerHandler>()
|
||||
.AddSingleton<IRemoteControllerRegistration, RemoteControllerRegistrationService>()
|
||||
.AddSingleton<ISystemInfoProvider, SystemInfoProvider>();
|
||||
.AddSingleton<ISystemInfoProvider, SystemInfoProvider>()
|
||||
.AddSingleton<IQueueRunnerService, QueueRunnerService>()
|
||||
.AddSingleton<IProgressStateProviderService, ProgressStateProviderService>()
|
||||
.AddSingleton(logWriteHandler)
|
||||
.AddSingleton(applicationSettings);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,10 @@ public static class WebApplicationExtensions
|
||||
methodMap!.Invoke(null, [group]);
|
||||
}
|
||||
|
||||
// Special handling for endpoints that are not supported in all modes
|
||||
var applicationSettings = application.Services.GetRequiredService<IApplicationSettings>();
|
||||
Endpoints.V1.RemoteControl.MapConditional(group, applicationSettings.Origin);
|
||||
|
||||
return application;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
using Duplicati.Library.AutoUpdater;
|
||||
using Duplicati.Library.Interface;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
namespace Duplicati.WebserverCore.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Application settings for the Duplicati server
|
||||
/// </summary>
|
||||
public class ApplicationSettings : IApplicationSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// The application exit event
|
||||
/// </summary>
|
||||
private readonly ManualResetEvent _applicationExitEvent = new ManualResetEvent(false);
|
||||
/// <summary>
|
||||
/// The folder where Duplicati data is stored
|
||||
/// </summary>
|
||||
private readonly string _dataFolder;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ApplicationSettings"/> class.
|
||||
/// </summary>
|
||||
public ApplicationSettings()
|
||||
{
|
||||
_dataFolder = DataFolderManager.GetDataFolder(DataFolderManager.AccessMode.ReadWritePermissionSet);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool SettingsEncryptionKeyProvidedExternally { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Action? StartOrStopUsageReporter { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DataFolder => _dataFolder;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Origin { get; set; } = "Server";
|
||||
|
||||
/// <inheritdoc />
|
||||
public ManualResetEvent ApplicationExitEvent => _applicationExitEvent;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISecretProvider? SecretProvider { get; set; }
|
||||
}
|
||||
@@ -19,35 +19,27 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System.Text;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Library.RestAPI.Abstractions;
|
||||
using Duplicati.Server;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
|
||||
namespace Duplicati.WebserverCore.Services;
|
||||
|
||||
public class CommandlineRunService(IWorkerThreadsManager workerThreadsManager) : ICommandlineRunService
|
||||
public class CommandlineRunService(IQueueRunnerService queueRunnerService, ILogWriteHandler logWriteHandler) : ICommandlineRunService
|
||||
{
|
||||
private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType<CommandlineRunService>();
|
||||
|
||||
private class LogWriter : TextWriter
|
||||
private class LogWriter(ActiveRun target, ILogWriteHandler logWriteHandler) : TextWriter
|
||||
{
|
||||
private readonly ActiveRun m_target;
|
||||
private readonly StringBuilder m_sb = new StringBuilder();
|
||||
private readonly StringBuilder sb = new StringBuilder();
|
||||
private int m_newlinechars = 0;
|
||||
|
||||
public LogWriter(ActiveRun target)
|
||||
{
|
||||
m_target = target;
|
||||
}
|
||||
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
|
||||
public override void Write(char value)
|
||||
{
|
||||
lock (m_target.Lock)
|
||||
lock (target.Lock)
|
||||
{
|
||||
m_sb.Append(value);
|
||||
sb.Append(value);
|
||||
if (NewLine[m_newlinechars] == value)
|
||||
{
|
||||
m_newlinechars++;
|
||||
@@ -62,34 +54,34 @@ public class CommandlineRunService(IWorkerThreadsManager workerThreadsManager) :
|
||||
public override void WriteLine(string? value)
|
||||
{
|
||||
value ??= string.Empty;
|
||||
lock (m_target.Lock)
|
||||
lock (target.Lock)
|
||||
{
|
||||
m_target.LastAccess = DateTime.Now;
|
||||
target.LastAccess = DateTime.Now;
|
||||
|
||||
//Avoid writing the log if it does not exist
|
||||
if (m_target.IsLogDisposed)
|
||||
if (target.IsLogDisposed)
|
||||
{
|
||||
FIXMEGlobal.LogHandler.WriteMessage(new Library.Logging.LogEntry("Attempted to write message after closing: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", null));
|
||||
logWriteHandler.WriteMessage(new Library.Logging.LogEntry("Attempted to write message after closing: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", null));
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (m_sb.Length != 0)
|
||||
if (sb.Length != 0)
|
||||
{
|
||||
m_target.Log.Add(m_sb + value);
|
||||
m_sb.Length = 0;
|
||||
target.Log.Add(sb + value);
|
||||
sb.Length = 0;
|
||||
m_newlinechars = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_target.Log.Add(value);
|
||||
target.Log.Add(value);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// This can happen on a very unlucky race where IsLogDisposed is set right after the check
|
||||
FIXMEGlobal.LogHandler.WriteMessage(new Library.Logging.LogEntry("Failed to forward commandline message: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", ex));
|
||||
logWriteHandler.WriteMessage(new Library.Logging.LogEntry("Failed to forward commandline message: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,7 +133,7 @@ public class CommandlineRunService(IWorkerThreadsManager workerThreadsManager) :
|
||||
public string StartTask(string[] args)
|
||||
{
|
||||
var k = new ActiveRun();
|
||||
k.Writer = new LogWriter(k);
|
||||
k.Writer = new LogWriter(k, logWriteHandler);
|
||||
|
||||
m_activeItems[k.ID] = k;
|
||||
StartCleanupTask();
|
||||
@@ -180,7 +172,7 @@ public class CommandlineRunService(IWorkerThreadsManager workerThreadsManager) :
|
||||
}
|
||||
});
|
||||
|
||||
workerThreadsManager.AddTask(k.Task);
|
||||
queueRunnerService.AddTask(k.Task);
|
||||
return k.ID;
|
||||
}
|
||||
|
||||
|
||||
+10
-36
@@ -18,44 +18,18 @@
|
||||
// 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.
|
||||
#nullable enable
|
||||
using Duplicati.Library.IO;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Library.RestAPI.Abstractions;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Server;
|
||||
using Duplicati.Server.Serialization.Interface;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
|
||||
namespace Duplicati.WebserverCore.Services;
|
||||
|
||||
public class WorkerThreadsManager(ILiveControls liveControls, IScheduler scheduler) : IWorkerThreadsManager
|
||||
/// <summary>
|
||||
/// Provides a way to generate progress state.
|
||||
/// </summary>
|
||||
public class ProgressStateProviderService : IProgressStateProviderService
|
||||
{
|
||||
public WorkerThread<Runner.IRunnerData>? WorkerThread { get; private set; }
|
||||
|
||||
public void Spawn(Action<Runner.IRunnerData> item)
|
||||
{
|
||||
WorkerThread = new WorkerThread<Runner.IRunnerData>(item, liveControls.IsPaused);
|
||||
scheduler.Init(WorkerThread);
|
||||
}
|
||||
|
||||
public Tuple<long, string>? CurrentTask
|
||||
{
|
||||
get
|
||||
{
|
||||
var t = WorkerThread?.CurrentTask;
|
||||
return t == null ? null : new Tuple<long, string>(t.TaskID, t.Backup.ID);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateThrottleSpeeds(string? uploadSpeed, string? downloadSpeed)
|
||||
{
|
||||
WorkerThread?.CurrentTask?.UpdateThrottleSpeed(uploadSpeed, downloadSpeed);
|
||||
}
|
||||
|
||||
public long AddTask(Runner.IRunnerData data, bool skipQueue = false)
|
||||
{
|
||||
WorkerThread!.AddTask(data, skipQueue);
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
return data.TaskID;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// The current progress state generator function.
|
||||
/// </summary>
|
||||
public Func<IProgressEventData>? GenerateProgressState { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
// 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.Interface;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Server;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.Server.Serialization.Interface;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
|
||||
namespace Duplicati.WebserverCore.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Simple queue that will run the given task
|
||||
/// </summary>
|
||||
public class QueueRunnerService(
|
||||
Connection connection,
|
||||
EventPollNotify eventPollNotify,
|
||||
INotificationUpdateService notificationUpdateService,
|
||||
IProgressStateProviderService progressStateProviderService,
|
||||
IApplicationSettings applicationSettings) : IQueueRunnerService
|
||||
{
|
||||
private readonly object _lock = new();
|
||||
/// <summary>
|
||||
/// A thread-safe dictionary to store cached task results.
|
||||
/// </summary>
|
||||
private readonly Dictionary<long, CachedTaskResult> _taskCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of completed task results to keep in memory
|
||||
/// </summary>
|
||||
private static readonly int MAX_TASK_RESULT_CACHE_SIZE = 100;
|
||||
|
||||
private readonly List<IQueuedTask> _tasks = new();
|
||||
private (Task? Task, IQueuedTask? QueuedTask) _current;
|
||||
private bool _isPaused;
|
||||
private bool _isTerminated;
|
||||
|
||||
public long AddTask(IQueuedTask task)
|
||||
=> AddTask(task, false);
|
||||
|
||||
public long AddTask(IQueuedTask task, bool skipQueue)
|
||||
{
|
||||
lock (_lock)
|
||||
if (skipQueue)
|
||||
_tasks.Insert(0, task);
|
||||
else
|
||||
_tasks.Add(task);
|
||||
|
||||
eventPollNotify.SignalNewEvent();
|
||||
StartNextTask();
|
||||
return task.TaskID;
|
||||
}
|
||||
|
||||
public bool GetIsActive()
|
||||
=> _current.Task != null;
|
||||
|
||||
public IQueuedTask? GetCurrentTask()
|
||||
=> _current.QueuedTask;
|
||||
|
||||
public List<IQueuedTask> GetCurrentTasks()
|
||||
{
|
||||
lock (_lock)
|
||||
return [.. _tasks];
|
||||
}
|
||||
|
||||
public void Pause()
|
||||
{
|
||||
lock (_lock)
|
||||
_isPaused = true;
|
||||
}
|
||||
|
||||
public void Resume()
|
||||
{
|
||||
lock (_lock)
|
||||
_isPaused = false;
|
||||
|
||||
StartNextTask();
|
||||
}
|
||||
|
||||
public void Terminate(bool wait)
|
||||
{
|
||||
_isTerminated = true;
|
||||
if (wait)
|
||||
{
|
||||
var task = _current.Task;
|
||||
if (task != null)
|
||||
task.Await();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartNextTask()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_isTerminated || _isPaused || (_current.Task != null && !_current.Task.IsCompleted))
|
||||
return;
|
||||
|
||||
// Clean up completed tasks
|
||||
if (_current.Task != null && _current.Task.IsCompleted)
|
||||
_current = (null, null);
|
||||
|
||||
if (_tasks.Count == 0)
|
||||
return;
|
||||
|
||||
var nextTask = _tasks[0];
|
||||
_tasks.RemoveAt(0);
|
||||
_current = (Task.Run(() => RunTask(nextTask), CancellationToken.None), nextTask);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunTask(IQueuedTask task)
|
||||
{
|
||||
var completed = false;
|
||||
try
|
||||
{
|
||||
eventPollNotify.SignalNewEvent();
|
||||
task.TaskStarted = DateTime.UtcNow;
|
||||
if (task.OnStarting != null)
|
||||
await task.OnStarting().ConfigureAwait(false);
|
||||
|
||||
Runner.Run(connection, eventPollNotify, notificationUpdateService, progressStateProviderService, applicationSettings, task, true);
|
||||
|
||||
// If the task is completed, don't call OnFinished again
|
||||
completed = true;
|
||||
AddTaskResult(new CachedTaskResult(task.TaskID, task.BackupID, task.TaskStarted, task.TaskFinished ?? DateTime.Now, null));
|
||||
if (task.OnFinished != null)
|
||||
await task.OnFinished(null).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
connection.LogError(task.BackupID, "Error in worker", ex);
|
||||
if (!completed)
|
||||
{
|
||||
AddTaskResult(new CachedTaskResult(task.TaskID, task.BackupID, task.TaskStarted, task.TaskFinished ?? DateTime.Now, ex));
|
||||
if (task.OnFinished != null)
|
||||
await task.OnFinished(ex).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
task.TaskFinished = DateTime.UtcNow;
|
||||
lock (_lock)
|
||||
_current = (null, null);
|
||||
eventPollNotify.SignalNewEvent();
|
||||
StartNextTask();
|
||||
}
|
||||
}
|
||||
|
||||
public IList<Tuple<long, string?>> GetQueueWithIds()
|
||||
{
|
||||
return (from n in GetCurrentTasks()
|
||||
where n.BackupID != null
|
||||
select new Tuple<long, string?>(n.TaskID, n.BackupID)).ToList();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CachedTaskResult? GetCachedTaskResults(long taskID)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_taskCache.TryGetValue(taskID, out var result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddTaskResult(CachedTaskResult taskResult)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// If the task result is already in the cache, remove it
|
||||
if (_taskCache.TryGetValue(taskResult.TaskID, out var existingResult))
|
||||
{
|
||||
// If the stored task result has an exception, do not overwrite it
|
||||
if (existingResult.Exception != null)
|
||||
return;
|
||||
}
|
||||
|
||||
// Add/update the new task result in the cache
|
||||
_taskCache[taskResult.TaskID] = taskResult;
|
||||
|
||||
// If the cache size exceeds the maximum, remove the oldest entry
|
||||
while (_taskCache.Count >= MAX_TASK_RESULT_CACHE_SIZE)
|
||||
{
|
||||
var oldestTaskID = _taskCache.Keys.Min();
|
||||
_taskCache.Remove(oldestTaskID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IBasicResults? RunImmediately(IQueuedTask task)
|
||||
{
|
||||
return Runner.Run(connection, eventPollNotify, notificationUpdateService, progressStateProviderService, applicationSettings, task, false);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@
|
||||
using System.Net.Http.Headers;
|
||||
using Duplicati.Library.Logging;
|
||||
using Duplicati.Library.RemoteControl;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
using Newtonsoft.Json;
|
||||
@@ -35,7 +34,7 @@ namespace Duplicati.WebserverCore.Services;
|
||||
/// <param name="connection">The connection to the database</param>
|
||||
/// <param name="httpClientFactory">The HTTP client factory</param>
|
||||
/// <param name="jwtTokenProvider">The JWT token provider</param>
|
||||
public class RemoteControllerHandler(Connection connection, IHttpClientFactory httpClientFactory, IJWTTokenProvider jwtTokenProvider) : IRemoteControllerHandler
|
||||
public class RemoteControllerHandler(Connection connection, IHttpClientFactory httpClientFactory, IJWTTokenProvider jwtTokenProvider, IApplicationSettings applicationSettings) : IRemoteControllerHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// The log tag for this class.
|
||||
@@ -64,7 +63,7 @@ public class RemoteControllerHandler(Connection connection, IHttpClientFactory h
|
||||
CertificateUrl = data.CertificateUrl
|
||||
});
|
||||
|
||||
if (!FIXMEGlobal.SettingsEncryptionKeyProvidedExternally)
|
||||
if (!applicationSettings.SettingsEncryptionKeyProvidedExternally)
|
||||
{
|
||||
// TODO: Implement changing the encryption key
|
||||
// if (!string.IsNullOrWhiteSpace(data.LocalEncryptionKey) && data.LocalEncryptionKey != connection.ApplicationSettings.SettingsEncryptionKey)
|
||||
@@ -86,10 +85,10 @@ public class RemoteControllerHandler(Connection connection, IHttpClientFactory h
|
||||
try
|
||||
{
|
||||
if (string.Equals(message.ControlRequestMessage.Command, ControlRequestMessage.ConfigureReportUrlSet, StringComparison.OrdinalIgnoreCase))
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.AdditionalReportUrl = message.ControlRequestMessage.Parameters.GetValueOrDefault(ControlRequestMessage.ConfigureReportUrlParameter);
|
||||
connection.ApplicationSettings.AdditionalReportUrl = message.ControlRequestMessage.Parameters.GetValueOrDefault(ControlRequestMessage.ConfigureReportUrlParameter);
|
||||
|
||||
if (string.Equals(message.ControlRequestMessage.Command, ControlRequestMessage.ConfigureReportUrlGet, StringComparison.OrdinalIgnoreCase))
|
||||
result = new Dictionary<string, string?> { { ControlRequestMessage.ConfigureReportUrlParameter, FIXMEGlobal.DataConnection.ApplicationSettings.AdditionalReportUrl } };
|
||||
result = new Dictionary<string, string?> { { ControlRequestMessage.ConfigureReportUrlParameter, connection.ApplicationSettings.AdditionalReportUrl } };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -19,31 +19,33 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Server;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.Server.Serialization.Interface;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
|
||||
namespace WebserverCore.Services;
|
||||
|
||||
public class SchedulerService : IScheduler
|
||||
public class SchedulerService : ISchedulerService
|
||||
{
|
||||
private readonly Duplicati.Server.Scheduler scheduler;
|
||||
public SchedulerService(Duplicati.Server.Scheduler scheduler)
|
||||
private readonly Scheduler scheduler;
|
||||
public SchedulerService(Connection connection, EventPollNotify eventPollNotify, INotificationUpdateService notificationUpdateService, IQueueRunnerService queueRunnerService)
|
||||
{
|
||||
this.scheduler = scheduler;
|
||||
this.scheduler = new Scheduler(connection, queueRunnerService);
|
||||
var lastScheduleId = notificationUpdateService.LastDataUpdateId;
|
||||
eventPollNotify.NewEvent += (sender, e) =>
|
||||
{
|
||||
if (lastScheduleId != notificationUpdateService.LastDataUpdateId)
|
||||
{
|
||||
lastScheduleId = notificationUpdateService.LastDataUpdateId;
|
||||
Reschedule();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public List<KeyValuePair<DateTime, ISchedule>> Schedule => scheduler.Schedule;
|
||||
|
||||
public List<Runner.IRunnerData> WorkerQueue => scheduler.WorkerQueue;
|
||||
|
||||
public void SubScribeToNewSchedule(Action handler)
|
||||
=> scheduler.NewSchedule += (_, _) => handler();
|
||||
|
||||
public IList<Tuple<long, string>> GetSchedulerQueueIds()
|
||||
=> scheduler.GetSchedulerQueueIds();
|
||||
|
||||
public IList<Tuple<string, DateTime>> GetProposedSchedule()
|
||||
=> scheduler.GetProposedSchedule();
|
||||
|
||||
@@ -52,7 +54,4 @@ public class SchedulerService : IScheduler
|
||||
|
||||
public void Terminate(bool wait)
|
||||
=> scheduler.Terminate(wait);
|
||||
|
||||
public void Init(WorkerThread<Runner.IRunnerData> worker)
|
||||
=> scheduler.Init(worker);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Library.RestAPI.Abstractions;
|
||||
using Duplicati.Server;
|
||||
using Duplicati.Server.Serialization;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
@@ -31,22 +30,24 @@ public class StatusService(
|
||||
LiveControls liveControls,
|
||||
UpdatePollThread updatePollThread,
|
||||
IUpdateService updateService,
|
||||
IWorkerThreadsManager workerThreadsManager,
|
||||
IQueueRunnerService queueRunnerService,
|
||||
ISettingsService settingsService,
|
||||
IScheduler scheduler,
|
||||
ISchedulerService scheduler,
|
||||
EventPollNotify eventPollNotify,
|
||||
INotificationUpdateService notificationUpdateService)
|
||||
: IStatusService
|
||||
{
|
||||
public ServerStatusDto GetStatus()
|
||||
{
|
||||
var task = queueRunnerService.GetCurrentTask();
|
||||
|
||||
var status = new ServerStatusDto
|
||||
{
|
||||
UpdatedVersion = GetUpdatedVersion(),
|
||||
UpdaterState = updatePollThread.ThreadState,
|
||||
UpdateDownloadProgress = updatePollThread.DownloadProgess,
|
||||
ActiveTask = workerThreadsManager.CurrentTask,
|
||||
SchedulerQueueIds = scheduler.GetSchedulerQueueIds(),
|
||||
ActiveTask = task == null ? null : new Tuple<long, string?>(task.TaskID, task.BackupID),
|
||||
SchedulerQueueIds = queueRunnerService.GetQueueWithIds(),
|
||||
ProposedSchedule = scheduler.GetProposedSchedule(),
|
||||
LastEventID = eventPollNotify.EventNo,
|
||||
LastDataUpdateID = notificationUpdateService.LastDataUpdateId,
|
||||
@@ -72,7 +73,8 @@ public class StatusService(
|
||||
|
||||
private SuggestedStatusIcon MapStateToIcon()
|
||||
{
|
||||
if (workerThreadsManager.CurrentTask == null)
|
||||
var task = queueRunnerService.GetCurrentTask();
|
||||
if (task == null)
|
||||
{
|
||||
if (liveControls.State == LiveControls.LiveControlState.Paused)
|
||||
return SuggestedStatusIcon.Paused;
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
using System.Globalization;
|
||||
using Duplicati.Library.AutoUpdater;
|
||||
using Duplicati.Library.Localization;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Server;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.Server.Serialization.Interface;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
using Duplicati.WebserverCore.Dto;
|
||||
@@ -32,7 +32,7 @@ namespace Duplicati.WebserverCore.Services;
|
||||
/// <summary>
|
||||
/// Produces system information.
|
||||
/// </summary>
|
||||
public class SystemInfoProvider : ISystemInfoProvider
|
||||
public class SystemInfoProvider(IApplicationSettings applicationSettings) : ISystemInfoProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// The API extensions that are available
|
||||
@@ -42,7 +42,9 @@ public class SystemInfoProvider : ISystemInfoProvider
|
||||
"v2:backup:list-folder",
|
||||
"v2:backup:list-versions",
|
||||
"v2:backup:search",
|
||||
"v2:destination:test"
|
||||
"v2:destination:test",
|
||||
"v1:gettask:taskstarted",
|
||||
"v1:gettask:taskfinished",
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
@@ -85,11 +87,6 @@ public class SystemInfoProvider : ISystemInfoProvider
|
||||
/// </summary>
|
||||
public required string RemoteControlRegistrationUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the started by.
|
||||
/// </summary>
|
||||
public required string StartedBy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default update channel.
|
||||
/// </summary>
|
||||
@@ -229,12 +226,11 @@ public class SystemInfoProvider : ISystemInfoProvider
|
||||
=> new StaticSystemInformation
|
||||
{
|
||||
APIVersion = 1,
|
||||
PasswordPlaceholder = FIXMEGlobal.PASSWORD_PLACEHOLDER,
|
||||
PasswordPlaceholder = Connection.PASSWORD_PLACEHOLDER,
|
||||
ServerVersion = UpdaterManager.SelfVersion.Version,
|
||||
ServerVersionName = License.VersionNumbers.VERSION_NAME,
|
||||
ServerVersionType = UpdaterManager.SelfVersion.ReleaseType,
|
||||
RemoteControlRegistrationUrl = Library.RemoteControl.RegisterForRemote.DefaultRegisterationUrl,
|
||||
StartedBy = FIXMEGlobal.Origin,
|
||||
DefaultUpdateChannel = AutoUpdateSettings.DefaultUpdateChannel.ToString(),
|
||||
DefaultUsageReportLevel = Library.UsageReporter.Reporter.DefaultReportLevel,
|
||||
OSType = UpdaterManager.OperatingSystemName,
|
||||
@@ -313,7 +309,7 @@ public class SystemInfoProvider : ISystemInfoProvider
|
||||
ServerVersionName = systeminfo.ServerVersionName,
|
||||
ServerVersionType = systeminfo.ServerVersionType,
|
||||
RemoteControlRegistrationUrl = systeminfo.RemoteControlRegistrationUrl,
|
||||
StartedBy = systeminfo.StartedBy,
|
||||
StartedBy = applicationSettings.Origin,
|
||||
DefaultUpdateChannel = systeminfo.DefaultUpdateChannel,
|
||||
DefaultUsageReportLevel = systeminfo.DefaultUsageReportLevel,
|
||||
ServerTime = DateTime.Now,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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.
|
||||
|
||||
namespace Duplicati.Backend.Tests.OneDrive;
|
||||
|
||||
/// <summary>
|
||||
/// OneDrive Tests
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public sealed class OneDriveTests : BaseTest
|
||||
{
|
||||
[TestMethod]
|
||||
public Task TestOneDrive()
|
||||
{
|
||||
CheckRequiredEnvironment(["TESTCREDENTIAL_ONEDRIVE_FOLDER", "TESTCREDENTIAL_ONEDRIVE_AUTHID"]);
|
||||
|
||||
var exitCode = CommandLine.BackendTester.Program.Main(
|
||||
new[]
|
||||
{
|
||||
$"onedrivev2://{Environment.GetEnvironmentVariable("TESTCREDENTIAL_ONEDRIVE_FOLDER")}/?authid={Uri.EscapeDataString(Environment.GetEnvironmentVariable("TESTCREDENTIAL_ONEDRIVE_AUTHID")!)}",
|
||||
|
||||
}.Concat(Parameters.GlobalTestParameters).ToArray());
|
||||
|
||||
if (exitCode != 0) Assert.Fail("BackendTester is returning non-zero exit code, check logs for details");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user