Merge remote-tracking branch 'origin/master' into feature/remote-control-support
This commit is contained in:
@@ -11,6 +11,7 @@ Duplicati/Localization/*.new.csv
|
||||
Duplicati/Localization/*.updated.csv
|
||||
Duplicati/Localization/report.*.xml
|
||||
Duplicati/Server/webroot/extjs-*
|
||||
Duplicati/Server/webroot/*/node_modules/
|
||||
packages
|
||||
changelog-news.txt
|
||||
.idea/
|
||||
|
||||
@@ -21,11 +21,11 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Duplicati.Library.AutoUpdater;
|
||||
using Duplicati.Library.Common;
|
||||
using FilterGroup = Duplicati.Library.Utility.FilterGroup;
|
||||
|
||||
namespace Duplicati.CommandLine
|
||||
@@ -88,8 +88,8 @@ namespace Duplicati.CommandLine
|
||||
//Force translation off if this is from the commandline
|
||||
if (Program.FROM_COMMANDLINE)
|
||||
{
|
||||
System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture;
|
||||
System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.InvariantCulture;
|
||||
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
|
||||
CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.NamingConventionBinder;
|
||||
|
||||
namespace Duplicati.CommandLine.ServerUtil.Commands;
|
||||
|
||||
public static class Health
|
||||
{
|
||||
public static Command Create() =>
|
||||
new Command("health", "Checks the server health endpoint")
|
||||
.WithHandler(CommandHandler.Create<Settings>(async (settings) =>
|
||||
{
|
||||
using var client = new HttpClient(new HttpClientHandler()
|
||||
{
|
||||
ServerCertificateCustomValidationCallback = settings.Insecure
|
||||
? HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
||||
: null
|
||||
})
|
||||
{
|
||||
BaseAddress = settings.HostUrl,
|
||||
Timeout = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.GetAsync("health");
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
Console.WriteLine("Server is healthy");
|
||||
return 0;
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
Console.WriteLine("Server is unhealthy");
|
||||
return 1;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>Duplicati.CommandLine.ServerUtil.Implementation</AssemblyName>
|
||||
<Copyright>Copyright © 2024 Team Duplicati, MIT license</Copyright>
|
||||
<DefaultNamespace>Duplicati.CommandLine.ServerUtil</DefaultNamespace>
|
||||
<RootNamespace>Duplicati.CommandLine.ServerUtil</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -30,6 +30,7 @@ public static class Program
|
||||
Logout.Create(),
|
||||
Import.Create(),
|
||||
Export.Create(),
|
||||
Health.Create(),
|
||||
};
|
||||
|
||||
rootCmd = SettingsBinder.AddGlobalOptions(rootCmd);
|
||||
|
||||
@@ -51,8 +51,8 @@ namespace Duplicati.GUI.TrayIcon
|
||||
catch (Exception ex)
|
||||
{
|
||||
m_runnerException = ex;
|
||||
Duplicati.Server.Program.ServerStartedEvent.Set();
|
||||
Duplicati.Server.Program.ApplicationExitEvent.Set();
|
||||
Duplicati.Server.Program.ServerStartedEvent?.Set();
|
||||
Duplicati.Server.Program.ApplicationExitEvent?.Set();
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -25,6 +25,7 @@ using System.IO;
|
||||
using System.Net;
|
||||
using Duplicati.Library.Interface;
|
||||
using Duplicati.Server;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace Duplicati.GUI.TrayIcon
|
||||
{
|
||||
@@ -107,11 +108,17 @@ namespace Duplicati.GUI.TrayIcon
|
||||
Server.Program.Origin = "Tray icon";
|
||||
hosted = new HostedInstanceKeeper(_args);
|
||||
}
|
||||
catch (Server.SingleInstance.MultipleInstanceException)
|
||||
catch (Exception ex)
|
||||
{
|
||||
return 1;
|
||||
if (ex.InnerException != null && ex.InnerException is Server.SingleInstance.MultipleInstanceException)
|
||||
{
|
||||
Console.WriteLine(Server.Strings.Program.AnotherInstanceDetected);
|
||||
return 1;
|
||||
}
|
||||
else
|
||||
throw;
|
||||
}
|
||||
|
||||
|
||||
// We have a hosted server, if this is the first run,
|
||||
// we should open the main page
|
||||
openui = Server.Program.IsFirstRun || Server.Program.ServerPortChanged;
|
||||
|
||||
@@ -354,7 +354,7 @@ namespace Duplicati.Library.AutoUpdater
|
||||
return null;
|
||||
|
||||
// Don't install a debug update on a release build and vice versa
|
||||
if (string.Equals(SelfVersion.ReleaseType, "Debug", StringComparison.OrdinalIgnoreCase) && !string.Equals(update.ReleaseType, SelfVersion.ReleaseType, StringComparison.CurrentCultureIgnoreCase))
|
||||
if (string.Equals(SelfVersion.ReleaseType, "Debug", StringComparison.OrdinalIgnoreCase) && !string.Equals(update.ReleaseType, SelfVersion.ReleaseType, StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
ReleaseType rt;
|
||||
|
||||
@@ -574,7 +574,7 @@ namespace Duplicati.Library.Main
|
||||
m_resetPriority = null;
|
||||
}
|
||||
|
||||
if(m_localeChange != null)
|
||||
if (m_localeChange != null)
|
||||
{
|
||||
m_localeChange.Dispose();
|
||||
m_localeChange = null;
|
||||
@@ -1043,7 +1043,7 @@ namespace Duplicati.Library.Main
|
||||
{
|
||||
bool found = false;
|
||||
foreach (string v in arg.ValidValues ?? new string[0])
|
||||
if (string.Equals(v, value, StringComparison.CurrentCultureIgnoreCase))
|
||||
if (string.Equals(v, value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
@@ -1061,7 +1061,7 @@ namespace Duplicati.Library.Main
|
||||
|
||||
foreach (var flag in flags)
|
||||
{
|
||||
if (!validFlags.Any(validFlag => string.Equals(validFlag, flag, StringComparison.CurrentCultureIgnoreCase)))
|
||||
if (!validFlags.Any(validFlag => string.Equals(validFlag, flag, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
validatedAllFlags = false;
|
||||
break;
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Duplicati.Library.Main.Operation
|
||||
{
|
||||
@@ -49,8 +50,8 @@ namespace Duplicati.Library.Main.Operation
|
||||
yield return string.Format("Machinename: {0}", Environment.MachineName);
|
||||
yield return string.Format("Processors: {0}", Environment.ProcessorCount);
|
||||
yield return string.Format(".Net Version: {0}", Environment.Version);
|
||||
yield return string.Format("Locale: {0}, {1}, {2}", System.Threading.Thread.CurrentThread.CurrentCulture, System.Threading.Thread.CurrentThread.CurrentUICulture, System.Globalization.CultureInfo.InstalledUICulture);
|
||||
yield return string.Format("Date/time strings: {0} - {1}", System.Threading.Thread.CurrentThread.CurrentCulture.DateTimeFormat.LongDatePattern, System.Threading.Thread.CurrentThread.CurrentCulture.DateTimeFormat.LongTimePattern);
|
||||
yield return string.Format("Locale: {0}, {1}, {2}", CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture, CultureInfo.InstalledUICulture);
|
||||
yield return string.Format("Date/time strings: {0} - {1}", CultureInfo.CurrentCulture.DateTimeFormat.LongDatePattern, CultureInfo.CurrentCulture.DateTimeFormat.LongTimePattern);
|
||||
yield return string.Format("Tempdir: {0}", Library.Utility.TempFolder.SystemTempPath);
|
||||
|
||||
Type sqlite = null;
|
||||
|
||||
@@ -610,20 +610,18 @@ namespace Duplicati.Library.Main
|
||||
/// <summary>
|
||||
/// Gets the forced locale for the current user
|
||||
/// </summary>
|
||||
public System.Globalization.CultureInfo ForcedLocale
|
||||
public CultureInfo ForcedLocale
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!m_options.ContainsKey("force-locale"))
|
||||
return System.Threading.Thread.CurrentThread.CurrentCulture;
|
||||
else
|
||||
{
|
||||
var localestring = m_options["force-locale"];
|
||||
if (string.IsNullOrWhiteSpace(localestring))
|
||||
return System.Globalization.CultureInfo.InvariantCulture;
|
||||
else
|
||||
return new System.Globalization.CultureInfo(localestring);
|
||||
}
|
||||
return CultureInfo.CurrentCulture;
|
||||
|
||||
var localestring = m_options["force-locale"];
|
||||
if (string.IsNullOrWhiteSpace(localestring))
|
||||
return CultureInfo.InvariantCulture;
|
||||
|
||||
return new CultureInfo(localestring);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,79 +4,80 @@ using System.Linq;
|
||||
|
||||
namespace Duplicati.Server.Strings
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
public static string AnotherInstanceDetected { get { return LC.L(@"Another instance is running, and was notified"); } }
|
||||
public static string DatabaseOpenError(string message) { return LC.L(@"Failed to create, open or upgrade the database.
|
||||
public static class Program
|
||||
{
|
||||
public static string AnotherInstanceDetected { get { return LC.L(@"Another instance is running, and was notified"); } }
|
||||
public static string DatabaseOpenError(string message) { return LC.L(@"Failed to create, open or upgrade the database.
|
||||
Error message: {0}", message); }
|
||||
public static string HelpCommandDescription { get { return LC.L(@"Display this help"); } }
|
||||
public static string HelpDisplayDialog { get { return LC.L(@"Supported commandline arguments:
|
||||
public static string HelpCommandDescription { get { return LC.L(@"Display this help"); } }
|
||||
public static string HelpDisplayDialog { get { return LC.L(@"Supported commandline arguments:
|
||||
|
||||
"); } }
|
||||
public static string HelpDisplayFormat(string optionname, string optiontext) { return LC.L(@"--{0}: {1}", optionname, optiontext); }
|
||||
public static string ParametersFileOptionLong2 { get { return LC.L(@"Use this option to store some or all of the options given to the commandline client. The file must be a plain text file, and UTF-8 encoding is preferred. Each line in the file should be of the format --option=value. Use the special options --{0} and --{1} to override the localpath and the remote destination uri, respectively. The options in this file take precedence over the options provided on the commandline. You cannot specify filters in both the file and on the commandline. Instead, you can use the special --{2}, --{3}, or --{4} options to specify filters inside the parameter file. Each filter must be prefixed with either a + or a -, and multiple filters must be joined with {5} ", "source", "target", "replace-filter", "append-filter", "prepend-filter", System.IO.Path.PathSeparator); } }
|
||||
public static string ParametersFileOptionShort { get { return LC.L(@"Path to a file with parameters"); } }
|
||||
public static string FiltersCannotBeUsedWithFileError2 { get { return LC.L(@"Filters cannot be specified on the commandline if filters are also present in the parameter file. Use the special --{0}, --{1}, or --{2} options to specify filters inside the parameter file. Each filter must be prefixed with either a + or a -, and multiple filters must be joined with {3}", "replace-filter", "append-filter", "prepend-filter", System.IO.Path.PathSeparator); } }
|
||||
public static string FailedToParseParametersFileError(string path, string message) { return LC.L(@"Unable to read the parameters file ""{0}"", reason: {1}", path, message); }
|
||||
public static string SkippingSourceArgumentsOnNonBackupOperation { get { return @"The --source argument was specified in the parameter file, but the current operation is not a backup operation, so the argument is ignored"; } }
|
||||
public static string LogfileCommandDescription { get { return LC.L(@"Output log information to the file given"); } }
|
||||
public static string LoglevelCommandDescription { get { return LC.L(@"Determine the amount of information written in the log file"); } }
|
||||
public static string PortablemodeCommandDescription { get { return LC.L(@"Activate portable mode where the database is placed below the program executable"); } }
|
||||
public static string SeriousError(string message) { return LC.L(@"A serious error occurred in Duplicati: {0}", message); }
|
||||
public static string TearDownError(string message) { return LC.L(@"An error occurred on server tear down: {0}", message); }
|
||||
public static string StartupFailure(System.Exception error) { return LC.L(@"Unable to start up. Perhaps another process is already running?
|
||||
public static string HelpDisplayFormat(string optionname, string optiontext) { return LC.L(@"--{0}: {1}", optionname, optiontext); }
|
||||
public static string ParametersFileOptionLong2 { get { return LC.L(@"Use this option to store some or all of the options given to the commandline client. The file must be a plain text file, and UTF-8 encoding is preferred. Each line in the file should be of the format --option=value. Use the special options --{0} and --{1} to override the localpath and the remote destination uri, respectively. The options in this file take precedence over the options provided on the commandline. You cannot specify filters in both the file and on the commandline. Instead, you can use the special --{2}, --{3}, or --{4} options to specify filters inside the parameter file. Each filter must be prefixed with either a + or a -, and multiple filters must be joined with {5} ", "source", "target", "replace-filter", "append-filter", "prepend-filter", System.IO.Path.PathSeparator); } }
|
||||
public static string ParametersFileOptionShort { get { return LC.L(@"Path to a file with parameters"); } }
|
||||
public static string FiltersCannotBeUsedWithFileError2 { get { return LC.L(@"Filters cannot be specified on the commandline if filters are also present in the parameter file. Use the special --{0}, --{1}, or --{2} options to specify filters inside the parameter file. Each filter must be prefixed with either a + or a -, and multiple filters must be joined with {3}", "replace-filter", "append-filter", "prepend-filter", System.IO.Path.PathSeparator); } }
|
||||
public static string FailedToParseParametersFileError(string path, string message) { return LC.L(@"Unable to read the parameters file ""{0}"", reason: {1}", path, message); }
|
||||
public static string SkippingSourceArgumentsOnNonBackupOperation { get { return @"The --source argument was specified in the parameter file, but the current operation is not a backup operation, so the argument is ignored"; } }
|
||||
public static string LogfileCommandDescription { get { return LC.L(@"Output log information to the file given"); } }
|
||||
public static string LoglevelCommandDescription { get { return LC.L(@"Determine the amount of information written in the log file"); } }
|
||||
public static string PortablemodeCommandDescription { get { return LC.L(@"Activate portable mode where the database is placed below the program executable"); } }
|
||||
public static string SeriousError(string message) { return LC.L(@"A serious error occurred in Duplicati: {0}", message); }
|
||||
public static string TearDownError(string message) { return LC.L(@"An error occurred on server tear down: {0}", message); }
|
||||
public static string StartupFailure(System.Exception error) { return LC.L(@"Unable to start up. Perhaps another process is already running?
|
||||
Error message: {0}", error); }
|
||||
public static string UnencrypteddatabaseCommandDescription { get { return LC.L(@"Disable database encryption"); } }
|
||||
public static string WrongSQLiteVersion(System.Version actualversion, string expectedversion) { return LC.L(@"Unsupported version of SQLite detected ({0}), must be {1} or higher", actualversion, expectedversion); }
|
||||
public static string WebserverWebrootDescription { get { return LC.L(@"The path to the folder where the static files for the webserver is present. The folder must be located beneath the installation folder."); } }
|
||||
public static string WebserverPortDescription { get { return LC.L(@"The port the webserver listens on. Multiple values may be supplied with a comma in between."); } }
|
||||
public static string WebserverCertificateFileDescription { get { return LC.L(@"The certificate and key file in PKCS #12 format the webserver use for SSL. Only RSA/DSA keys are supported."); } }
|
||||
public static string WebserverCertificatePasswordDescription { get { return LC.L(@"The password for decryption of certificate PKCS #12 file."); } }
|
||||
public static string WebserverInterfaceDescription { get { return LC.L(@"The interface the webserver listens on. The special values ""*"" and ""any"" means any interface. The special value ""loopback"" means the loopback adapter."); } }
|
||||
public static string WebserverPasswordDescription { get { return LC.L(@"The password required to access the webserver. This option is saved so you do not need to set it on each run. Setting an empty value disables the password."); } }
|
||||
public static string WebserverAllowedhostnamesDescription { get { return LC.L(@"The hostnames that are accepted, separated with semicolons. If any of the hostnames are ""*"", all hostnames are allowed and the hostname checking is disabled."); } }
|
||||
public static string PingpongkeepaliveLong { get { return LC.L(@"When running as a server, the service daemon must verify that the process is responding. If this option is enabled, the server reads stdin and writes a reply to each line read."); } }
|
||||
public static string PingpongkeepaliveShort { get { return LC.L(@"Enable the ping-pong responder"); } }
|
||||
public static string LogretentionLong { get { return LC.L(@"Set the time after which log data will be purged from the database."); } }
|
||||
public static string LogretentionShort { get { return LC.L(@"Clean up old log data"); } }
|
||||
public static string ServerdatafolderLong(string envname) { return LC.L(@"Duplicati needs to store a small database with all settings. Use this option to choose where the settings are stored. This option can also be set with the environment variable {0}.", envname); }
|
||||
public static string ServerdatafolderShort { get { return LC.L(@"Set the folder where settings are stored"); } }
|
||||
public static string ServerencryptionkeyLong(string envname, string decryptionoption) { return LC.L(@"This option sets the encryption key used to scramble the local settings database. This option can also be set with the environment variable {0}. Use the option --{1} to disable the database scrambling.", envname, decryptionoption); }
|
||||
public static string ServerencryptionkeyShort { get { return LC.L(@"Set the database encryption key"); } }
|
||||
public static string TempdirLong { get { return LC.L(@"Use this option to supply an alternative folder for temporary storage. By default the system default temporary folder is used. Note that also SQLite will put temporary files in this temporary folder."); } }
|
||||
public static string TempdirShort { get { return LC.L(@"Temporary storage folder"); } }
|
||||
public static string WebserverResetJwtConfigDescription { get { return LC.L(@"Reset the JWT configuration, invalidating any issued login tokens"); } }
|
||||
public static string WebserverDisableVisualCaptchaDescription { get { return LC.L(@"Disable the visual captcha"); } }
|
||||
public static string WebserverApiOnlyDescription { get { return LC.L(@"Disable the web interface and only allow API access"); } }
|
||||
public static string WebserverDisableSigninTokensDescription { get { return LC.L(@"Disable the use of signin tokens"); } }
|
||||
public static string DisabledbencryptionLong { get { return LC.L(@"Use this option to disable database encryption of sensitive fields"); } }
|
||||
public static string DisabledbencryptionShort { get { return LC.L(@"Disable database encryption"); } }
|
||||
public static string LogwindowseventlogLong { get { return LC.L(@"Use this option to log to the Windows event log."); } }
|
||||
public static string LogwindowseventlogShort { get { return LC.L(@"Log to the Windows event log"); } }
|
||||
public static string LogwindowseventloglevelLong { get { return LC.L(@"Use this option to set the log level for the Windows event log."); } }
|
||||
public static string LogwindowseventloglevelShort { get { return LC.L(@"Set the log level for the Windows event log"); } }
|
||||
public static string WindowsEventLogSourceNotFound(string source) { return LC.L(@"The Windows event log source {0} was not found. The source must be registered before the log can be written.", source); }
|
||||
public static string WindowsEventLogNotSupported { get { return LC.L(@"The Windows event log is not supported on this platform"); } }
|
||||
public static string ServerStarted(int port) { return LC.L(@"Server has started and is listening on port {0}", port); }
|
||||
public static string ServerStartedSignin(string url) { return LC.L(@"Use the following link to sign in: {0}", url); }
|
||||
public static string ServerCrashed(string message) { return LC.L(@"The server crashed: {0}", message); }
|
||||
public static string RequiredbencryptionLong { get { return LC.L(@"Use this option to require a custom provided key for database encryption of sensitive fields and not rely on the serial number."); } }
|
||||
public static string RequiredbencryptionShort { get { return LC.L(@"Require database encryption"); } }
|
||||
public static string DatabaseEncryptionKeyRequired(string envkey, string disableoptionname) { return LC.L(@"Database encryption key is required. Supply an encryption key via the environment variable {0} or disable database encryption with the option --{1}", envkey, disableoptionname); }
|
||||
public static string BlacklistedEncryptionKey(string envkey, string disableoptionname) { return LC.L(@"The database encryption key is blacklisted and cannot be used. The database has been decrypted. Supply a new encryption key via the environment variable {0} or disable database encryption with the option --{1}", envkey, disableoptionname); }
|
||||
public static string NoEncryptionKeySpecified(string envkey, string disableoptionname) { return LC.L(@"No database encryption key was found. The database will be stored unencrypted. Supply an encryption key via the environment variable {0} or disable database encryption with the option --{1}", envkey, disableoptionname); }
|
||||
public static string EncryptionKeyMissing(string envkey) { return LC.L(@"The database appears to be encrypted, but no key was specified. Opening the database will likely fail. Use the environment variable {0} to specify the key.", envkey); }
|
||||
}
|
||||
internal static class Scheduler
|
||||
{
|
||||
public static string InvalidTimeSetupError(System.DateTime startdate, string interval, string alloweddays) { return LC.L(@"Unable to find a valid date, given the start date {0}, the repetition interval {1} and the allowed days {2}", startdate, interval, alloweddays); }
|
||||
}
|
||||
public static class Server
|
||||
{
|
||||
public static string DefectSSLCertInDatabase { get { return @"Unable to create SSL certificate using data from database. Starting without SSL."; } }
|
||||
public static string StartedServer(string ip, int port) { return LC.L(@"Server has started and is listening on {0}, port {1}", ip, port); }
|
||||
public static string SSLCertificateFailure(string errormessage) { return LC.L(@"Unable to create SSL certificate using provided parameters. Exception detail: {0}", errormessage); }
|
||||
public static string ServerStartFailure(IEnumerable<int> portstried) { return LC.L(@"Unable to open a socket for listening, tried ports: {0}", string.Join(",", from n in (portstried ?? new int[0]) select n.ToString())); }
|
||||
}
|
||||
public static string UnencrypteddatabaseCommandDescription { get { return LC.L(@"Disable database encryption"); } }
|
||||
public static string WrongSQLiteVersion(System.Version actualversion, string expectedversion) { return LC.L(@"Unsupported version of SQLite detected ({0}), must be {1} or higher", actualversion, expectedversion); }
|
||||
public static string WebserverWebrootDescription { get { return LC.L(@"The path to the folder where the static files for the webserver is present. The folder must be located beneath the installation folder."); } }
|
||||
public static string WebserverPortDescription { get { return LC.L(@"The port the webserver listens on. Multiple values may be supplied with a comma in between."); } }
|
||||
public static string WebserverCertificateFileDescription { get { return LC.L(@"The certificate and key file in PKCS #12 format the webserver use for SSL. Only RSA/DSA keys are supported."); } }
|
||||
public static string WebserverCertificatePasswordDescription { get { return LC.L(@"The password for decryption of certificate PKCS #12 file."); } }
|
||||
public static string WebserverInterfaceDescription { get { return LC.L(@"The interface the webserver listens on. The special values ""*"" and ""any"" means any interface. The special value ""loopback"" means the loopback adapter."); } }
|
||||
public static string WebserverPasswordDescription { get { return LC.L(@"The password required to access the webserver. This option is saved so you do not need to set it on each run. Setting an empty value disables the password."); } }
|
||||
public static string WebserverAllowedhostnamesDescription { get { return LC.L(@"The hostnames that are accepted, separated with semicolons. If any of the hostnames are ""*"", all hostnames are allowed and the hostname checking is disabled."); } }
|
||||
public static string PingpongkeepaliveLong { get { return LC.L(@"When running as a server, the service daemon must verify that the process is responding. If this option is enabled, the server reads stdin and writes a reply to each line read."); } }
|
||||
public static string PingpongkeepaliveShort { get { return LC.L(@"Enable the ping-pong responder"); } }
|
||||
public static string LogretentionLong { get { return LC.L(@"Set the time after which log data will be purged from the database."); } }
|
||||
public static string LogretentionShort { get { return LC.L(@"Clean up old log data"); } }
|
||||
public static string ServerdatafolderLong(string envname) { return LC.L(@"Duplicati needs to store a small database with all settings. Use this option to choose where the settings are stored. This option can also be set with the environment variable {0}.", envname); }
|
||||
public static string ServerdatafolderShort { get { return LC.L(@"Set the folder where settings are stored"); } }
|
||||
public static string ServerencryptionkeyLong(string envname, string decryptionoption) { return LC.L(@"This option sets the encryption key used to scramble the local settings database. This option can also be set with the environment variable {0}. Use the option --{1} to disable the database scrambling.", envname, decryptionoption); }
|
||||
public static string ServerencryptionkeyShort { get { return LC.L(@"Set the database encryption key"); } }
|
||||
public static string TempdirLong { get { return LC.L(@"Use this option to supply an alternative folder for temporary storage. By default the system default temporary folder is used. Note that also SQLite will put temporary files in this temporary folder."); } }
|
||||
public static string TempdirShort { get { return LC.L(@"Temporary storage folder"); } }
|
||||
public static string WebserverResetJwtConfigDescription { get { return LC.L(@"Reset the JWT configuration, invalidating any issued login tokens"); } }
|
||||
public static string WebserverDisableVisualCaptchaDescription { get { return LC.L(@"Disable the visual captcha"); } }
|
||||
public static string WebserverApiOnlyDescription { get { return LC.L(@"Disable the web interface and only allow API access"); } }
|
||||
public static string WebserverDisableSigninTokensDescription { get { return LC.L(@"Disable the use of signin tokens"); } }
|
||||
public static string WebserverSpaPathsDescription { get { return LC.L(@"The relative paths that should be served as single page applications, separated with semicolons."); } }
|
||||
public static string DisabledbencryptionLong { get { return LC.L(@"Use this option to disable database encryption of sensitive fields"); } }
|
||||
public static string DisabledbencryptionShort { get { return LC.L(@"Disable database encryption"); } }
|
||||
public static string LogwindowseventlogLong { get { return LC.L(@"Use this option to log to the Windows event log."); } }
|
||||
public static string LogwindowseventlogShort { get { return LC.L(@"Log to the Windows event log"); } }
|
||||
public static string LogwindowseventloglevelLong { get { return LC.L(@"Use this option to set the log level for the Windows event log."); } }
|
||||
public static string LogwindowseventloglevelShort { get { return LC.L(@"Set the log level for the Windows event log"); } }
|
||||
public static string WindowsEventLogSourceNotFound(string source) { return LC.L(@"The Windows event log source {0} was not found. The source must be registered before the log can be written.", source); }
|
||||
public static string WindowsEventLogNotSupported { get { return LC.L(@"The Windows event log is not supported on this platform"); } }
|
||||
public static string ServerStarted(int port) { return LC.L(@"Server has started and is listening on port {0}", port); }
|
||||
public static string ServerStartedSignin(string url) { return LC.L(@"Use the following link to sign in: {0}", url); }
|
||||
public static string ServerCrashed(string message) { return LC.L(@"The server crashed: {0}", message); }
|
||||
public static string RequiredbencryptionLong { get { return LC.L(@"Use this option to require a custom provided key for database encryption of sensitive fields and not rely on the serial number."); } }
|
||||
public static string RequiredbencryptionShort { get { return LC.L(@"Require database encryption"); } }
|
||||
public static string DatabaseEncryptionKeyRequired(string envkey, string disableoptionname) { return LC.L(@"Database encryption key is required. Supply an encryption key via the environment variable {0} or disable database encryption with the option --{1}", envkey, disableoptionname); }
|
||||
public static string BlacklistedEncryptionKey(string envkey, string disableoptionname) { return LC.L(@"The database encryption key is blacklisted and cannot be used. The database has been decrypted. Supply a new encryption key via the environment variable {0} or disable database encryption with the option --{1}", envkey, disableoptionname); }
|
||||
public static string NoEncryptionKeySpecified(string envkey, string disableoptionname) { return LC.L(@"No database encryption key was found. The database will be stored unencrypted. Supply an encryption key via the environment variable {0} or disable database encryption with the option --{1}", envkey, disableoptionname); }
|
||||
public static string EncryptionKeyMissing(string envkey) { return LC.L(@"The database appears to be encrypted, but no key was specified. Opening the database will likely fail. Use the environment variable {0} to specify the key.", envkey); }
|
||||
}
|
||||
internal static class Scheduler
|
||||
{
|
||||
public static string InvalidTimeSetupError(System.DateTime startdate, string interval, string alloweddays) { return LC.L(@"Unable to find a valid date, given the start date {0}, the repetition interval {1} and the allowed days {2}", startdate, interval, alloweddays); }
|
||||
}
|
||||
public static class Server
|
||||
{
|
||||
public static string DefectSSLCertInDatabase { get { return @"Unable to create SSL certificate using data from database. Starting without SSL."; } }
|
||||
public static string StartedServer(string ip, int port) { return LC.L(@"Server has started and is listening on {0}, port {1}", ip, port); }
|
||||
public static string SSLCertificateFailure(string errormessage) { return LC.L(@"Unable to create SSL certificate using provided parameters. Exception detail: {0}", errormessage); }
|
||||
public static string ServerStartFailure(IEnumerable<int> portstried) { return LC.L(@"Unable to open a socket for listening, tried ports: {0}", string.Join(",", from n in (portstried ?? new int[0]) select n.ToString())); }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -50,7 +50,7 @@ namespace Duplicati.Library.Utility
|
||||
|
||||
int multiplier = negate ? -1 : 1;
|
||||
|
||||
if (string.IsNullOrEmpty(datestring))
|
||||
if (string.IsNullOrEmpty(datestring))
|
||||
return offset;
|
||||
|
||||
if (String.Equals(datestring.Trim(), "now", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -59,12 +59,11 @@ namespace Duplicati.Library.Utility
|
||||
long l;
|
||||
if (long.TryParse(datestring, System.Globalization.NumberStyles.Integer, null, out l))
|
||||
return offset.AddSeconds(l * multiplier);
|
||||
|
||||
DateTime t;
|
||||
if (DateTime.TryParse(datestring, System.Globalization.CultureInfo.CurrentUICulture, System.Globalization.DateTimeStyles.AssumeLocal, out t))
|
||||
|
||||
if (DateTime.TryParse(datestring, System.Globalization.CultureInfo.CurrentCulture, System.Globalization.DateTimeStyles.AssumeLocal, out var t))
|
||||
return t;
|
||||
|
||||
if (Library.Utility.Utility.TryDeserializeDateTime(datestring, out t))
|
||||
|
||||
if (Utility.TryDeserializeDateTime(datestring, out t))
|
||||
return t;
|
||||
|
||||
char[] separators = new char[] { 's', 'm', 'h', 'D', 'W', 'M', 'Y' };
|
||||
@@ -107,7 +106,7 @@ namespace Duplicati.Library.Utility
|
||||
default:
|
||||
throw new Exception(Strings.Timeparser.InvalidSpecifierError(datestring[index]));
|
||||
}
|
||||
previndex = index + 1;
|
||||
previndex = index + 1;
|
||||
}
|
||||
|
||||
if (datestring.Substring(previndex).Trim().Length > 0)
|
||||
|
||||
@@ -399,7 +399,9 @@ namespace Duplicati.Server
|
||||
parsedOptions.Certificate,
|
||||
parsedOptions.Servername,
|
||||
parsedOptions.AllowedHostnames,
|
||||
parsedOptions.DisableStaticFiles);
|
||||
parsedOptions.DisableStaticFiles,
|
||||
parsedOptions.SPAPaths
|
||||
);
|
||||
|
||||
var server = new DuplicatiWebserver();
|
||||
|
||||
@@ -908,6 +910,7 @@ namespace Duplicati.Server
|
||||
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_DISABLE_VISUAL_CAPTCHA, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverDisableVisualCaptchaDescription, Strings.Program.WebserverDisableVisualCaptchaDescription),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_API_ONLY, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverApiOnlyDescription, Strings.Program.WebserverApiOnlyDescription),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_DISABLE_SIGNIN_TOKENS, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverDisableSigninTokensDescription, Strings.Program.WebserverDisableSigninTokensDescription),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_SPAPATHS, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.WebserverSpaPathsDescription, Strings.Program.WebserverSpaPathsDescription, WebServerLoader.DEFAULT_OPTION_SPAPATHS),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(PING_PONG_KEEPALIVE_OPTION, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.PingpongkeepaliveShort, Strings.Program.PingpongkeepaliveLong),
|
||||
new Duplicati.Library.Interface.CommandLineArgument("log-retention", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Timespan, Strings.Program.LogretentionShort, Strings.Program.LogretentionLong, DEFAULT_LOG_RETENTION),
|
||||
new Duplicati.Library.Interface.CommandLineArgument("server-datafolder", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.ServerdatafolderShort, Strings.Program.ServerdatafolderLong(DATAFOLDER_ENV_NAME), System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), Library.AutoUpdater.AutoUpdateSettings.AppName)),
|
||||
|
||||
@@ -86,12 +86,21 @@ public static class WebServerLoader
|
||||
/// Option for disabling the use of signin tokens
|
||||
/// </summary>
|
||||
public const string OPTION_WEBSERVICE_DISABLE_SIGNIN_TOKENS = "webservice-disable-signin-tokens";
|
||||
/// <summary>
|
||||
/// Option for setting the webservice SPA paths
|
||||
/// </summary>
|
||||
public const string OPTION_WEBSERVICE_SPAPATHS = "webservice-spa-paths";
|
||||
|
||||
/// <summary>
|
||||
/// The default path to the web root
|
||||
/// </summary>
|
||||
public const string DEFAULT_OPTION_WEBROOT = "webroot";
|
||||
|
||||
/// <summary>
|
||||
/// The default paths to serve as SPAs
|
||||
/// </summary>
|
||||
public const string DEFAULT_OPTION_SPAPATHS = "/ngclient";
|
||||
|
||||
/// <summary>
|
||||
/// The default listening port
|
||||
/// </summary>
|
||||
@@ -122,6 +131,7 @@ public static class WebServerLoader
|
||||
/// <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>
|
||||
public record ParsedWebserverSettings(
|
||||
string WebRoot,
|
||||
int Port,
|
||||
@@ -129,7 +139,8 @@ public static class WebServerLoader
|
||||
X509Certificate2? Certificate,
|
||||
string Servername,
|
||||
IEnumerable<string> AllowedHostnames,
|
||||
bool DisableStaticFiles
|
||||
bool DisableStaticFiles,
|
||||
IEnumerable<string> SPAPaths
|
||||
);
|
||||
|
||||
|
||||
@@ -223,6 +234,10 @@ public static class WebServerLoader
|
||||
#endif
|
||||
}
|
||||
|
||||
options.TryGetValue(OPTION_WEBSERVICE_SPAPATHS, out var spaPathsString);
|
||||
if (string.IsNullOrWhiteSpace(spaPathsString))
|
||||
spaPathsString = DEFAULT_OPTION_SPAPATHS;
|
||||
|
||||
var certValid = cert != null && cert.HasPrivateKey;
|
||||
var settings = new ParsedWebserverSettings(
|
||||
webroot,
|
||||
@@ -231,7 +246,8 @@ public static class WebServerLoader
|
||||
cert,
|
||||
string.Format("{0} v{1}", Library.AutoUpdater.AutoUpdateSettings.AppName, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version),
|
||||
(connection.ApplicationSettings.AllowedHostnames ?? string.Empty).Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries),
|
||||
Duplicati.Library.Utility.Utility.ParseBoolOption(options, OPTION_WEBSERVICE_API_ONLY)
|
||||
Duplicati.Library.Utility.Utility.ParseBoolOption(options, OPTION_WEBSERVICE_API_ONLY),
|
||||
spaPathsString.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
);
|
||||
|
||||
// Materialize the list of ports, and move the last-used port to the front, so we try the last-known port first
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>
|
||||
This path is intended to host a single-page application (SPA) but it
|
||||
failed to install.
|
||||
</h1>
|
||||
<div>
|
||||
Check the logs for more information. You may be able to fix the issue by
|
||||
running the following command inside the folder:
|
||||
<pre>npm i</pre>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "ngclient",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ngclient",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@duplicati/ngclient": "^0.0.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@duplicati/ngclient": {
|
||||
"version": "0.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@duplicati/ngclient/-/ngclient-0.0.11.tgz",
|
||||
"integrity": "sha512-J1Oy5IZ20i3vV6lAzON2FTouU3gancBpurRubtyb/gt9dEWmN5niqvfAuupiO2lb5gQxcifV9DuarAXQKrPNRA==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "ngclient",
|
||||
"version": "1.0.0",
|
||||
"description": "Modern Angular Client for Duplicati",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@duplicati/ngclient": "^0.0.11"
|
||||
}
|
||||
}
|
||||
@@ -1,98 +1,105 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
|
||||
<title>Select theme</title>
|
||||
<style type="text/css">
|
||||
.button {
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 10px;
|
||||
background-color: lightblue;
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.button.disabled {
|
||||
.button.disabled {
|
||||
background-color: lightgray;
|
||||
color: white;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
function setDefault(theme) {
|
||||
|
||||
function setDefault(theme) {
|
||||
var d = new Date();
|
||||
d.setTime(d.getTime() + (90*24*60*60*1000));
|
||||
d.setTime(d.getTime() + 90 * 24 * 60 * 60 * 1000);
|
||||
|
||||
document.cookie = 'default-theme=' + theme + '; expires=' + d.toUTCString() + '; path=/';
|
||||
document.cookie =
|
||||
"default-theme=" +
|
||||
theme +
|
||||
"; expires=" +
|
||||
d.toUTCString() +
|
||||
"; path=/";
|
||||
updateToCurrentTheme();
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
function parseCookies(){
|
||||
|
||||
var cookiestrings = document.cookie.split('; ');
|
||||
function parseCookies() {
|
||||
var cookiestrings = document.cookie.split("; ");
|
||||
var cookies = {};
|
||||
|
||||
for(var i=cookiestrings.length-1; i>=0; i--){
|
||||
var parts = cookiestrings[i].split('=');
|
||||
cookies[parts[0]] = parts[1];
|
||||
for (var i = cookiestrings.length - 1; i >= 0; i--) {
|
||||
var parts = cookiestrings[i].split("=");
|
||||
cookies[parts[0]] = parts[1];
|
||||
}
|
||||
|
||||
return cookies;
|
||||
};
|
||||
}
|
||||
|
||||
function updateToCurrentTheme() {
|
||||
var theme = parseCookies()['default-theme'];
|
||||
function updateToCurrentTheme() {
|
||||
var theme = parseCookies()["default-theme"];
|
||||
|
||||
var themes = ['ngax'];
|
||||
for(var k in themes) {
|
||||
var el = document.getElementById(themes[k]);
|
||||
el.className = "button" + (theme == themes[k] ? " disabled" : "");
|
||||
var themes = ["ngax", "ngclient"];
|
||||
for (var k in themes) {
|
||||
var el = document.getElementById(themes[k]);
|
||||
el.className = "button" + (theme == themes[k] ? " disabled" : "");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function onLabelClick(e) {
|
||||
document.forms[0]['theme'].value = e;
|
||||
};
|
||||
function onLabelClick(e) {
|
||||
document.forms[0]["theme"].value = e;
|
||||
}
|
||||
|
||||
function setView(theme) {
|
||||
function setView(theme) {
|
||||
window.location = theme;
|
||||
};
|
||||
|
||||
// Lazy hack for DOM timing issues...
|
||||
window.setTimeout(updateToCurrentTheme, 1000);
|
||||
}
|
||||
|
||||
// Lazy hack for DOM timing issues...
|
||||
window.setTimeout(updateToCurrentTheme, 1000);
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src="oem/root/theme/oem.js" ></script>
|
||||
<script type="text/javascript" src="oem/root/theme/oem.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="oem/root/theme/oem.css" />
|
||||
</head>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body>
|
||||
<h2>Duplicati theme</h2>
|
||||
<ul>
|
||||
<li class="theme-item">
|
||||
<h3 class="title">NgAx - The responsive theme</h3>
|
||||
<div class="buttons">
|
||||
<div class="button" id="ngax" onclick="setDefault('ngax')">Use as default</div>
|
||||
<div class="button" onclick="setView('ngax')">View now</div>
|
||||
</div>
|
||||
</li>
|
||||
<!--<li class="theme-item">
|
||||
<h3 class="title">Greeno - Your friendly green theme</h3>
|
||||
<div class="buttons">
|
||||
<div class="button" id="greeno" onclick="setDefault('greeno')">Use as default</div>
|
||||
<div class="button" onclick="setView('greeno')">View now</div>
|
||||
</div>
|
||||
</li>-->
|
||||
<li class="theme-item">
|
||||
<h3 class="title">NgAx - The responsive theme</h3>
|
||||
<div class="buttons">
|
||||
<div class="button" id="ngax" onclick="setDefault('ngax')">
|
||||
Use as default
|
||||
</div>
|
||||
<div class="button" onclick="setView('ngax')">View now</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="theme-item">
|
||||
<h3 class="title">NgClient - Modern Angular theme</h3>
|
||||
<div class="buttons">
|
||||
<div class="button" id="ngclient" onclick="setDefault('ngclient')">
|
||||
Use as default
|
||||
</div>
|
||||
<div class="button" onclick="setView('ngclient')">View now</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<script type="text/javascript">updateToCurrentTheme();</script>
|
||||
</body>
|
||||
<script type="text/javascript">
|
||||
updateToCurrentTheme();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -72,10 +72,10 @@ namespace Duplicati.UnitTest
|
||||
Action<string, string> checkStringComparison = (x, y) => Assert.IsFalse(String.Equals(x, y, Utility.ClientFilenameStringComparison));
|
||||
Action<string, string> checkStringComparer = (x, y) => Assert.IsFalse(new HashSet<string>(new[] { x }).Contains(y, Utility.ClientFilenameStringComparer));
|
||||
|
||||
System.Globalization.CultureInfo originalCulture = System.Globalization.CultureInfo.CurrentCulture;
|
||||
CultureInfo originalCulture = CultureInfo.CurrentCulture;
|
||||
try
|
||||
{
|
||||
System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(cultureName, false);
|
||||
CultureInfo.CurrentCulture = new CultureInfo(cultureName, false);
|
||||
|
||||
// These are equivalent with respect to hu-HU, but different with respect to en-US.
|
||||
string ddzs = "ddzs";
|
||||
@@ -97,7 +97,7 @@ namespace Duplicati.UnitTest
|
||||
}
|
||||
finally
|
||||
{
|
||||
System.Threading.Thread.CurrentThread.CurrentCulture = originalCulture;
|
||||
CultureInfo.CurrentCulture = originalCulture;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Http.Json;
|
||||
using Microsoft.Extensions.Configuration.Json;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
namespace Duplicati.WebserverCore;
|
||||
@@ -65,6 +66,7 @@ public partial class DuplicatiWebserver
|
||||
/// <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>
|
||||
public record InitSettings(
|
||||
string WebRoot,
|
||||
int Port,
|
||||
@@ -72,7 +74,8 @@ public partial class DuplicatiWebserver
|
||||
X509Certificate2? Certificate,
|
||||
string Servername,
|
||||
IEnumerable<string> AllowedHostnames,
|
||||
bool DisableStaticFiles
|
||||
bool DisableStaticFiles,
|
||||
IEnumerable<string> SPAPaths
|
||||
);
|
||||
|
||||
public void InitWebServer(InitSettings settings, Connection connection)
|
||||
@@ -180,6 +183,9 @@ public partial class DuplicatiWebserver
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddCheck("Basic", () => HealthCheckResult.Healthy("Service is running"));
|
||||
|
||||
builder.Services.AddDuplicati(connection);
|
||||
|
||||
// Prevent logs from spamming the console, but allow enabling for debugging
|
||||
@@ -213,7 +219,7 @@ public partial class DuplicatiWebserver
|
||||
}
|
||||
|
||||
if (!settings.DisableStaticFiles)
|
||||
App.UseDefaultStaticFiles(settings.WebRoot);
|
||||
App.UseDefaultStaticFiles(settings.WebRoot, settings.SPAPaths);
|
||||
|
||||
App.UseExceptionHandler(app =>
|
||||
{
|
||||
@@ -244,6 +250,7 @@ public partial class DuplicatiWebserver
|
||||
|
||||
public Task Start(InitSettings settings)
|
||||
{
|
||||
App.MapHealthChecks("/health");
|
||||
App.AddEndpoints()
|
||||
.UseNotifications("/notifications");
|
||||
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
#if DEBUG
|
||||
using System.Formats.Tar;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
using Duplicati.Library.Logging;
|
||||
using Duplicati.Library.Utility;
|
||||
|
||||
namespace Duplicati.WebserverCore.Middlewares;
|
||||
|
||||
internal static class NpmSpaHelper
|
||||
{
|
||||
private static readonly string LOGTAG = Log.LogTagFromType(typeof(NpmSpaHelper));
|
||||
|
||||
private sealed record PackageJson(Dictionary<string, string>? dependencies, string? packageId);
|
||||
private sealed record PackageLockJson(Dictionary<string, PackageInfo>? packages);
|
||||
private sealed record PackageInfo(string version, string resolved, string? integrity);
|
||||
private sealed record InstalledPackageJson(string version);
|
||||
public sealed record SpaConfig(FileInfo IndexFile, string BasePath);
|
||||
|
||||
public static SpaConfig? InstallNpmPackage(string packageUrl, string targetPath)
|
||||
{
|
||||
var tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
|
||||
try
|
||||
{
|
||||
// Download and extract the package
|
||||
Directory.CreateDirectory(tempPath);
|
||||
|
||||
var tgzFile = Path.Combine(tempPath, "package.tgz");
|
||||
var tarFile = Path.ChangeExtension(tgzFile, ".tar");
|
||||
using (var client = new HttpClient())
|
||||
{
|
||||
var response = client.GetAsync(packageUrl).Await();
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
using (var stream = response.Content.ReadAsStream())
|
||||
using (var fileStream = File.Create(tgzFile))
|
||||
stream.CopyTo(fileStream);
|
||||
}
|
||||
|
||||
// Extract the zipped contents
|
||||
using (var fsource = new FileStream(tgzFile, FileMode.Open, FileAccess.Read))
|
||||
using (var ftarget = new FileStream(tarFile, FileMode.Create, FileAccess.Write))
|
||||
using (var gzip = new GZipStream(new FileStream(tgzFile, FileMode.Open, FileAccess.Read), CompressionMode.Decompress))
|
||||
gzip.CopyTo(ftarget);
|
||||
|
||||
var dest = Path.Combine(tempPath, "package");
|
||||
|
||||
// Extract the tar entries into the temp folder
|
||||
using (var fsource = new FileStream(tarFile, FileMode.Open, FileAccess.Read))
|
||||
using (var reader = new TarReader(fsource))
|
||||
while (reader.GetNextEntry() is TarEntry tar)
|
||||
{
|
||||
var path = Path.Combine(tempPath, tar.Name);
|
||||
|
||||
// Ensure the path is within the temp folder
|
||||
if (!path.StartsWith(dest))
|
||||
continue;
|
||||
|
||||
if (tar.EntryType == TarEntryType.Directory)
|
||||
Directory.CreateDirectory(path);
|
||||
else if (tar.EntryType == TarEntryType.RegularFile)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Invalid path"));
|
||||
tar.ExtractToFile(path, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Read the package.json and check it looks correct
|
||||
var packageJson = JsonSerializer.Deserialize<InstalledPackageJson>(File.ReadAllText(Path.Combine(dest, "package.json")));
|
||||
if (packageJson == null || string.IsNullOrWhiteSpace(packageJson.version))
|
||||
return null;
|
||||
|
||||
// Make room for the new package
|
||||
if (Directory.Exists(targetPath))
|
||||
Directory.Delete(targetPath, true);
|
||||
|
||||
var parentFolder = Path.GetDirectoryName(targetPath);
|
||||
if (parentFolder != null && !Directory.Exists(parentFolder))
|
||||
Directory.CreateDirectory(parentFolder);
|
||||
|
||||
// Move the package to the target path
|
||||
Directory.Move(dest, targetPath);
|
||||
|
||||
// Clean up
|
||||
Directory.Delete(tempPath, true);
|
||||
|
||||
return new SpaConfig(new FileInfo(Path.Combine(targetPath, "index.html")), targetPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Library.Logging.Log.WriteErrorMessage(LOGTAG, "SpaDebugHelperFail", ex, "NPM SPA package installation failed");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(tempPath))
|
||||
Directory.Delete(tempPath, true);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Probes for a missing/outdates SPA in the target folder and installs it if found.
|
||||
/// </summary>
|
||||
/// <param name="basepath">The base path to probe</param>
|
||||
/// <returns>The SPA configuration if found, otherwise null</returns>
|
||||
public static SpaConfig? ProbeForNpmSpa(string basepath)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check if the package.json and package-lock.json files exist
|
||||
var packageFile = Path.Combine(basepath, "package.json");
|
||||
var packagelockFile = Path.Combine(basepath, "package-lock.json");
|
||||
|
||||
if (!File.Exists(packageFile) || !File.Exists(packagelockFile))
|
||||
return null;
|
||||
|
||||
var packageJson = JsonSerializer.Deserialize<PackageJson>(File.ReadAllText(packageFile));
|
||||
var packageLockJson = JsonSerializer.Deserialize<PackageLockJson>(File.ReadAllText(packagelockFile));
|
||||
|
||||
var packageId = string.IsNullOrWhiteSpace(packageJson?.packageId) ? packageJson?.dependencies?.Keys.FirstOrDefault() : packageJson.packageId;
|
||||
if (string.IsNullOrWhiteSpace(packageId) || packageJson?.dependencies == null)
|
||||
return null;
|
||||
|
||||
var version = packageJson.dependencies.GetValueOrDefault(packageId);
|
||||
if (string.IsNullOrWhiteSpace(version))
|
||||
return null;
|
||||
|
||||
// Remove version prefix
|
||||
if (!char.IsAsciiDigit(version.First()))
|
||||
version = version[1..];
|
||||
|
||||
var packageUrl = packageLockJson?.packages?.GetValueOrDefault($"node_modules/{packageId}")?.resolved;
|
||||
if (string.IsNullOrWhiteSpace(packageUrl))
|
||||
return null;
|
||||
|
||||
// Package is not installed, install it
|
||||
var packageFolder = Path.Combine(basepath, "node_modules", packageId);
|
||||
if (!Directory.Exists(packageFolder))
|
||||
return InstallNpmPackage(packageUrl, packageFolder);
|
||||
|
||||
// The installed package is missing version, reinstall
|
||||
var installedPackageJson = Path.Combine(packageFolder, "package.json");
|
||||
if (!File.Exists(installedPackageJson))
|
||||
return InstallNpmPackage(packageUrl, packageFolder);
|
||||
|
||||
// The installed package is not the correct version, reinstall
|
||||
var installedJson = JsonSerializer.Deserialize<InstalledPackageJson>(File.ReadAllText(installedPackageJson));
|
||||
if (installedJson == null || string.IsNullOrWhiteSpace(installedJson.version) || installedJson.version != version)
|
||||
return InstallNpmPackage(packageUrl, packageFolder);
|
||||
|
||||
// Package is installed and correct version
|
||||
return new SpaConfig(new FileInfo(Path.Combine(packageFolder, "index.html")), packageFolder);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Library.Logging.Log.WriteErrorMessage(LOGTAG, "SpaDebugHelperFail", ex, "NPM SPA package installation failed");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.FileProviders.Physical;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Duplicati.WebserverCore.Middlewares;
|
||||
@@ -27,9 +29,111 @@ public static class StaticFilesExtensions
|
||||
MaxAge = TimeSpan.FromDays(7)
|
||||
};
|
||||
|
||||
private sealed record SpaConfig(string Prefix, byte[] IndexFile, string BasePath);
|
||||
|
||||
public static IApplicationBuilder UseDefaultStaticFiles(this WebApplication app, string webroot)
|
||||
private static byte[] ReadAndPatchIndexFile(FileInfo file, string prefix)
|
||||
{
|
||||
if (!prefix.EndsWith("/"))
|
||||
prefix += "/";
|
||||
|
||||
var index = File.ReadAllBytes(file.FullName);
|
||||
var indexStr = Encoding.UTF8.GetString(index);
|
||||
indexStr = indexStr.Replace("<base href=\"/\">", $"<base href=\"{prefix}\">");
|
||||
return Encoding.UTF8.GetBytes(indexStr);
|
||||
}
|
||||
|
||||
public static IApplicationBuilder UseDefaultStaticFiles(this WebApplication app, string webroot, IEnumerable<string> spaPaths)
|
||||
{
|
||||
var fileTypeMappings = new FileExtensionContentTypeProvider()
|
||||
{
|
||||
Mappings = {
|
||||
["htc"] = "text/x-component",
|
||||
["json"] = "application/json",
|
||||
["map"] = "application/json",
|
||||
["htm"] = "text/html; charset=utf-8",
|
||||
["html"] = "text/html; charset=utf-8",
|
||||
["hbs"] = "application/x-handlebars-template",
|
||||
["woff"] = "application/font-woff",
|
||||
["woff2"] = "application/font-woff"
|
||||
}
|
||||
};
|
||||
|
||||
var prefixHandlerMap = new List<SpaConfig>();
|
||||
var missingFile = new FileInfo(Path.Combine(webroot, "missing-spa.html"));
|
||||
|
||||
foreach (var prefix in spaPaths)
|
||||
{
|
||||
var basepath = Path.Combine(webroot, prefix.TrimStart('/'));
|
||||
if (!Directory.Exists(basepath))
|
||||
continue;
|
||||
|
||||
var file = Path.Combine(basepath, "index.html");
|
||||
var fi = new FileInfo(file);
|
||||
if (fi.Exists)
|
||||
{
|
||||
prefixHandlerMap.Add(new SpaConfig(prefix, ReadAndPatchIndexFile(fi, prefix), basepath));
|
||||
}
|
||||
#if DEBUG
|
||||
else
|
||||
{
|
||||
// Install from NPM in debug mode for easier development
|
||||
var spaConfig = NpmSpaHelper.ProbeForNpmSpa(basepath);
|
||||
if (spaConfig != null)
|
||||
prefixHandlerMap.Add(new SpaConfig(prefix, ReadAndPatchIndexFile(spaConfig.IndexFile, prefix), spaConfig.BasePath));
|
||||
else if (spaConfig == null && missingFile.Exists)
|
||||
prefixHandlerMap.Add(new SpaConfig(prefix, File.ReadAllBytes(missingFile.FullName), basepath));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (prefixHandlerMap.Any())
|
||||
{
|
||||
prefixHandlerMap = prefixHandlerMap.OrderByDescending(p => p.Prefix.Length).ToList();
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
await next();
|
||||
|
||||
// Not found only
|
||||
if (context.Response.StatusCode != 404 || context.Response.HasStarted)
|
||||
return;
|
||||
|
||||
// Check if we can use the path
|
||||
var path = context.Request.Path;
|
||||
if (!path.HasValue)
|
||||
return;
|
||||
|
||||
// Check if the path is a SPA path
|
||||
var spaConfig = prefixHandlerMap.FirstOrDefault(p => path.Value.StartsWith(p.Prefix));
|
||||
if (spaConfig == null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(Path.GetExtension(path)) || path.Value.EndsWith("/index.html"))
|
||||
{
|
||||
// Serve the index file
|
||||
context.Response.ContentType = "text/html";
|
||||
context.Response.StatusCode = 200;
|
||||
#if DEBUG
|
||||
await context.Response.Body.WriteAsync(ReadAndPatchIndexFile(new FileInfo(Path.Combine(spaConfig.BasePath, "index.html")), spaConfig.Prefix));
|
||||
#else
|
||||
await context.Response.Body.WriteAsync(spaConfig.IndexFile);
|
||||
#endif
|
||||
await context.Response.CompleteAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Serve the static file
|
||||
var file = new FileInfo(Path.Combine(spaConfig.BasePath, path.Value.Substring(spaConfig.Prefix.Length).TrimStart('/')));
|
||||
if (file.FullName.StartsWith(spaConfig.BasePath) && file.Exists && fileTypeMappings.Mappings.TryGetValue(Path.GetExtension(file.Extension), out var contentType))
|
||||
{
|
||||
context.Response.ContentType = contentType;
|
||||
context.Response.StatusCode = 200;
|
||||
await context.Response.SendFileAsync(new PhysicalFileInfo(file));
|
||||
await context.Response.CompleteAsync();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var fileProvider = new PhysicalFileProvider(Path.GetFullPath(webroot));
|
||||
var defaultFiles = GetDefaultFiles(fileProvider);
|
||||
app.UseDefaultFiles(defaultFiles);
|
||||
@@ -43,23 +147,11 @@ public static class StaticFilesExtensions
|
||||
var headers = context.Context.Response.GetTypedHeaders();
|
||||
var path = context.Context.Request.Path.Value ?? string.Empty;
|
||||
headers.CacheControl =
|
||||
(path.EndsWith("/index.html") || _nonCachePaths.Contains(path))
|
||||
? _noCache
|
||||
: _allowCache;
|
||||
(path.EndsWith("/index.html") || _nonCachePaths.Contains(path))
|
||||
? _noCache
|
||||
: _allowCache;
|
||||
},
|
||||
ContentTypeProvider = new FileExtensionContentTypeProvider()
|
||||
{
|
||||
Mappings = {
|
||||
["htc"] = "text/x-component",
|
||||
["json"] = "application/json",
|
||||
["map"] = "application/json",
|
||||
["htm"] = "text/html; charset=utf-8",
|
||||
["html"] = "text/html; charset=utf-8",
|
||||
["hbs"] = "application/x-handlebars-template",
|
||||
["woff"] = "application/font-woff",
|
||||
["woff2"] = "application/font-woff",
|
||||
}
|
||||
}
|
||||
ContentTypeProvider = fileTypeMappings
|
||||
});
|
||||
|
||||
return app;
|
||||
|
||||
@@ -491,6 +491,10 @@ public static partial class Command
|
||||
// Inject a version tag into the html files
|
||||
revertableFiles.AddRange(InjectVersionIntoFiles(baseDir, releaseInfo));
|
||||
|
||||
// Record the files that are replaced by the npm package
|
||||
var foldersToRemove = new List<string>();
|
||||
revertableFiles.AddRange(await ReplaceNpmPackages(baseDir, input.BuildPath.FullName, foldersToRemove, releaseInfo, rtcfg));
|
||||
|
||||
// Perform the main compilations
|
||||
var sourceBuildMap = new Dictionary<InterfaceType, IEnumerable<string>> {
|
||||
{ InterfaceType.GUI, sourceProjects
|
||||
@@ -623,6 +627,10 @@ public static partial class Command
|
||||
}
|
||||
|
||||
// Clean up the source tree
|
||||
foreach (var folder in foldersToRemove)
|
||||
if (Directory.Exists(folder))
|
||||
Directory.Delete(folder, true);
|
||||
|
||||
await ProcessHelper.Execute(new[] {
|
||||
"git", "checkout",
|
||||
}.Concat(revertableFiles.Select(x => Path.GetRelativePath(baseDir, x)))
|
||||
@@ -756,6 +764,64 @@ public static partial class Command
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses NPM to replace the node_modules folder in the webroot with a fresh install
|
||||
/// </summary>
|
||||
/// <param name="baseDir">The base directory</param>
|
||||
/// <param name="buildDir">The build directory</param>
|
||||
/// <param name="foldersToRemove">The folders to remove after the build</param>
|
||||
/// <param name="releaseInfo">The release info</param>
|
||||
/// <param name="rtcfg">The runtime configuration</param>
|
||||
/// <returns>The files that were deleted</returns>
|
||||
static async Task<List<string>> ReplaceNpmPackages(string baseDir, string buildDir, List<string> foldersToRemove, ReleaseInfo releaseInfo, RuntimeConfig rtcfg)
|
||||
{
|
||||
var deleted = new List<string>();
|
||||
|
||||
// Find all webroot folders with a package.json and package-lock.json
|
||||
var targets = Directory.EnumerateDirectories(Path.Combine(baseDir, "Duplicati", "Server", "webroot"), "*", SearchOption.TopDirectoryOnly)
|
||||
.Where(x => File.Exists(Path.Combine(x, "package.json")) && File.Exists(Path.Combine(x, "package-lock.json")))
|
||||
.ToList();
|
||||
|
||||
foreach (var target in targets)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Program.Configuration.Commands.Npm))
|
||||
throw new Exception("NPM command not found, but required for building");
|
||||
|
||||
// Remove existing node_modules folder
|
||||
if (Directory.Exists(Path.Combine(target, "node_modules")))
|
||||
Directory.Delete(Path.Combine(target, "node_modules"), true);
|
||||
|
||||
// These will be deleted after the build
|
||||
deleted.AddRange(Directory.EnumerateFiles(target, "*", SearchOption.AllDirectories));
|
||||
|
||||
var tmp = Path.Combine(buildDir, "tmp-npm");
|
||||
if (Directory.Exists(tmp))
|
||||
Directory.Delete(tmp, true);
|
||||
|
||||
Directory.CreateDirectory(tmp);
|
||||
EnvHelper.CopyDirectory(target, tmp, true);
|
||||
|
||||
// Run npm install in the temporary folder
|
||||
await ProcessHelper.Execute(new[] { Program.Configuration.Commands.Npm, "ci" }, workingDirectory: tmp);
|
||||
var basefolder = Directory.EnumerateDirectories(Path.Combine(tmp, "node_modules"), "*", SearchOption.AllDirectories)
|
||||
.Where(x => File.Exists(Path.Combine(x, "index.html")))
|
||||
.OrderBy(x => x.Length)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(basefolder))
|
||||
throw new Exception($"Failed to locate node_modules folder in {target}");
|
||||
|
||||
Directory.Delete(target, true);
|
||||
Directory.Move(basefolder, target);
|
||||
Directory.Delete(tmp, true);
|
||||
foldersToRemove.Add(target);
|
||||
}
|
||||
|
||||
return deleted.Where(x => !Path.GetFileName(x).StartsWith("."))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Loads a keyfile and decrypts the RSA key inside
|
||||
/// </summary>
|
||||
|
||||
@@ -297,6 +297,7 @@ public record ConfigFiles(
|
||||
/// <param name="Productsign">The "productsign" command</param>
|
||||
/// <param name="Wix">The "wix" command</param>
|
||||
/// <param name="Docker">The "docker" command</param>
|
||||
/// <param name="Npm">The "npm" command</param>
|
||||
public record Commands(
|
||||
string Dotnet,
|
||||
string? Gpg,
|
||||
@@ -304,7 +305,8 @@ public record Commands(
|
||||
string? Codesign,
|
||||
string? Productsign,
|
||||
string? Wix,
|
||||
string? Docker
|
||||
string? Docker,
|
||||
string? Npm
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
@@ -319,7 +321,8 @@ public record Commands(
|
||||
OperatingSystem.IsMacOS() ? FindCommand("codesign", "CODESIGN") : null,
|
||||
OperatingSystem.IsMacOS() ? FindCommand("productsign", "PRODUCTSIGN") : null,
|
||||
FindCommand(OperatingSystem.IsWindows() ? "wix" : "wixl", "WIX"),
|
||||
FindCommand("docker", "DOCKER")
|
||||
FindCommand("docker", "DOCKER"),
|
||||
FindCommand("npm", "NPM")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user