diff --git a/Duplicati.Library.RestAPI/RESTMethods/BackupDefaults.cs b/Duplicati.Library.RestAPI/RESTMethods/BackupDefaults.cs index 9138214b2..4d6675520 100644 --- a/Duplicati.Library.RestAPI/RESTMethods/BackupDefaults.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/BackupDefaults.cs @@ -59,7 +59,7 @@ namespace Duplicati.Server.WebServer.RESTMethods try { // Add install defaults/overrides, if present - var path = SystemIO.IO_OS.PathCombine(Library.AutoUpdater.UpdaterManager.InstalledBaseDir, "newbackup.json"); + var path = SystemIO.IO_OS.PathCombine(Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR, "newbackup.json"); if (System.IO.File.Exists(path)) { Newtonsoft.Json.Linq.JObject n; diff --git a/Duplicati.Library.RestAPI/RESTMethods/SystemInfo.cs b/Duplicati.Library.RestAPI/RESTMethods/SystemInfo.cs index 465396ea9..0f3fda512 100644 --- a/Duplicati.Library.RestAPI/RESTMethods/SystemInfo.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/SystemInfo.cs @@ -77,7 +77,6 @@ namespace Duplicati.Server.WebServer.RESTMethods ServerVersionName = Duplicati.License.VersionNumbers.Version, ServerVersionType = Duplicati.Library.AutoUpdater.UpdaterManager.SelfVersion.ReleaseType, StartedBy = FIXMEGlobal.Origin, - BaseVersionName = Duplicati.Library.AutoUpdater.UpdaterManager.BaseVersion.Displayname, DefaultUpdateChannel = Duplicati.Library.AutoUpdater.AutoUpdateSettings.DefaultUpdateChannel, DefaultUsageReportLevel = Duplicati.Library.UsageReporter.Reporter.DefaultReportLevel, ServerTime = DateTime.Now, diff --git a/Duplicati.Library.RestAPI/RESTMethods/Updates.cs b/Duplicati.Library.RestAPI/RESTMethods/Updates.cs index 5eec5a43b..94abc4491 100644 --- a/Duplicati.Library.RestAPI/RESTMethods/Updates.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Updates.cs @@ -33,23 +33,6 @@ namespace Duplicati.Server.WebServer.RESTMethods FIXMEGlobal.UpdatePoller.CheckNow(); info.OutputOK(); return; - - case "install": - FIXMEGlobal.UpdatePoller.InstallUpdate(); - info.OutputOK(); - return; - - case "activate": - if (FIXMEGlobal.WorkThread.CurrentTask != null || FIXMEGlobal.WorkThread.CurrentTasks.Count != 0) - { - info.ReportServerError("Cannot activate update while task is running or scheduled"); - } - else - { - FIXMEGlobal.UpdatePoller.ActivateUpdate(); - info.OutputOK(); - } - return; default: info.ReportClientError("No such action", System.Net.HttpStatusCode.NotFound); diff --git a/Duplicati.Library.RestAPI/Runner.cs b/Duplicati.Library.RestAPI/Runner.cs index 1dcdc267c..315ec6c60 100644 --- a/Duplicati.Library.RestAPI/Runner.cs +++ b/Duplicati.Library.RestAPI/Runner.cs @@ -358,7 +358,7 @@ namespace Duplicati.Server var exe = System.IO.Path.Combine( - Library.AutoUpdater.UpdaterManager.InstalledBaseDir, + Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR, System.IO.Path.GetFileName( typeof(Duplicati.CommandLine.Commands).Assembly.Location ) diff --git a/Duplicati.Library.RestAPI/Serializable/ServerStatus.cs b/Duplicati.Library.RestAPI/Serializable/ServerStatus.cs index c77ae6027..7b9f9f752 100644 --- a/Duplicati.Library.RestAPI/Serializable/ServerStatus.cs +++ b/Duplicati.Library.RestAPI/Serializable/ServerStatus.cs @@ -55,9 +55,9 @@ namespace Duplicati.Server.Serializable } } - public UpdatePollerStates UpdaterState { get { return FIXMEGlobal.UpdatePoller.ThreadState; } } + public string UpdateDownloadLink => FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion.GetUpdateUrls()?.FirstOrDefault(); - public bool UpdateReady { get { return Duplicati.Library.AutoUpdater.UpdaterManager.HasUpdateInstalled; } } + public UpdatePollerStates UpdaterState { get { return FIXMEGlobal.UpdatePoller.ThreadState; } } public double UpdateDownloadProgress { get { return FIXMEGlobal.UpdatePoller.DownloadProgess; } } diff --git a/Duplicati.Library.RestAPI/UpdatePollThread.cs b/Duplicati.Library.RestAPI/UpdatePollThread.cs index c82bd8d2d..b255be2cd 100644 --- a/Duplicati.Library.RestAPI/UpdatePollThread.cs +++ b/Duplicati.Library.RestAPI/UpdatePollThread.cs @@ -30,7 +30,6 @@ namespace Duplicati.Server { private readonly Thread m_thread; private volatile bool m_terminated = false; - private volatile bool m_download = false; private volatile bool m_forceCheck = false; private readonly object m_lock = new object(); private readonly AutoResetEvent m_waitSignal; @@ -71,25 +70,6 @@ namespace Duplicati.Server } } - public void InstallUpdate() - { - lock(m_lock) - { - m_forceCheck = true; - m_download = true; - m_waitSignal.Set(); - } - } - - public void ActivateUpdate() - { - if (Duplicati.Library.AutoUpdater.UpdaterManager.SetRunUpdate()) - { - IsUpdateRequested = true; - FIXMEGlobal.ApplicationExitEvent.Set(); - } - } - public void Terminate() { lock(m_lock) @@ -175,12 +155,15 @@ namespace Duplicati.Server FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion = null; } - if (FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion != null && Duplicati.Library.AutoUpdater.UpdaterManager.TryParseVersion(FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion.Version) > System.Reflection.Assembly.GetExecutingAssembly().GetName().Version) + var updatedinfo = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion; + if (updatedinfo != null && Duplicati.Library.AutoUpdater.UpdaterManager.TryParseVersion(updatedinfo.Version) > System.Reflection.Assembly.GetExecutingAssembly().GetName().Version) { + var package = updatedinfo.FindPackage(); + FIXMEGlobal.DataConnection.RegisterNotification( NotificationType.Information, "Found update", - FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion.Displayname, + updatedinfo.Displayname, null, null, "update:new", @@ -194,22 +177,6 @@ namespace Duplicati.Server } } - if (m_download) - { - lock(m_lock) - m_download = false; - - var v = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion; - if (v != null) - { - ThreadState = UpdatePollerStates.Downloading; - FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); - - if (Duplicati.Library.AutoUpdater.UpdaterManager.DownloadAndUnpackUpdate(v, (pg) => { DownloadProgess = pg; })) - FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); - } - } - DownloadProgess = 0; if (ThreadState != UpdatePollerStates.Waiting) diff --git a/Duplicati.Library.RestAPI/WebServer/Server.cs b/Duplicati.Library.RestAPI/WebServer/Server.cs index c8e4408f3..3d72a44b6 100644 --- a/Duplicati.Library.RestAPI/WebServer/Server.cs +++ b/Duplicati.Library.RestAPI/WebServer/Server.cs @@ -225,11 +225,11 @@ namespace Duplicati.Server.WebServer server.Add(new RESTHandler()); string webroot = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); - string install_webroot = System.IO.Path.Combine(Library.AutoUpdater.UpdaterManager.InstalledBaseDir, "webroot"); + string install_webroot = System.IO.Path.Combine(Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR, "webroot"); #if DEBUG // Easy test for extensions while debugging - install_webroot = Library.AutoUpdater.UpdaterManager.InstalledBaseDir; + install_webroot = Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR; if (!System.IO.Directory.Exists(System.IO.Path.Combine(webroot, "webroot"))) { diff --git a/Duplicati/CommandLine/BackendTester/Program.cs b/Duplicati/CommandLine/BackendTester/Program.cs index 5671d7647..7b6973ebd 100644 --- a/Duplicati/CommandLine/BackendTester/Program.cs +++ b/Duplicati/CommandLine/BackendTester/Program.cs @@ -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; @@ -31,7 +31,6 @@ namespace Duplicati.CommandLine.BackendTester { public class Program { - /// /// Used to maintain a reference to initialized system settings. /// @@ -63,13 +62,7 @@ namespace Duplicati.CommandLine.BackendTester /// The main entry point for the application. /// [STAThread] - public static int Main(string[] args) - { - Duplicati.Library.AutoUpdater.UpdaterManager.IgnoreWebrootFolder = true; - return Duplicati.Library.AutoUpdater.UpdaterManager.RunFromMostRecent(typeof(Program).GetMethod("RealMain"), args); - } - - public static void RealMain(string[] _args) + public static int Main(string[] _args) { try { @@ -107,7 +100,7 @@ namespace Duplicati.CommandLine.BackendTester foreach (string s in lines) Console.WriteLine(s); - return; + return 0; } if (options.ContainsKey("tempdir") && !string.IsNullOrEmpty(options["tempdir"])) @@ -129,14 +122,17 @@ namespace Duplicati.CommandLine.BackendTester { Console.WriteLine("Starting run no {0}", i); if (!Run(args, options, i == 0)) - return; + return 1; } Console.WriteLine("Unittest complete!"); + return 0; } catch (Exception ex) { Console.WriteLine("Unittest failed: " + ex); } + + return 1; } static bool Run(List args, Dictionary options, bool first) diff --git a/Duplicati/CommandLine/BackendTool/Program.cs b/Duplicati/CommandLine/BackendTool/Program.cs index b367cddfc..0a8d22f4b 100644 --- a/Duplicati/CommandLine/BackendTool/Program.cs +++ b/Duplicati/CommandLine/BackendTool/Program.cs @@ -34,13 +34,7 @@ namespace Duplicati.CommandLine.BackendTool /// The main entry point for the application. /// [STAThread] - public static int Main(string[] args) - { - Duplicati.Library.AutoUpdater.UpdaterManager.IgnoreWebrootFolder = true; - return Duplicati.Library.AutoUpdater.UpdaterManager.RunFromMostRecent(typeof(Program).GetMethod("RealMain"), args); - } - - public static int RealMain(string[] _args) + public static int Main(string[] _args) { bool debugoutput = false; try diff --git a/Duplicati/CommandLine/CLI/Program.cs b/Duplicati/CommandLine/CLI/Program.cs index 57c82a57a..6614a419c 100644 --- a/Duplicati/CommandLine/CLI/Program.cs +++ b/Duplicati/CommandLine/CLI/Program.cs @@ -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; @@ -39,12 +39,6 @@ namespace Duplicati.CommandLine /// The main entry point for the application. /// public static int Main(string[] args) - { - Duplicati.Library.AutoUpdater.UpdaterManager.IgnoreWebrootFolder = true; - return Duplicati.Library.AutoUpdater.UpdaterManager.RunFromMostRecent(typeof(Program).GetMethod("RealMain"), args); - } - - public static int RealMain(string[] args) { Library.UsageReporter.Reporter.Initialize(); FROM_COMMANDLINE = true; @@ -113,18 +107,28 @@ namespace Duplicati.CommandLine if (update != null && update.Version != Library.AutoUpdater.UpdaterManager.SelfVersion.Version) { - outwriter.WriteLine("Found update \"{0}\", downloading ...", update.Displayname); - long lastpg = 0; - Library.AutoUpdater.UpdaterManager.DownloadAndUnpackUpdate(update, f => + var package = update.FindPackage(); + if (package == null) { - var npg = (long)(f * 100); - if (Math.Abs(npg - lastpg) >= 5 || (npg == 100 && lastpg != 100)) + outwriter.WriteLine($"Failed to locate a matching package for this machine, please visit this link and select the correct package: {update.GetGenericUpdatePageUrl()}"); + } + else + { + var filename = Path.GetFullPath(package.GetFilename()); + outwriter.WriteLine("Downloading update \"{0}\" to {1} ...", update.Displayname, filename); + + long lastpg = 0; + Library.AutoUpdater.UpdaterManager.DownloadUpdate(update, package, filename, f => { - lastpg = npg; - outwriter.WriteLine("Downloading {0}% ...", npg); - } - }); - outwriter.WriteLine("Update \"{0}\" ({1}) installed, using on next launch", update.Displayname, update.Version); + var npg = (long)(f * 100); + if (Math.Abs(npg - lastpg) >= 5 || (npg == 100 && lastpg != 100)) + { + lastpg = npg; + outwriter.WriteLine("Downloading {0}% ...", npg); + } + }); + outwriter.WriteLine("Update \"{0}\" ({1}) downloaded", update.Displayname, update.Version); + } } } diff --git a/Duplicati/CommandLine/RecoveryTool/Program.cs b/Duplicati/CommandLine/RecoveryTool/Program.cs index adf3ab1e1..78c991834 100644 --- a/Duplicati/CommandLine/RecoveryTool/Program.cs +++ b/Duplicati/CommandLine/RecoveryTool/Program.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2024, The Duplicati Team +// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a @@ -27,19 +27,13 @@ namespace Duplicati.CommandLine.RecoveryTool { public static class Program { + private delegate int CommandRunner(List args, Dictionary options, Library.Utility.IFilter filter); + /// /// The main entry point for the application. /// [STAThread] - public static int Main(string[] args) - { - Duplicati.Library.AutoUpdater.UpdaterManager.IgnoreWebrootFolder = true; - return Duplicati.Library.AutoUpdater.UpdaterManager.RunFromMostRecent(typeof(Program).GetMethod("RealMain"), args); - } - - private delegate int CommandRunner(List args, Dictionary options, Library.Utility.IFilter filter); - - public static int RealMain(string[] _args) + public static int Main(string[] _args) { try { diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs index 857abfeba..ec0d5f528 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs @@ -59,18 +59,12 @@ namespace Duplicati.GUI.TrayIcon /// The main entry point for the application. /// [STAThread] - public static int Main(string[] args) - { - Duplicati.Library.AutoUpdater.UpdaterManager.RequiresRespawn = true; - return Duplicati.Library.AutoUpdater.UpdaterManager.RunFromMostRecent(typeof(Program).GetMethod("RealMain"), args, Duplicati.Library.AutoUpdater.AutoUpdateStrategy.Never); - } - - public static void RealMain(string[] _args) + public static int Main(string[] _args) { List args = new List(_args); Dictionary options = Duplicati.Library.Utility.CommandLineParser.ExtractOptions(args); - if (Platform.IsClientWindows && (Duplicati.Library.AutoUpdater.UpdaterManager.IsRunningInUpdateEnvironment || !Duplicati.Library.Utility.Utility.ParseBoolOption(options, DETACHED_PROCESS))) + if (Platform.IsClientWindows && !Duplicati.Library.Utility.Utility.ParseBoolOption(options, DETACHED_PROCESS)) Duplicati.Library.Utility.Win32.AttachConsole(Duplicati.Library.Utility.Win32.ATTACH_PARENT_PROCESS); foreach (string s in args) @@ -97,7 +91,7 @@ namespace Duplicati.GUI.TrayIcon foreach (Library.Interface.ICommandLineArgument arg in Duplicati.Server.Program.SupportedCommands) Console.WriteLine("--{0}: {1}", arg.Name, arg.LongDescription); - return; + return 0; } options.TryGetValue(BROWSER_COMMAND_OPTION, out _browser_command); @@ -115,7 +109,7 @@ namespace Duplicati.GUI.TrayIcon } catch (Server.SingleInstance.MultipleInstanceException) { - return; + return 1; } // We have a hosted server, if this is the first run, @@ -176,6 +170,8 @@ namespace Duplicati.GUI.TrayIcon serverURL = new Uri(url); StartTray(_args, options, hosted, password, saltedpassword); + + return 0; } private static void StartTray(string[] _args, Dictionary options, HostedInstanceKeeper hosted, string password, bool saltedpassword) diff --git a/Duplicati/Library/AutoUpdater/AutoUpdateSettings.cs b/Duplicati/Library/AutoUpdater/AutoUpdateSettings.cs index 70febbec9..48ec8fa43 100644 --- a/Duplicati/Library/AutoUpdater/AutoUpdateSettings.cs +++ b/Duplicati/Library/AutoUpdater/AutoUpdateSettings.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2024, The Duplicati Team +// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a @@ -143,10 +143,6 @@ namespace Duplicati.Library.AutoUpdater if (string.IsNullOrWhiteSpace(channelstring)) channelstring = BuildUpdateChannel; - if (string.IsNullOrWhiteSpace(channelstring)) - channelstring = UpdaterManager.BaseVersion.ReleaseType; - - // Update from older builds if (string.Equals(channelstring, "preview", StringComparison.OrdinalIgnoreCase)) channelstring = ReleaseType.Experimental.ToString(); diff --git a/Duplicati/Library/AutoUpdater/FileEntry.cs b/Duplicati/Library/AutoUpdater/InstallerEntry.cs similarity index 54% rename from Duplicati/Library/AutoUpdater/FileEntry.cs rename to Duplicati/Library/AutoUpdater/InstallerEntry.cs index c1386ad00..f36fec324 100644 --- a/Duplicati/Library/AutoUpdater/FileEntry.cs +++ b/Duplicati/Library/AutoUpdater/InstallerEntry.cs @@ -1,35 +1,66 @@ -// 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; - -namespace Duplicati.Library.AutoUpdater -{ - public class FileEntry - { - public string Path; - public string MD5; - public string SHA256; - public DateTime LastWriteTime; - public bool Ignore; - } -} - +// 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.IO; + +namespace Duplicati.Library.AutoUpdater +{ + /// + /// An installer entry, describing an architecture specific package + /// + public class PackageEntry + { + /// + /// The urls for the updater payload + /// + public string[] RemoteUrls; + /// + /// The length of the payload + /// + public long Length; + /// + /// The MD5 hash of the payload + /// + public string MD5; + /// + /// The SHA256 hash of the payload + /// + public string SHA256; + /// + /// The package type id + /// + public string PackageTypeId; + + /// + /// Gets the name of the package file + /// + /// The filename of the package + public string GetFilename() + { + var guess = Path.GetFileName(new Uri(RemoteUrls[0]).LocalPath); + if (string.IsNullOrWhiteSpace(guess)) + guess = "update.bin"; + + return guess; + } + } +} \ No newline at end of file diff --git a/Duplicati/Library/AutoUpdater/Program.cs b/Duplicati/Library/AutoUpdater/Program.cs index 2283cddc1..73dc50271 100644 --- a/Duplicati/Library/AutoUpdater/Program.cs +++ b/Duplicati/Library/AutoUpdater/Program.cs @@ -20,27 +20,16 @@ // DEALINGS IN THE SOFTWARE. using System; +using System.IO; using System.Linq; -using System.Collections.Generic; -using Duplicati.Library.Common.IO; namespace Duplicati.Library.AutoUpdater { public static class Program { - public static int Main(string[] args) + public static int Main(string[] _args) { - // Ignore webroot during startup verification - Duplicati.Library.AutoUpdater.UpdaterManager.IgnoreWebrootFolder = true; - return Duplicati.Library.AutoUpdater.UpdaterManager.RunFromMostRecent(typeof(Program).GetMethod("RealMain"), args, AutoUpdateStrategy.Never); - } - - public static int RealMain(string[] _args) - { - // Enable webroot checks for the verifier - Duplicati.Library.AutoUpdater.UpdaterManager.IgnoreWebrootFolder = false; - - var args = new List(_args); + var args = _args.ToList(); Duplicati.Library.Utility.CommandLineParser.ExtractOptions(args); if (args.Count == 0) @@ -62,26 +51,6 @@ namespace Duplicati.Library.AutoUpdater case "help": WriteUsage(); return 0; - case "list": - { - var versions = UpdaterManager.FindInstalledVersions(); - var selfdir = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); - if (string.Equals(Util.AppendDirSeparator(selfdir), Util.AppendDirSeparator(UpdaterManager.InstalledBaseDir))) - versions = versions.Union(new KeyValuePair[] { new KeyValuePair(selfdir, UpdaterManager.SelfVersion) }); - Console.WriteLine(string.Join(Environment.NewLine, versions.Select(x => string.Format(" {0} {1} ({2})", (x.Value.Version == UpdaterManager.SelfVersion.Version ? "*" : "-"), x.Value.Displayname, x.Value.Version)))); - return 0; - } - - case "verify": - { - var versions = UpdaterManager.FindInstalledVersions(); - var selfdir = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); - if (string.Equals(Util.AppendDirSeparator(selfdir), Util.AppendDirSeparator(UpdaterManager.InstalledBaseDir))) - versions = versions.Union(new KeyValuePair[] { new KeyValuePair(selfdir, UpdaterManager.SelfVersion) }); - - Console.WriteLine(string.Join(Environment.NewLine, versions.Select(x => string.Format(" {0} {1} ({2}): {3}", (x.Value.Version == UpdaterManager.SelfVersion.Version ? "*" : "-"), x.Value.Displayname, x.Value.Version, UpdaterManager.VerifyUnpackedFolder(x.Key, x.Value) ? "Valid" : "*** Modified ***")))); - return 0; - } case "check": { var update = UpdaterManager.CheckForUpdate(); @@ -94,7 +63,7 @@ namespace Duplicati.Library.AutoUpdater return 0; } - case "install": + case "download": { var update = UpdaterManager.CheckForUpdate(); if (update == null || update.Version == UpdaterManager.SelfVersion.Version) @@ -102,19 +71,29 @@ namespace Duplicati.Library.AutoUpdater Console.WriteLine("You are running the latest version: {0} ({1})", UpdaterManager.SelfVersion.Displayname, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version); return 0; } - Console.WriteLine("Downloading update \"{0}\" ...", update.Displayname); - long lastpg = 0; - UpdaterManager.DownloadAndUnpackUpdate(update, f => { - var npg = (long)(f*100); - if (Math.Abs(npg - lastpg) >= 5 || (npg == 100 && lastpg != 100)) - { - lastpg = npg; - Console.WriteLine("Downloading {0}% ...", npg); - } - }); + var package = update.FindPackage(); + if (package == null) + { + Console.WriteLine($"Failed to locate a matching package for this machine, please visit this link and select the correct package: {update.GetGenericUpdatePageUrl()}"); + } + else + { + var filename = Path.GetFullPath(package.GetFilename()); + Console.WriteLine("Downloading update \"{0}\" to {1} ...", update.Displayname, filename); - Console.WriteLine("New version \"{0}\" installed!", update.Displayname); + long lastpg = 0; + UpdaterManager.DownloadUpdate(update, package, filename, f => { + var npg = (long)(f*100); + if (Math.Abs(npg - lastpg) >= 5 || (npg == 100 && lastpg != 100)) + { + lastpg = npg; + Console.WriteLine("Downloading {0}% ...", npg); + } + }); + } + + Console.WriteLine("New version \"{0}\" downloaded!", update.Displayname); return 0; } default: @@ -127,18 +106,16 @@ namespace Duplicati.Library.AutoUpdater private static void WriteUsage() { - Console.WriteLine("Usage:{0}\t{1}{2} [LIST|VERIFY|CHECK|INSTALL|HELP]", Environment.NewLine, Duplicati.Library.Utility.Utility.IsMono ? "mono " : "", System.IO.Path.GetFileName(System.Reflection.Assembly.GetExecutingAssembly().Location)); + Console.WriteLine("Usage:{0}\t{1}{2} [CHECK|DOWNLOAD|HELP]", Environment.NewLine, Duplicati.Library.Utility.Utility.IsMono ? "mono " : "", System.IO.Path.GetFileName(System.Reflection.Assembly.GetExecutingAssembly().Location)); Console.WriteLine(); Console.WriteLine("Environment variables:"); Console.WriteLine(); Console.WriteLine("{0} - Disables updates completely", string.Format(UpdaterManager.SKIPUPDATE_ENVNAME_TEMPLATE, AutoUpdateSettings.AppName)); - Console.WriteLine("{0} - Choose how to handle updates, valid settings: {1}", string.Format(UpdaterManager.UPDATE_STRATEGY_ENVNAME_TEMPLATE, AutoUpdateSettings.AppName), string.Join(", ", Enum.GetNames(typeof(AutoUpdateStrategy)))); Console.WriteLine("{0} - Use alternate updates urls", string.Format(AutoUpdateSettings.UPDATEURL_ENVNAME_TEMPLATE, AutoUpdateSettings.AppName)); Console.WriteLine("{0} - Choose different channel than the default {1}, valid settings: {2}", string.Format(AutoUpdateSettings.UPDATECHANNEL_ENVNAME_TEMPLATE, AutoUpdateSettings.AppName), AutoUpdater.AutoUpdateSettings.DefaultUpdateChannel, string.Join(",", Enum.GetNames(typeof(ReleaseType)).Where( x => x != ReleaseType.Unknown.ToString()))); Console.WriteLine(); Console.WriteLine("Updates are downloaded from: {0}", string.Join(";", AutoUpdateSettings.URLs)); - Console.WriteLine("Updates are installed in: {0}", UpdaterManager.INSTALLDIR); - Console.WriteLine("The base version is \"{0}\" ({1}) and is installed in: {2}", UpdaterManager.BaseVersion.Displayname, UpdaterManager.BaseVersion.Version, UpdaterManager.InstalledBaseDir); + Console.WriteLine("Machine settings are installed in: {0}", UpdaterManager.UPDATEDIR); Console.WriteLine("This version is \"{0}\" ({1}) and is installed in: {2}", UpdaterManager.SelfVersion.Displayname, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version, System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)); Console.WriteLine(); } diff --git a/Duplicati/Library/AutoUpdater/UpdateInfo.cs b/Duplicati/Library/AutoUpdater/UpdateInfo.cs index af6509c46..e2739a9b1 100644 --- a/Duplicati/Library/AutoUpdater/UpdateInfo.cs +++ b/Duplicati/Library/AutoUpdater/UpdateInfo.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2024, The Duplicati Team +// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a @@ -20,6 +20,7 @@ // DEALINGS IN THE SOFTWARE. using System; +using System.Linq; namespace Duplicati.Library.AutoUpdater { @@ -48,23 +49,86 @@ namespace Duplicati.Library.AutoUpdater public class UpdateInfo { + public string UpdateFromV1Url; + public string UpdateFromV2Url; public string Displayname; public string Version; public DateTime ReleaseTime; public string ReleaseType; public string UpdateSeverity; public string ChangeInfo; - public long CompressedSize; - public long UncompressedSize; - public string SHA256; - public string MD5; - public string[] RemoteURLS; - public FileEntry[] Files; + public int PackageUpdaterVersion; + /// + /// List of installer packages + /// + public PackageEntry[] Packages; + /// + /// Link to a generic download page + /// + public string GenericUpdatePageUrl; - public UpdateInfo Clone() + /// + /// Finds a package that matches the + /// + /// The package type id; null uses the currently installed package type id + /// The matching package or null + public PackageEntry? FindPackage(string packageTypeId = null) { - return (UpdateInfo)this.MemberwiseClone(); + if (!string.IsNullOrWhiteSpace(UpdateFromV2Url) || PackageUpdaterVersion != UpdaterManager.SUPPORTED_PACKAGE_UPDATER_VERSION) + return null; + + packageTypeId ??= UpdaterManager.PackageTypeId; + if (string.IsNullOrWhiteSpace(packageTypeId) || Packages == null) + return null; + + return Packages.FirstOrDefault(x => string.Equals(x.PackageTypeId, packageTypeId, StringComparison.OrdinalIgnoreCase)); } + + /// + /// Gets the generic update page url for the + /// + /// The package type id; null uses the currently installed package type id + /// The generic update page url + public string GetGenericUpdatePageUrl(string packageTypeId = null) + { + var baseurl = string.IsNullOrWhiteSpace(UpdateFromV2Url) + ? GenericUpdatePageUrl + : UpdateFromV2Url; + + packageTypeId ??= UpdaterManager.PackageTypeId; + if (string.IsNullOrWhiteSpace(packageTypeId)) + return GenericUpdatePageUrl; + + return GenericUpdatePageUrl + $"{(GenericUpdatePageUrl.IndexOf('?') > 0 ? "&" : "?")}packagetypeid={Uri.EscapeDataString(packageTypeId ?? "")}"; + } + + /// + /// Gets the updated package urls for the + /// + /// The package type id; null uses the currently installed package type id + /// The matching update urls + public string[] GetUpdateUrls(string packageTypeId = null) + { + packageTypeId ??= UpdaterManager.PackageTypeId; + var package = FindPackage(packageTypeId); + if (package != null) + return package.RemoteUrls; + + var generic = GetGenericUpdatePageUrl(packageTypeId); + if (!string.IsNullOrWhiteSpace(generic)) + return [generic]; + + return Array.Empty(); + } + + /// + /// Creates a copy of the instance by serializing and deseriaizing the data + /// + /// A cloned copy + public UpdateInfo Clone() + => System.Text.Json.JsonSerializer.Deserialize( + System.Text.Json.JsonSerializer.Serialize(this) + ); } } diff --git a/Duplicati/Library/AutoUpdater/UpdaterManager.cs b/Duplicati/Library/AutoUpdater/UpdaterManager.cs index 6dc7c80d1..60b7f09c4 100644 --- a/Duplicati/Library/AutoUpdater/UpdaterManager.cs +++ b/Duplicati/Library/AutoUpdater/UpdaterManager.cs @@ -23,193 +23,131 @@ using System; using System.Linq; using System.Collections.Generic; using System.IO; -using System.Runtime.ExceptionServices; -using System.Threading.Tasks; -using Duplicati.Library.Interface; -using Duplicati.Library.Common.IO; -using Duplicati.Library.Common; using Duplicati.Library.Utility; +using Duplicati.Library.Common; using System.Diagnostics; namespace Duplicati.Library.AutoUpdater { - public enum AutoUpdateStrategy - { - CheckBefore, - CheckDuring, - CheckAfter, - InstallBefore, - InstallDuring, - InstallAfter, - Never - } - public static class UpdaterManager { /// - /// The magic exit code that signals an update has been installed and that the app should restart + /// The RSA key used to sign the manifest /// - public const int MAGIC_EXIT_CODE = 126; - private static readonly System.Security.Cryptography.RSACryptoServiceProvider SIGN_KEY = AutoUpdateSettings.SignKey; + /// + /// Urls to check for updated packages + /// private static readonly string[] MANIFEST_URLS = AutoUpdateSettings.URLs; + /// + /// The app name to show + /// private static readonly string APPNAME = AutoUpdateSettings.AppName; - - public static readonly string INSTALLDIR; - - private static readonly string INSTALLED_BASE_DIR = - string.IsNullOrWhiteSpace(System.Environment.GetEnvironmentVariable(string.Format(BASEINSTALLDIR_ENVNAME_TEMPLATE, APPNAME))) - ? System.IO.Path.GetDirectoryName(Duplicati.Library.Utility.Utility.getEntryAssembly().Location) - : Environment.ExpandEnvironmentVariables(System.Environment.GetEnvironmentVariable(string.Format(BASEINSTALLDIR_ENVNAME_TEMPLATE, APPNAME))); - - private static readonly bool DISABLE_UPDATE_DOMAIN = Debugger.IsAttached || Utility.Utility.ParseBool(Environment.GetEnvironmentVariable(string.Format(SKIPUPDATE_ENVNAME_TEMPLATE, APPNAME)), false); - - public static bool RequiresRespawn { get; set; } - - public static bool IgnoreWebrootFolder { get; set; } - - private static KeyValuePair? m_hasUpdateInstalled; - + /// + /// The version that the updater supports + /// + public const int SUPPORTED_PACKAGE_UPDATER_VERSION = 2; + /// + /// The folder where the machine id is placed + /// + public static readonly string UPDATEDIR; + /// + /// The directory where the program is running from + /// + public static readonly string INSTALLATIONDIR; + /// + /// Env variable that allows fully disabling all update checks + /// + public static readonly bool DISABLE_UPDATE_CHECK = Debugger.IsAttached || Utility.Utility.ParseBool(Environment.GetEnvironmentVariable(string.Format(SKIPUPDATE_ENVNAME_TEMPLATE, APPNAME)), false); + /// + /// The update information for the running version + /// public static readonly UpdateInfo SelfVersion; - public static readonly UpdateInfo BaseVersion; - + /// + /// Event trigger for errors on update + /// public static event Action OnError; - private const string DATETIME_FORMAT = "yyyymmddhhMMss"; - private const string BASEINSTALLDIR_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_INSTALL_ROOT"; - private const string UPDATEINSTALLDIR_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_UPDATE_ROOT"; - public const string SKIPUPDATE_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_SKIP_UPDATE"; - private const string RUN_UPDATED_FOLDER_PATH = "AUTOUPDATER_LOAD_UPDATE"; - private const string SLEEP_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_SLEEP"; - public const string UPDATE_STRATEGY_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_POLICY"; - private const string UPDATE_MANIFEST_FILENAME = "autoupdate.manifest"; - private const string README_FILE = "README.txt"; - private const string INSTALL_FILE = "installation.txt"; - private const string CURRENT_FILE = "current"; - /// - /// Gets the original directory that this application was installed into + /// Common formatting string for date-time values /// - /// The original directory that this application was installed into - public static string InstalledBaseDir { get { return INSTALLED_BASE_DIR; } } + private const string DATETIME_FORMAT = "yyyymmddhhMMss"; + /// + /// The template for the environment variable name that allows an overriden root folder + /// + private const string UPDATEINSTALLDIR_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_UPDATE_ROOT"; + /// + /// The template for the environment variable that toggles disabling updates + /// + public const string SKIPUPDATE_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_SKIP_UPDATE"; + /// + /// The name of the file that contains the manifest, located in the folder + /// + private const string UPDATE_MANIFEST_FILENAME = "autoupdate.manifest"; + /// + /// The name of the file that contains the package type id, located in the folder + /// + private const string PACKAGE_TYPE_FILE = "package_type_id.txt"; + /// + /// The README file stored in the folder, explaining what the folder is for + /// + private const string README_FILE = "README.txt"; + /// + /// The installation ID filename stored in + /// + private const string INSTALL_FILE = "installation.txt"; /// /// Gets the last version found from an update /// public static UpdateInfo LastUpdateCheckVersion { get; private set; } + /// + /// Performs static initialization of the update manager, populating the readonly fields of the manager + /// static UpdaterManager() { - // Update folder strategy is a bit complicated, - // because it depends on the actual system, - // and because it tries to find a good spot - // by probing for locations - - // The "overrides" paths are checked, - // to see if they exist and are writeable. - // The first existing and writeable path - // for "overrides" is chosen - - // If override was not found, the "legacypaths" - // are checked in the same way to see if - // we have previously used such a folder - // and if that folder has contents, - // which indicates that it has been used. - - // Finally we check the "attempts", - // which are suitable candidates - // for storing the updates on each - // operating system + // Set the installation path + INSTALLATIONDIR = Path.GetDirectoryName(Duplicati.Library.Utility.Utility.getEntryAssembly().Location); + // Check for override if (string.IsNullOrWhiteSpace(System.Environment.GetEnvironmentVariable(string.Format(UPDATEINSTALLDIR_ENVNAME_TEMPLATE, APPNAME)))) { - string installdir = null; - var programfiles = System.Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); - - // The user can override updates by having a local updates folder - var overrides = new List(new string[] { - System.IO.Path.Combine(InstalledBaseDir, "updates"), - }); - + // OS specific folders for probing + var candidates = new List(); if (Platform.IsClientWindows) { - overrides.Add(System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), APPNAME, "updates")); - overrides.Add(System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), APPNAME, "updates")); + candidates.Add(System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), APPNAME, "updates")); + candidates.Add(System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), APPNAME, "updates")); } else { if (Platform.IsClientOSX) - overrides.Add(System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Library", "Application Support", APPNAME, "updates")); + candidates.Add(System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Library", "Application Support", APPNAME, "updates")); - overrides.Add(System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), APPNAME, "updates")); + candidates.Add(System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), APPNAME, "updates")); } - // Previous locations that we don't want to use, - // but we keep them active to avoid breaking the update system - var legacypaths = new List(); - - if (!string.IsNullOrWhiteSpace(programfiles)) - legacypaths.Add(System.IO.Path.Combine(programfiles, APPNAME, "updates")); - if (Platform.IsClientPosix) - legacypaths.Add(System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), APPNAME, "updates")); - - // The real attempts that we probe for - var attempts = new List(); - - // We do not want to install anything in the basedir, if the application is installed in "ProgramFiles" - if (!string.IsNullOrWhiteSpace(programfiles) && !InstalledBaseDir.StartsWith(Util.AppendDirSeparator(programfiles), StringComparison.Ordinal)) - attempts.Add(System.IO.Path.Combine(InstalledBaseDir, "updates")); - - if (Platform.IsClientOSX) - attempts.Add(System.IO.Path.Combine("/", "Library", "Application Support", APPNAME, "updates")); - else - attempts.Add(System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), APPNAME, "updates")); - - attempts.AddRange(overrides.Skip(1)); - - // Check if the override folder exists, and choose that - foreach (var p in overrides) - if (!string.IsNullOrWhiteSpace(p) && System.IO.Directory.Exists(p) && TestDirectoryIsWriteable(p)) - { - installdir = p; - break; - } - - if (string.IsNullOrWhiteSpace(installdir)) - foreach (var p in legacypaths) - if (!string.IsNullOrWhiteSpace(p) && System.IO.Directory.Exists(p) && System.IO.Directory.EnumerateFiles(p, "*", System.IO.SearchOption.TopDirectoryOnly).Any() && TestDirectoryIsWriteable(p)) - { - installdir = p; - break; - } - - if (string.IsNullOrWhiteSpace(installdir)) - foreach (var p in attempts) - if (!string.IsNullOrWhiteSpace(p) && TestDirectoryIsWriteable(p)) - { - installdir = p; - break; - } - - INSTALLDIR = installdir; + // Find the first writeable directory in the list + UPDATEDIR = candidates.FirstOrDefault(p => !string.IsNullOrWhiteSpace(p) && System.IO.Directory.Exists(p) && TestDirectoryIsWriteable(p)); } else { - INSTALLDIR = Environment.ExpandEnvironmentVariables(System.Environment.GetEnvironmentVariable(string.Format(UPDATEINSTALLDIR_ENVNAME_TEMPLATE, APPNAME))); + // Use override, no checks + UPDATEDIR = Environment.ExpandEnvironmentVariables(System.Environment.GetEnvironmentVariable(string.Format(UPDATEINSTALLDIR_ENVNAME_TEMPLATE, APPNAME))); } - - if (INSTALLDIR != null) + if (!string.IsNullOrWhiteSpace(UPDATEDIR)) { - if (!System.IO.File.Exists(System.IO.Path.Combine(INSTALLDIR, README_FILE))) - System.IO.File.WriteAllText(System.IO.Path.Combine(INSTALLDIR, README_FILE), AutoUpdateSettings.UpdateFolderReadme); - if (!System.IO.File.Exists(System.IO.Path.Combine(INSTALLDIR, INSTALL_FILE))) - System.IO.File.WriteAllText(System.IO.Path.Combine(INSTALLDIR, INSTALL_FILE), AutoUpdateSettings.UpdateInstallFileText); + if (!System.IO.File.Exists(System.IO.Path.Combine(UPDATEDIR, README_FILE))) + System.IO.File.WriteAllText(System.IO.Path.Combine(UPDATEDIR, README_FILE), AutoUpdateSettings.UpdateFolderReadme); + if (!System.IO.File.Exists(System.IO.Path.Combine(UPDATEDIR, INSTALL_FILE))) + System.IO.File.WriteAllText(System.IO.Path.Combine(UPDATEDIR, INSTALL_FILE), AutoUpdateSettings.UpdateInstallFileText); } + // Attempt to read the installed manifest file UpdateInfo selfVersion = null; - UpdateInfo baseVersion = null; try { selfVersion = ReadInstalledManifest(System.IO.Path.GetDirectoryName(Duplicati.Library.Utility.Utility.getEntryAssembly().Location)); @@ -218,17 +156,10 @@ namespace Duplicati.Library.AutoUpdater { } - try - { - baseVersion = ReadInstalledManifest(InstalledBaseDir); - } - catch - { - } - + // In case the installed manifest is broken, try to set some sane values if (selfVersion == null) { - selfVersion = new UpdateInfo() { + SelfVersion = new UpdateInfo() { Displayname = string.IsNullOrWhiteSpace(Duplicati.License.VersionNumbers.TAG) ? "Current" : Duplicati.License.VersionNumbers.TAG, Version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(), ReleaseTime = new DateTime(0), @@ -239,15 +170,7 @@ namespace Duplicati.Library.AutoUpdater string.IsNullOrWhiteSpace(AutoUpdateSettings.BuildUpdateChannel) ? "Nightly" : AutoUpdateSettings.BuildUpdateChannel #endif }; - - } - - if (baseVersion == null) - baseVersion = selfVersion; - - SelfVersion = selfVersion; - BaseVersion = baseVersion; } public static Version TryParseVersion(string str) @@ -259,28 +182,6 @@ namespace Duplicati.Library.AutoUpdater return new Version(0, 0); } - public static bool HasUpdateInstalled - { - get - { - if (!m_hasUpdateInstalled.HasValue) - { - var selfversion = TryParseVersion(SelfVersion.Version); - - m_hasUpdateInstalled = - (from n in FindInstalledVersions() - let nversion = TryParseVersion(n.Value.Version) - let newerVersion = selfversion < nversion - where newerVersion && VerifyUnpackedFolder(n.Key, n.Value) - orderby nversion descending - select n) - .FirstOrDefault(); - } - - return m_hasUpdateInstalled.Value.Value != null; - } - } - private static bool TestDirectoryIsWriteable(string path) { var p2 = System.IO.Path.Combine(path, "test-" + DateTime.UtcNow.ToString(DATETIME_FORMAT, System.Globalization.CultureInfo.InvariantCulture)); @@ -303,17 +204,39 @@ namespace Duplicati.Library.AutoUpdater return false; } + /// + /// The unique machine installation ID + /// public static string InstallID { get { - try { return System.IO.File.ReadAllText(System.IO.Path.Combine(INSTALLDIR, INSTALL_FILE)).Replace('\r', '\n').Split(new char[] { '\n' }).FirstOrDefault().Trim() ?? ""; } + try { return System.IO.File.ReadAllLines(System.IO.Path.Combine(UPDATEDIR, INSTALL_FILE)).FirstOrDefault(x => !string.IsNullOrWhiteSpace(x))?.Trim() ?? ""; } catch { } return ""; } } + /// + /// The package type ID + /// + public static string PackageTypeId + { + get + { + try { return System.IO.File.ReadAllLines(System.IO.Path.Combine(INSTALLATIONDIR, PACKAGE_TYPE_FILE)).FirstOrDefault(x => !string.IsNullOrWhiteSpace(x))?.Trim() ?? ""; } + catch { } + +#if DEBUG + return "debug"; +#else + return ""; +#endif + } + } + + public static UpdateInfo CheckForUpdate(ReleaseType channel = ReleaseType.Unknown) { if (channel == ReleaseType.Unknown) @@ -410,27 +333,12 @@ namespace Duplicati.Library.AutoUpdater return null; } - public static IEnumerable> FindInstalledVersions() + public static bool DownloadUpdate(UpdateInfo version, PackageEntry package, string targetPath, Action progress = null) { - var res = new List>(); - if (INSTALLDIR != null) - foreach (var folder in SystemIO.IO_OS.GetDirectories(INSTALLDIR)) - { - var r = ReadInstalledManifest(folder); - if (r != null) - res.Add(new KeyValuePair(folder, r)); - } - - return res; - } - - public static bool DownloadAndUnpackUpdate(UpdateInfo version, Action progress = null) - { - if (INSTALLDIR == null) + if (UPDATEDIR == null) return false; - - var updates = version.RemoteURLS.ToList(); + var updates = package.RemoteUrls.ToList(); // If alternate update URLs are specified, // we look for packages there as well @@ -460,7 +368,7 @@ namespace Duplicati.Library.AutoUpdater { Action cb = null; if (progress != null) - cb = (s) => { progress(Math.Min(1.0, Math.Max(0.0, (double)s / version.CompressedSize))); }; + cb = (s) => { progress(Math.Min(1.0, Math.Max(0.0, (double)s / package.Length))); }; var wreq = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(url); wreq.UserAgent = string.Format("{0} v{1}", APPNAME, SelfVersion.Version); @@ -470,98 +378,28 @@ namespace Duplicati.Library.AutoUpdater using (var resp = areq.GetResponse()) using (var rss = areq.GetResponseStream()) using (var pgs = new Duplicati.Library.Utility.ProgressReportingStream(rss, cb)) - { Duplicati.Library.Utility.Utility.CopyStream(pgs, tempfile); - } var sha256 = System.Security.Cryptography.SHA256.Create(); var md5 = System.Security.Cryptography.MD5.Create(); - if (tempfile.Length != version.CompressedSize) - throw new Exception(string.Format("Invalid file size {0}, expected {1} for {2}", tempfile.Length, version.CompressedSize, url)); + if (tempfile.Length != package.Length) + throw new Exception(string.Format("Invalid file size {0}, expected {1} for {2}", tempfile.Length, package.Length, url)); tempfile.Position = 0; var sha256hash = Convert.ToBase64String(sha256.ComputeHash(tempfile)); - if (sha256hash != version.SHA256) + if (sha256hash != package.SHA256) throw new Exception(string.Format("Damaged or corrupted file, sha256 mismatch for {0}", url)); tempfile.Position = 0; var md5hash = Convert.ToBase64String(md5.ComputeHash(tempfile)); - if (md5hash != version.MD5) - throw new Exception(string.Format("Damaged or corrupted file, md5 mismatch for {0}", url)); - - tempfile.Position = 0; - using (var tempfolder = new Duplicati.Library.Utility.TempFolder()) - using (ICompression zip = new Duplicati.Library.Compression.FileArchiveZip(tempfile, ArchiveMode.Read, new Dictionary())) - { - foreach (var file in zip.ListFilesWithSize("")) - { - if (System.IO.Path.IsPathRooted(file.Key) || file.Key.Trim().StartsWith("..", StringComparison.OrdinalIgnoreCase)) - throw new Exception(string.Format("Out-of-place file path detected: {0}", file.Key)); - - var targetpath = System.IO.Path.Combine(tempfolder, file.Key); - var targetfolder = System.IO.Path.GetDirectoryName(targetpath); - if (!System.IO.Directory.Exists(targetfolder)) - System.IO.Directory.CreateDirectory(targetfolder); - - using (var zs = zip.OpenRead(file.Key)) - using (var fs = System.IO.File.Create(targetpath)) - zs.CopyTo(fs); - } - - if (VerifyUnpackedFolder(tempfolder, version)) - { - var versionstring = TryParseVersion(version.Version).ToString(); - var targetfolder = System.IO.Path.Combine(INSTALLDIR, versionstring); - if (System.IO.Directory.Exists(targetfolder)) - System.IO.Directory.Delete(targetfolder, true); - - System.IO.Directory.CreateDirectory(targetfolder); - - var tempfolderpath = Util.AppendDirSeparator(tempfolder); - var tempfolderlength = tempfolderpath.Length; - - // Would be nice, but does not work :( - //System.IO.Directory.Move(tempfolder, targetfolder); - - foreach (var e in Duplicati.Library.Utility.Utility.EnumerateFileSystemEntries(tempfolder)) - { - var relpath = e.Substring(tempfolderlength); - if (string.IsNullOrWhiteSpace(relpath)) - continue; - - var fullpath = System.IO.Path.Combine(targetfolder, relpath); - if (relpath.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal)) - System.IO.Directory.CreateDirectory(fullpath); - else - System.IO.File.Copy(e, fullpath); - } - - // Verification will kick in when we list the installed updates - //VerifyUnpackedFolder(targetfolder, version); - System.IO.File.WriteAllText(System.IO.Path.Combine(INSTALLDIR, CURRENT_FILE), versionstring); - - m_hasUpdateInstalled = null; - - var obsolete = (from n in FindInstalledVersions() - where n.Value.Version != version.Version && n.Value.Version != SelfVersion.Version - let x = TryParseVersion(n.Value.Version) - orderby x descending - select n).Skip(1).ToArray(); - - foreach (var f in obsolete) - try { System.IO.Directory.Delete(f.Key, true); } - catch { } - - return true; - } - else - { - throw new Exception(string.Format("Unable to verify unpacked folder for url: {0}", url)); - } - } + if (md5hash != package.MD5) + throw new Exception(string.Format("Damaged or corrupted file, md5 mismatch for {0}", url)); } + + File.Copy(tempfilename, targetPath, true); + return true; } catch (Exception ex) { @@ -574,130 +412,16 @@ namespace Duplicati.Library.AutoUpdater return false; } - public static bool VerifyUnpackedFolder(string folder, UpdateInfo version = null) - { - try - { - UpdateInfo update; - FileEntry manifest; - - var sha256 = System.Security.Cryptography.SHA256.Create(); - var md5 = System.Security.Cryptography.MD5.Create(); - - using (var fs = System.IO.File.OpenRead(System.IO.Path.Combine(folder, UPDATE_MANIFEST_FILENAME))) - { - using (var ss = new SignatureReadingStream(fs, SIGN_KEY)) - using (var tr = new System.IO.StreamReader(ss)) - using (var jr = new Newtonsoft.Json.JsonTextReader(tr)) - update = new Newtonsoft.Json.JsonSerializer().Deserialize(jr); - - sha256.Initialize(); - md5.Initialize(); - - fs.Position = 0; - var h1 = Convert.ToBase64String(sha256.ComputeHash(fs)); - fs.Position = 0; - var h2 = Convert.ToBase64String(md5.ComputeHash(fs)); - - manifest = new FileEntry() { - Path = UPDATE_MANIFEST_FILENAME, - Ignore = false, - LastWriteTime = update.ReleaseTime, - SHA256 = h1, - MD5 = h2 - }; - } - - if (version != null && (update.Displayname != version.Displayname || update.ReleaseTime != version.ReleaseTime)) - throw new Exception("The found version was not the expected version"); - - var paths = update.Files.Where(x => !x.Ignore).ToDictionary(x => x.Path.Replace('/', System.IO.Path.DirectorySeparatorChar), Library.Utility.Utility.ClientFilenameStringComparer); - paths.Add(manifest.Path, manifest); - - var ignores = (from x in update.Files where x.Ignore select Util.AppendDirSeparator(x.Path.Replace('/', System.IO.Path.DirectorySeparatorChar))).ToList(); - - folder = Util.AppendDirSeparator(folder); - var baselen = folder.Length; - - foreach (var file in Library.Utility.Utility.EnumerateFileSystemEntries(folder)) - { - var relpath = file.Substring(baselen); - if (string.IsNullOrWhiteSpace(relpath)) - continue; - - if (IgnoreWebrootFolder && relpath.StartsWith("webroot", Library.Utility.Utility.ClientFilenameStringComparison)) - continue; - - FileEntry fe; - if (!paths.TryGetValue(relpath, out fe)) - { - var ignore = false; - foreach (var c in ignores) - if (ignore = relpath.StartsWith(c, Library.Utility.Utility.ClientFilenameStringComparison)) - break; - - if (ignore) - continue; - - throw new Exception(string.Format("Found unexpected file: {0}", file)); - } - - paths.Remove(relpath); - - if (fe.Path.EndsWith("/", StringComparison.Ordinal)) - continue; - - sha256.Initialize(); - md5.Initialize(); - - using (var fs = System.IO.File.OpenRead(file)) - { - if (Convert.ToBase64String(sha256.ComputeHash(fs)) != fe.SHA256) - throw new Exception(string.Format("Invalid sha256 hash for file: {0}", file)); - - fs.Position = 0; - if (Convert.ToBase64String(md5.ComputeHash(fs)) != fe.MD5) - throw new Exception(string.Format("Invalid md5 hash for file: {0}", file)); - } - } - - var filteredpaths = paths - .Where(p => !string.IsNullOrWhiteSpace(p.Key) && !p.Key.EndsWith("/", StringComparison.Ordinal)) - .Where(p => !IgnoreWebrootFolder || !p.Key.StartsWith("webroot", Library.Utility.Utility.ClientFilenameStringComparison)) - .Select(p => p.Key) - .ToList(); - - if (filteredpaths.Count == 1) - throw new Exception(string.Format("Folder {0} is missing: {1}", folder, filteredpaths.First())); - else if (filteredpaths.Count > 0) - throw new Exception(string.Format("Folder {0} is missing {1} and {2} other file(s)", folder, filteredpaths.First(), filteredpaths.Count - 1)); - - return true; - } - catch (Exception ex) - { - if (OnError != null) - OnError(ex); - } - - return false; - } - - public static bool SetRunUpdate() - { - if (HasUpdateInstalled) - { - AppDomain.CurrentDomain.SetData(RUN_UPDATED_FOLDER_PATH, m_hasUpdateInstalled.Value.Key); - return true; - } - - return false; - } - - public static void CreateUpdatePackage(System.Security.Cryptography.RSACryptoServiceProvider key, string inputfolder, string outputfolder, string manifest = null) + /// + /// Helper method to create a signed manifest file + /// + /// + /// + /// + /// + public static void CreateSignedManifest(System.Security.Cryptography.RSACryptoServiceProvider key, string inputfolder, string outputfolder, string manifest = null) { // Read the existing manifest - UpdateInfo remoteManifest; var manifestpath = manifest ?? System.IO.Path.Combine(inputfolder, UPDATE_MANIFEST_FILENAME); @@ -707,134 +431,26 @@ namespace Duplicati.Library.AutoUpdater using (var jr = new Newtonsoft.Json.JsonTextReader(sr)) remoteManifest = new Newtonsoft.Json.JsonSerializer().Deserialize(jr); - if (remoteManifest.Files == null) - remoteManifest.Files = new FileEntry[0]; - if (remoteManifest.ReleaseTime.Ticks == 0) remoteManifest.ReleaseTime = DateTime.UtcNow; + + // No files to update with are allowed, as we currently do not use the information + if (remoteManifest.Packages == null) + remoteManifest.Packages = Array.Empty(); - var ignoreFiles = (from n in remoteManifest.Files - where n.Ignore - select n).ToArray(); - - var ignoreMap = ignoreFiles.ToDictionary(k => k.Path, k => "", Duplicati.Library.Utility.Utility.ClientFilenameStringComparer); - - remoteManifest.MD5 = null; - remoteManifest.SHA256 = null; - remoteManifest.Files = null; - remoteManifest.UncompressedSize = 0; - - var localManifest = remoteManifest.Clone(); - localManifest.RemoteURLS = null; - - inputfolder = Util.AppendDirSeparator(inputfolder); - var baselen = inputfolder.Length; - var dirsep = Util.DirectorySeparatorString; - - ignoreMap.Add(UPDATE_MANIFEST_FILENAME, ""); - - var md5 = System.Security.Cryptography.MD5.Create(); - var sha256 = System.Security.Cryptography.SHA256.Create(); - - Func computeStreamMD5 = (stream) => - { - md5.Initialize(); - return Convert.ToBase64String(md5.ComputeHash(stream)); - }; - - Func computeStreamSHA256 = (stream) => - { - sha256.Initialize(); - return Convert.ToBase64String(sha256.ComputeHash(stream)); - }; - - Func computeMD5 = (path) => - { - using (Stream fs = System.IO.File.OpenRead(path)) - return computeStreamMD5(fs); - }; - - Func computeSHA256 = (path) => - { - using (Stream fs = System.IO.File.OpenRead(path)) - return computeStreamSHA256(fs); - }; - - // Build a zip - using (var archive_temp_file = new Duplicati.Library.Utility.TempFile()) - { - using (var archive_temp = System.IO.File.Open(archive_temp_file, FileMode.Create, FileAccess.ReadWrite, FileShare.None)) - { - using (ICompression zipfile = new Duplicati.Library.Compression.FileArchiveZip(archive_temp, ArchiveMode.Write, new Dictionary())) - { - Func addToArchive = (path, relpath) => - { - if (ignoreMap.ContainsKey(relpath)) - return false; - - if (path.EndsWith(dirsep, StringComparison.Ordinal)) - return true; - - using (var source = System.IO.File.OpenRead(path)) - using (var target = zipfile.CreateFile(relpath, - Duplicati.Library.Interface.CompressionHint.Compressible, - System.IO.File.GetLastAccessTimeUtc(path))) - { - source.CopyTo(target); - remoteManifest.UncompressedSize += source.Length; - } - - return true; - }; - - // Build the update manifest - localManifest.Files = - (from fse in Duplicati.Library.Utility.Utility.EnumerateFileSystemEntries(inputfolder) - let relpath = fse.Substring(baselen) - where addToArchive(fse, relpath) - select new FileEntry() - { - Path = relpath, - LastWriteTime = System.IO.File.GetLastAccessTimeUtc(fse), - MD5 = fse.EndsWith(dirsep, StringComparison.Ordinal) ? null : computeMD5(fse), - SHA256 = fse.EndsWith(dirsep, StringComparison.Ordinal) ? null : computeSHA256(fse) - }) - .Union(ignoreFiles).ToArray(); - - // Write a signed manifest with the files - - using (var ms = new System.IO.MemoryStream()) - using (var sw = new System.IO.StreamWriter(ms)) - { - new Newtonsoft.Json.JsonSerializer().Serialize(sw, localManifest); - sw.Flush(); - - using (var ms2 = new System.IO.MemoryStream()) - { - SignatureReadingStream.CreateSignedStream(ms, ms2, key); - ms2.Position = 0; - using (var sigfile = zipfile.CreateFile(UPDATE_MANIFEST_FILENAME, - Duplicati.Library.Interface.CompressionHint.Compressible, - DateTime.UtcNow)) - ms2.CopyTo(sigfile); - - } - } - } - - remoteManifest.CompressedSize = archive_temp.Length; - - archive_temp.Position = 0; - remoteManifest.MD5 = computeStreamMD5(archive_temp); - - archive_temp.Position = 0; - remoteManifest.SHA256 = computeStreamSHA256(archive_temp); - } - System.IO.File.Move(archive_temp_file, System.IO.Path.Combine(outputfolder, "package.zip")); - } + if (string.IsNullOrWhiteSpace(remoteManifest.UpdateFromV1Url)) + remoteManifest.UpdateFromV1Url = remoteManifest.GenericUpdatePageUrl; + + if (string.IsNullOrWhiteSpace(remoteManifest.UpdateFromV1Url)) + throw new Exception($"Field must be set: {nameof(remoteManifest.UpdateFromV1Url)}"); + if (string.IsNullOrWhiteSpace(remoteManifest.GenericUpdatePageUrl)) + throw new Exception($"Field must be set: {nameof(remoteManifest.GenericUpdatePageUrl)}"); + if (string.IsNullOrWhiteSpace(remoteManifest.Version)) + throw new Exception($"Field must be set: {nameof(remoteManifest.Version)}"); + if (string.IsNullOrWhiteSpace(remoteManifest.ReleaseType)) + throw new Exception($"Field must be set: {nameof(remoteManifest.ReleaseType)}"); // Write a signed manifest for upload - using (var tf = new Duplicati.Library.Utility.TempFile()) { using (var ms = new System.IO.MemoryStream()) @@ -849,327 +465,6 @@ namespace Duplicati.Library.AutoUpdater System.IO.File.Move(tf, System.IO.Path.Combine(outputfolder, UPDATE_MANIFEST_FILENAME)); } - - } - - private static void WrapWithUpdater(AutoUpdateStrategy defaultstrategy, Action wrappedFunction) - { - string optstr = Environment.GetEnvironmentVariable(string.Format(UPDATE_STRATEGY_ENVNAME_TEMPLATE, APPNAME)); - AutoUpdateStrategy strategy; - if (string.IsNullOrWhiteSpace(optstr) || !Enum.TryParse(optstr, true, out strategy)) - strategy = defaultstrategy; - - System.Threading.Thread backgroundChecker = null; - UpdateInfo updateDetected = null; - bool updateInstalled = false; - - bool checkForUpdate; - bool downloadUpdate; - bool runAfter; - bool runDuring; - bool runBefore; - - - switch (strategy) - { - case AutoUpdateStrategy.CheckBefore: - case AutoUpdateStrategy.CheckDuring: - case AutoUpdateStrategy.CheckAfter: - checkForUpdate = true; - downloadUpdate = false; - break; - - case AutoUpdateStrategy.InstallBefore: - case AutoUpdateStrategy.InstallDuring: - case AutoUpdateStrategy.InstallAfter: - checkForUpdate = true; - downloadUpdate = true; - break; - - default: - checkForUpdate = false; - downloadUpdate = false; - break; - } - - switch (strategy) - { - case AutoUpdateStrategy.CheckBefore: - case AutoUpdateStrategy.InstallBefore: - runBefore = true; - runDuring = false; - runAfter = false; - break; - - case AutoUpdateStrategy.CheckAfter: - case AutoUpdateStrategy.InstallAfter: - runBefore = false; - runDuring = false; - runAfter = true; - break; - - case AutoUpdateStrategy.CheckDuring: - case AutoUpdateStrategy.InstallDuring: - runBefore = false; - runDuring = true; - runAfter = false; - break; - - default: - runBefore = false; - runDuring = false; - runAfter = false; - break; - } - - if (checkForUpdate) - { - backgroundChecker = new System.Threading.Thread(() => - { - // Don't run "during" if the task is short - if (runDuring) - System.Threading.Thread.Sleep(TimeSpan.FromSeconds(10)); - - updateDetected = CheckForUpdate(); - if (updateDetected != null && downloadUpdate) - { - if (!runDuring) - Console.WriteLine("Update to {0} detected, installing...", updateDetected.Displayname); - updateInstalled = DownloadAndUnpackUpdate(updateDetected); - } - }); - - backgroundChecker.IsBackground = true; - backgroundChecker.Name = "BackgroundUpdateChecker"; - - if (!runAfter) - backgroundChecker.Start(); - - if (runBefore) - { - Console.WriteLine("Checking for update ..."); - backgroundChecker.Join(); - - if (downloadUpdate) - { - if (updateInstalled) - Console.WriteLine("Install succeeded, running updated version"); - else - Console.WriteLine("Install or download failed, using current version"); - } - else if (updateDetected != null) - { - Console.WriteLine("Update \"{0}\" detected", updateDetected.Displayname); - } - - backgroundChecker = null; - } - } - - wrappedFunction(); - - if (backgroundChecker != null && runAfter) - { - Console.WriteLine("Checking for update ..."); - - backgroundChecker.Start(); - backgroundChecker.Join(); - } - - if (backgroundChecker != null && updateDetected != null) - { - if (backgroundChecker.IsAlive) - { - Console.WriteLine("Waiting for update \"{0}\" to complete", updateDetected.Displayname); - backgroundChecker.Join(); - } - - if (downloadUpdate) - { - if (updateInstalled) - Console.WriteLine("Install succeeded, running updated version on next launch"); - else - Console.WriteLine("Install or download failed, using current version on next launch"); - } - else - { - Console.WriteLine("Update \"{0}\" detected", updateDetected.Displayname); - } - } - } - - private static int RunMethod(System.Reflection.MethodInfo method, string[] args) - { - try - { - var n = method.Invoke(null, new object[] { args }); - if (method.ReturnType == typeof(int)) - return (int)n; - - return 0; - } - catch (System.Reflection.TargetInvocationException tex) - { - try - { - Console.WriteLine("Crash! {0}{1}", Environment.NewLine, tex); - } - catch - { - } - - try - { - var report_file = System.IO.Path.Combine( - string.IsNullOrEmpty(INSTALLDIR) ? Library.Utility.TempFolder.SystemTempPath : INSTALLDIR, - string.Format("{0}-crashlog.txt", AutoUpdateSettings.AppName) - ); - - System.IO.File.WriteAllText(report_file, tex.ToString()); - } - catch - { - } - - if (tex.InnerException != null) - { - // Unwrap exceptions for nicer display. The ExceptionDispatchInfo class allows us to - // rethrow an exception without changing the stack trace. - ExceptionDispatchInfo.Capture(tex.InnerException).Throw(); - } - - throw; - } - } - - private static KeyValuePair GetBestUpdateVersion(bool forcecheck = false) - { - if (forcecheck) - m_hasUpdateInstalled = null; - - // Check if there are updates installed, otherwise use current - KeyValuePair best = new KeyValuePair(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), SelfVersion); - - if (HasUpdateInstalled) - best = m_hasUpdateInstalled.Value; - - if (INSTALLDIR != null && System.IO.File.Exists(System.IO.Path.Combine(INSTALLDIR, CURRENT_FILE))) - { - try - { - var current = System.IO.File.ReadAllText(System.IO.Path.Combine(INSTALLDIR, CURRENT_FILE)).Trim(); - if (!string.IsNullOrWhiteSpace(current)) - { - var targetfolder = System.IO.Path.Combine(INSTALLDIR, current); - var currentmanifest = ReadInstalledManifest(targetfolder); - if (currentmanifest != null && TryParseVersion(currentmanifest.Version) > TryParseVersion(best.Value.Version) && VerifyUnpackedFolder(targetfolder, currentmanifest)) - best = new KeyValuePair(targetfolder, currentmanifest); - } - } - catch (Exception ex) - { - if (OnError != null) - OnError(ex); - } - } - - return best; - } - - public static bool IsRunningInUpdateEnvironment => !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(string.Format(BASEINSTALLDIR_ENVNAME_TEMPLATE, APPNAME))); - - public static int RunFromMostRecent(System.Reflection.MethodInfo method, string[] cmdargs, AutoUpdateStrategy defaultstrategy = AutoUpdateStrategy.CheckDuring) - { - // TODO: Disabled auto-updater as it does not currently work - return RunMethod(method, cmdargs); - //return RunFromMostRecentSpawn(method, cmdargs, defaultstrategy); - } - - public static int RunFromMostRecentSpawn(System.Reflection.MethodInfo method, string[] cmdargs, AutoUpdateStrategy defaultstrategy = AutoUpdateStrategy.CheckDuring) - { - // If the update is disabled, go straight in - if (DISABLE_UPDATE_DOMAIN) - return RunMethod(method, cmdargs); - - // If we are not the primary entry, just execute - if (IsRunningInUpdateEnvironment) - { - // For some reason this does not work - //if (Platform.IsClientWindows) - //Duplicati.Library.Utility.Win32.AttachConsole(Duplicati.Library.Utility.Win32.ATTACH_PARENT_PROCESS); - - int r = 0; - WrapWithUpdater(defaultstrategy, () => { - r = RunMethod(method, cmdargs); - }); - - return r; - } - - var app = Environment.GetCommandLineArgs().First().Substring(0, Environment.GetCommandLineArgs().First().Length - 3) + "exe"; - var args = Library.Utility.Utility.WrapAsCommandLine(Environment.GetCommandLineArgs().Skip(1), false); - - if (!Path.IsPathRooted(app)) - app = Path.Combine(InstalledBaseDir, app); - - var executable = Path.GetFileName(app); - - while (true) - { - var best = GetBestUpdateVersion(true); - var folder = best.Key; - - var pi = new System.Diagnostics.ProcessStartInfo(Path.Combine(folder, executable), args) - { - CreateNoWindow = true, - UseShellExecute = false, - ErrorDialog = false, - }; - pi.EnvironmentVariables.Clear(); - - var cur = Environment.GetEnvironmentVariables(); - foreach (var e in cur.Keys) - if (e is string s) - pi.EnvironmentVariables[s] = cur[s] as string; - - pi.EnvironmentVariables[string.Format(BASEINSTALLDIR_ENVNAME_TEMPLATE, APPNAME)] = InstalledBaseDir; - pi.EnvironmentVariables["LOCALIZATION_FOLDER"] = InstalledBaseDir; - - // On Windows, we manually redirect the streams - if (Platform.IsClientWindows) - { - pi.RedirectStandardError = true; - pi.RedirectStandardInput = true; - pi.RedirectStandardOutput = true; - } - - var proc = System.Diagnostics.Process.Start(pi); - Task tasks = null; - if (Platform.IsClientWindows) - { - // On Windows, we manually redirect the streams - tasks = Task.WhenAll( - // This does some unwanted buffering that breaks things - //Console.OpenStandardInput().CopyToAsync(proc.StandardInput.BaseStream), - Task.Run(async () => { - var stdin = new StreamReader(Console.OpenStandardInput()); - var line = string.Empty; - while ((line = await stdin.ReadLineAsync().ConfigureAwait(false)) != null) - await proc.StandardInput.WriteLineAsync(line); - }), - proc.StandardOutput.BaseStream.CopyToAsync(Console.OpenStandardOutput()), - proc.StandardError.BaseStream.CopyToAsync(Console.OpenStandardError()) - ); - } - - proc.WaitForExit(); - if (tasks != null) - tasks.Wait(1000); - - if (proc.ExitCode != MAGIC_EXIT_CODE) - return proc.ExitCode; - } - } } } diff --git a/Duplicati/Library/Main/Operation/SystemInfoHandler.cs b/Duplicati/Library/Main/Operation/SystemInfoHandler.cs index 59ccd93d3..820dbe627 100644 --- a/Duplicati/Library/Main/Operation/SystemInfoHandler.cs +++ b/Duplicati/Library/Main/Operation/SystemInfoHandler.cs @@ -39,8 +39,8 @@ namespace Duplicati.Library.Main.Operation yield return string.Format("Duplicati: {0} ({1})", Duplicati.Library.Utility.Utility.getEntryAssembly().FullName, System.Reflection.Assembly.GetExecutingAssembly().FullName); yield return string.Format("Autoupdate urls: {0}", string.Join(";", Duplicati.Library.AutoUpdater.AutoUpdateSettings.URLs)); - yield return string.Format("Update folder: {0}", Duplicati.Library.AutoUpdater.UpdaterManager.INSTALLDIR); - yield return string.Format("Base install folder: {0}", Duplicati.Library.AutoUpdater.UpdaterManager.InstalledBaseDir); + yield return string.Format("Update folder: {0}", Duplicati.Library.AutoUpdater.UpdaterManager.UPDATEDIR); + yield return string.Format("Install folder: {0}", Duplicati.Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR); yield return string.Format("Version name: \"{0}\" ({1})", Duplicati.Library.AutoUpdater.UpdaterManager.SelfVersion.Displayname, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version); yield return string.Format("Current Version folder {0}", System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)); diff --git a/Duplicati/Library/Main/Options.cs b/Duplicati/Library/Main/Options.cs index 476e2f65e..5049e71e2 100644 --- a/Duplicati/Library/Main/Options.cs +++ b/Duplicati/Library/Main/Options.cs @@ -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.Linq; @@ -191,7 +191,7 @@ namespace Duplicati.Library.Main } - private static readonly string DEFAULT_COMPRESSED_EXTENSION_FILE = System.IO.Path.Combine(Duplicati.Library.AutoUpdater.UpdaterManager.InstalledBaseDir, "default_compressed_extensions.txt"); + private static readonly string DEFAULT_COMPRESSED_EXTENSION_FILE = System.IO.Path.Combine(Duplicati.Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR, "default_compressed_extensions.txt"); /// /// Lock that protects the options collection diff --git a/Duplicati/Library/Snapshots/LinuxSnapshot.cs b/Duplicati/Library/Snapshots/LinuxSnapshot.cs index 48e2a2a98..2c7e6ad98 100644 --- a/Duplicati/Library/Snapshots/LinuxSnapshot.cs +++ b/Duplicati/Library/Snapshots/LinuxSnapshot.cs @@ -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; @@ -218,7 +218,7 @@ namespace Duplicati.Library.Snapshots /// A string with the combined output of the stdout and stderr private static string ExecuteCommand(string program, string commandline, int expectedExitCode) { - program = System.IO.Path.Combine(System.IO.Path.Combine(Duplicati.Library.AutoUpdater.UpdaterManager.InstalledBaseDir, "lvm-scripts"), program); + program = System.IO.Path.Combine(System.IO.Path.Combine(Duplicati.Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR, "lvm-scripts"), program); var inf = new ProcessStartInfo(program, commandline) { CreateNoWindow = true, diff --git a/Duplicati/Server/Duplicati.Server.Serialization/Implementations/ServerStatus.cs b/Duplicati/Server/Duplicati.Server.Serialization/Implementations/ServerStatus.cs index a843d7829..fe16bc7ac 100644 --- a/Duplicati/Server/Duplicati.Server.Serialization/Implementations/ServerStatus.cs +++ b/Duplicati/Server/Duplicati.Server.Serialization/Implementations/ServerStatus.cs @@ -1,27 +1,25 @@ -// 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; -using System.Linq; -using System.Text; namespace Duplicati.Server.Serialization.Implementations { @@ -39,8 +37,8 @@ namespace Duplicati.Server.Serialization.Implementations public long LastNotificationUpdateID { get; set; } public string UpdatedVersion { get; set; } + public string UpdateDownloadLink { get; set; } public UpdatePollerStates UpdaterState { get; set; } - public bool UpdateReady { get; set; } public double UpdateDownloadProgress { get; set; } } } diff --git a/Duplicati/Server/Duplicati.Server.Serialization/Interface/IServerStatus.cs b/Duplicati/Server/Duplicati.Server.Serialization/Interface/IServerStatus.cs index 0fbb37c9c..46f4850ae 100644 --- a/Duplicati/Server/Duplicati.Server.Serialization/Interface/IServerStatus.cs +++ b/Duplicati/Server/Duplicati.Server.Serialization/Interface/IServerStatus.cs @@ -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; @@ -27,7 +27,7 @@ namespace Duplicati.Server.Serialization.Interface { Tuple ActiveTask { get; } LiveControlState ProgramState { get; } - System.Collections.Generic.IList> SchedulerQueueIds { get; } + IList> SchedulerQueueIds { get; } bool HasWarning { get; } bool HasError { get; } SuggestedStatusIcon SuggestedStatusIcon { get; } @@ -37,8 +37,8 @@ namespace Duplicati.Server.Serialization.Interface long LastNotificationUpdateID { get; } string UpdatedVersion { get; } + string UpdateDownloadLink { get; } UpdatePollerStates UpdaterState { get; } - bool UpdateReady { get; } double UpdateDownloadProgress { get; } } diff --git a/Duplicati/Server/Program.cs b/Duplicati/Server/Program.cs index 82dcf0db8..849ffe327 100644 --- a/Duplicati/Server/Program.cs +++ b/Duplicati/Server/Program.cs @@ -43,7 +43,7 @@ namespace Duplicati.Server /// /// The path to the directory that contains the main executable /// - public static readonly string StartupPath = Duplicati.Library.AutoUpdater.UpdaterManager.InstalledBaseDir; + public static readonly string StartupPath = Duplicati.Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR; /// /// The name of the environment variable that holds the path to the data folder used by Duplicati @@ -218,17 +218,8 @@ namespace Duplicati.Server /// The main entry point for the application. /// [STAThread] - public static int Main(string[] args) + public static int Main(string[] _args) { - return Duplicati.Library.AutoUpdater.UpdaterManager.RunFromMostRecent(typeof(Program).GetMethod("RealMain"), args, Duplicati.Library.AutoUpdater.AutoUpdateStrategy.Never); - } - - public static int RealMain(string[] _args) - { -#if DEBUG - System.Diagnostics.Debugger.Launch(); -#endif - //If we are on Windows, append the bundled "win-tools" programs to the search path //We add it last, to allow the user to override with other versions if (Platform.IsClientWindows) @@ -354,9 +345,6 @@ namespace Duplicati.Server LogHandler?.Dispose(); } - if (UpdatePoller != null && UpdatePoller.IsUpdateRequested) - return Library.AutoUpdater.UpdaterManager.MAGIC_EXIT_CODE; - return 0; } diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/AboutController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/AboutController.js index f6ec7fafb..b5e37303e 100644 --- a/Duplicati/Server/webroot/ngax/scripts/controllers/AboutController.js +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/AboutController.js @@ -46,14 +46,6 @@ backupApp.controller('AboutController', function($scope, $location, BrandingServ $location.path('/updatechangelog'); }; - $scope.doStartUpdateDownload = function() { - AppService.post('/updates/install'); - }; - - $scope.doStartUpdateActivate = function() { - AppService.post('/updates/activate').then(function() {}, AppUtils.connectionError(gettextCatalog.getString('Activate failed:') + ' ')); - }; - $scope.doCheckForUpdates = function() { AppService.post('/updates/check'); diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/UpdateChangelogController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/UpdateChangelogController.js index 9f5b0cb55..2aa4051e1 100644 --- a/Duplicati/Server/webroot/ngax/scripts/controllers/UpdateChangelogController.js +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/UpdateChangelogController.js @@ -10,14 +10,6 @@ backupApp.controller('UpdateChangelogController', function($scope, BrandingServi }); }; - $scope.doInstall = function() { - AppService.post('/updates/install').then(function() {}, AppUtils.connectionError(gettextCatalog.getString('Install failed:') + ' ')); - }; - - $scope.doActivate = function() { - AppService.post('/updates/activate').then(function() {}, AppUtils.connectionError(gettextCatalog.getString('Activate failed:') + ' ')); - }; - $scope.doCheck = function() { AppService.post('/updates/check').then(function() { reloadChangeLog(); diff --git a/Duplicati/Server/webroot/ngax/scripts/directives/notificationArea.js b/Duplicati/Server/webroot/ngax/scripts/directives/notificationArea.js index 7ea5cba05..f233c2fdd 100644 --- a/Duplicati/Server/webroot/ngax/scripts/directives/notificationArea.js +++ b/Duplicati/Server/webroot/ngax/scripts/directives/notificationArea.js @@ -54,14 +54,6 @@ backupApp.directive('notificationArea', function() { $location.path('/'); }; - $scope.doInstallUpdate = function(id) { - AppService.post('/updates/install'); - }; - - $scope.doActivateUpdate = function(id) { - AppService.post('/updates/activate').then(function() { $scope.doDismiss(id); }, AppUtils.connectionError('Activate failed: ')); - }; - $scope.doShowUpdate = function(id) { $location.path('/updatechangelog'); }; diff --git a/Duplicati/Server/webroot/ngax/scripts/services/ServerStatus.js b/Duplicati/Server/webroot/ngax/scripts/services/ServerStatus.js index 8092dc04d..dd09dbc9f 100644 --- a/Duplicati/Server/webroot/ngax/scripts/services/ServerStatus.js +++ b/Duplicati/Server/webroot/ngax/scripts/services/ServerStatus.js @@ -18,8 +18,8 @@ backupApp.service('ServerStatus', function($rootScope, $timeout, AppService, App failedConnectionAttempts: 0, lastPgEvent: null, updaterState: 'Waiting', + updateDownloadLink: null, updatedVersion: null, - updateReady: false, updateDownloadProgress: 0, proposedSchedule: [], schedulerQueueIds: [] @@ -218,7 +218,7 @@ backupApp.service('ServerStatus', function($rootScope, $timeout, AppService, App notifyIfChanged(response.data, 'ProgramState', 'programState') | notifyIfChanged(response.data, 'EstimatedPauseEnd', 'estimatedPauseEnd') | notifyIfChanged(response.data, 'UpdaterState', 'updaterState') | - notifyIfChanged(response.data, 'UpdateReady', 'updateReady') | + notifyIfChanged(response.data, 'UpdateDownloadLink', 'updateDownloadLink') | notifyIfChanged(response.data, 'UpdatedVersion', 'updatedVersion')| notifyIfChanged(response.data, 'UpdateDownloadProgress', 'updateDownloadProgress'); diff --git a/Duplicati/Server/webroot/ngax/templates/about.html b/Duplicati/Server/webroot/ngax/templates/about.html index 9a1e0cf3f..e0fb6aa16 100644 --- a/Duplicati/Server/webroot/ngax/templates/about.html +++ b/Duplicati/Server/webroot/ngax/templates/about.html @@ -26,11 +26,8 @@
 
You are currently running {{appname}} {{version}}
-
- Update {{state.updatedVersion}} is available, download now -
-
- Update {{state.updatedVersion}} is installed, activate now +
+ Update {{state.updatedVersion}} is available, download now
Check for updates now diff --git a/Duplicati/Server/webroot/ngax/templates/notificationarea.html b/Duplicati/Server/webroot/ngax/templates/notificationarea.html index 852dbbef3..e91cb6bd9 100644 --- a/Duplicati/Server/webroot/ngax/templates/notificationarea.html +++ b/Duplicati/Server/webroot/ngax/templates/notificationarea.html @@ -19,16 +19,13 @@
-
New update found: {{message}}
+
New update found: {{message}}
diff --git a/Duplicati/Server/webroot/ngax/templates/updatechangelog.html b/Duplicati/Server/webroot/ngax/templates/updatechangelog.html index ee5755dfe..881d5b25e 100644 --- a/Duplicati/Server/webroot/ngax/templates/updatechangelog.html +++ b/Duplicati/Server/webroot/ngax/templates/updatechangelog.html @@ -7,8 +7,7 @@
Loading …
diff --git a/Duplicati/Service/Program.cs b/Duplicati/Service/Program.cs index e01500692..25a6d5006 100644 --- a/Duplicati/Service/Program.cs +++ b/Duplicati/Service/Program.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2024, The Duplicati Team +// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a @@ -27,15 +27,11 @@ namespace Duplicati.Service { [STAThread] public static int Main(string[] args) - { - return Duplicati.Library.AutoUpdater.UpdaterManager.RunFromMostRecent(typeof(Program).GetMethod("RealMain"), args, Duplicati.Library.AutoUpdater.AutoUpdateStrategy.Never); - } - - public static void RealMain(string[] args) { using(var runner = new Runner(args)) runner.Wait(); + return 0; } } } diff --git a/Duplicati/UnitTest/BackendToolTests.cs b/Duplicati/UnitTest/BackendToolTests.cs index 1ec94df54..2b3c4b631 100755 --- a/Duplicati/UnitTest/BackendToolTests.cs +++ b/Duplicati/UnitTest/BackendToolTests.cs @@ -66,7 +66,7 @@ namespace Duplicati.UnitTest { // Absolute path var downloadFileName = Path.Combine(absoluteDownloadFolder, Path.GetFileName(targetFile)); - var status = CommandLine.BackendTool.Program.RealMain(new[] { "GET", $"{backendURL}", $"{downloadFileName}" }); + var status = CommandLine.BackendTool.Program.Main(new[] { "GET", $"{backendURL}", $"{downloadFileName}" }); Assert.AreEqual(0, status); Assert.IsTrue(File.Exists(downloadFileName)); TestUtils.AssertFilesAreEqual(targetFile, downloadFileName, false, downloadFileName); @@ -83,7 +83,7 @@ namespace Duplicati.UnitTest { // Relative path var downloadFileName = Path.GetFileName(targetFile); - var status = CommandLine.BackendTool.Program.RealMain(new[] { "GET", $"{backendURL}", $"{downloadFileName}" }); + var status = CommandLine.BackendTool.Program.Main(new[] { "GET", $"{backendURL}", $"{downloadFileName}" }); Assert.AreEqual(0, status); Assert.IsTrue(File.Exists(downloadFileName)); TestUtils.AssertFilesAreEqual(targetFile, downloadFileName, false, downloadFileName); diff --git a/Duplicati/UnitTest/CommandLineOperationsTests.cs b/Duplicati/UnitTest/CommandLineOperationsTests.cs index 86ec197d4..fa387fc9b 100644 --- a/Duplicati/UnitTest/CommandLineOperationsTests.cs +++ b/Duplicati/UnitTest/CommandLineOperationsTests.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2024, The Duplicati Team +// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a @@ -174,17 +174,17 @@ namespace Duplicati.UnitTest ProgressWriteLine("Running backup with {0} data added ...", Duplicati.Library.Utility.Utility.FormatSizeString(size)); using (new Library.Logging.Timer(LOGTAG, "BackupWithDataAdded", string.Format("Backup with {0} data added", Duplicati.Library.Utility.Utility.FormatSizeString(size)))) - Duplicati.CommandLine.Program.RealMain(backupargs); + Duplicati.CommandLine.Program.Main(backupargs); ProgressWriteLine("Testing data ..."); using (new Library.Logging.Timer(LOGTAG, "TestRemoteData", "Test remote data")) - if (Duplicati.CommandLine.Program.RealMain((new string[] { "test", target, "all" }.Union(opts)).ToArray()) != 0) + if (Duplicati.CommandLine.Program.Main((new string[] { "test", target, "all" }.Union(opts)).ToArray()) != 0) throw new Exception("Failed during remote verification"); } ProgressWriteLine("Running unchanged backup ..."); using (new Library.Logging.Timer(LOGTAG, "UnchangedBackup", "Unchanged backup")) - Duplicati.CommandLine.Program.RealMain(backupargs); + Duplicati.CommandLine.Program.Main(backupargs); var datafolders = systemIO.EnumerateDirectories(DATAFOLDER); @@ -195,7 +195,7 @@ namespace Duplicati.UnitTest ProgressWriteLine("Running backup with renamed folder..."); using (new Library.Logging.Timer(LOGTAG, "BackupWithRenamedFolder", "Backup with renamed folder")) - Duplicati.CommandLine.Program.RealMain(backupargs); + Duplicati.CommandLine.Program.Main(backupargs); datafolders = systemIO.EnumerateDirectories(DATAFOLDER); @@ -212,11 +212,11 @@ namespace Duplicati.UnitTest ProgressWriteLine("Running backup with deleted data..."); using (new Library.Logging.Timer(LOGTAG, "BackupWithDeletedData", "Backup with deleted data")) - Duplicati.CommandLine.Program.RealMain(backupargs); + Duplicati.CommandLine.Program.Main(backupargs); ProgressWriteLine("Testing the compare method ..."); using (new Library.Logging.Timer(LOGTAG, "CompareMethod", "Compare method")) - Duplicati.CommandLine.Program.RealMain((new string[] { "compare", target, "0", "1" }.Union(opts)).ToArray()); + Duplicati.CommandLine.Program.Main((new string[] { "compare", target, "0", "1" }.Union(opts)).ToArray()); for (var i = 0; i < 5; i++) { @@ -224,12 +224,12 @@ namespace Duplicati.UnitTest systemIO.FileCopy(LOGFILE, Path.Combine(SOURCEFOLDER, Path.GetFileName(LOGFILE)), true); using (new Library.Logging.Timer(LOGTAG, "BackupWithLogfileChange", string.Format("Backup with logfilechange {0}", i + 1))) - Duplicati.CommandLine.Program.RealMain(backupargs); + Duplicati.CommandLine.Program.Main(backupargs); } ProgressWriteLine("Compacting data ..."); using (new Library.Logging.Timer(LOGTAG, "Compacting", "Compacting")) - Duplicati.CommandLine.Program.RealMain((new string[] { "compact", target, "--small-file-max-count=2" }.Union(opts)).ToArray()); + Duplicati.CommandLine.Program.Main((new string[] { "compact", target, "--small-file-max-count=2" }.Union(opts)).ToArray()); datafolders = systemIO.EnumerateDirectories(DATAFOLDER); @@ -237,7 +237,7 @@ namespace Duplicati.UnitTest ProgressWriteLine("Partial restore of {0} ...", Path.GetFileName(rf)); using (new Library.Logging.Timer(LOGTAG, "PartialRestore", "Partial restore")) - Duplicati.CommandLine.Program.RealMain((new string[] { "restore", target, rf + "*", "--restore-path=\"" + RESTOREFOLDER + "\"" }.Union(opts)).ToArray()); + Duplicati.CommandLine.Program.Main((new string[] { "restore", target, rf + "*", "--restore-path=\"" + RESTOREFOLDER + "\"" }.Union(opts)).ToArray()); ProgressWriteLine("Verifying partial restore ..."); using (new Library.Logging.Timer(LOGTAG, "VerificationOfPartialRestore", "Verification of partial restored files")) @@ -247,7 +247,7 @@ namespace Duplicati.UnitTest ProgressWriteLine("Partial restore of {0} without local db...", Path.GetFileName(rf)); using (new Library.Logging.Timer(LOGTAG, "PartialRestoreWithoutLocalDb", "Partial restore without local db")) - Duplicati.CommandLine.Program.RealMain((new string[] { "restore", target, rf + "*", "--restore-path=\"" + RESTOREFOLDER + "\"", "--no-local-db" }.Union(opts)).ToArray()); + Duplicati.CommandLine.Program.Main((new string[] { "restore", target, rf + "*", "--restore-path=\"" + RESTOREFOLDER + "\"", "--no-local-db" }.Union(opts)).ToArray()); ProgressWriteLine("Verifying partial restore ..."); using (new Library.Logging.Timer(LOGTAG, "VerificationOfPartialRestore", "Verification of partial restored files")) @@ -257,7 +257,7 @@ namespace Duplicati.UnitTest ProgressWriteLine("Full restore ..."); using (new Library.Logging.Timer(LOGTAG, "FullRestore", "Full restore")) - Duplicati.CommandLine.Program.RealMain((new string[] { "restore", target, "*", "--restore-path=\"" + RESTOREFOLDER + "\"" }.Union(opts)).ToArray()); + Duplicati.CommandLine.Program.Main((new string[] { "restore", target, "*", "--restore-path=\"" + RESTOREFOLDER + "\"" }.Union(opts)).ToArray()); ProgressWriteLine("Verifying full restore ..."); using (new Library.Logging.Timer(LOGTAG, "VerificationOfFullRestore", "Verification of restored files")) @@ -268,7 +268,7 @@ namespace Duplicati.UnitTest ProgressWriteLine("Full restore without local db..."); using (new Library.Logging.Timer(LOGTAG, "FullRestoreWithoutDb", "Full restore without local db")) - Duplicati.CommandLine.Program.RealMain((new string[] { "restore", target, "*", "--restore-path=\"" + RESTOREFOLDER + "\"", "--no-local-db" }.Union(opts)).ToArray()); + Duplicati.CommandLine.Program.Main((new string[] { "restore", target, "*", "--restore-path=\"" + RESTOREFOLDER + "\"", "--no-local-db" }.Union(opts)).ToArray()); ProgressWriteLine("Verifying full restore ..."); using (new Library.Logging.Timer(LOGTAG, "VerificationOfFullRestoreWithoutDb", "Verification of restored files")) @@ -277,7 +277,7 @@ namespace Duplicati.UnitTest ProgressWriteLine("Testing data ..."); using (new Library.Logging.Timer(LOGTAG, "TestRemoteData", "Test remote data")) - if (Duplicati.CommandLine.Program.RealMain((new string[] { "test", target, "all" }.Union(opts)).ToArray()) != 0) + if (Duplicati.CommandLine.Program.Main((new string[] { "test", target, "all" }.Union(opts)).ToArray()) != 0) throw new Exception("Failed during final remote verification"); } } diff --git a/Duplicati/UnitTest/RecoveryToolTests.cs b/Duplicati/UnitTest/RecoveryToolTests.cs index f654bbb77..80f9dc1af 100644 --- a/Duplicati/UnitTest/RecoveryToolTests.cs +++ b/Duplicati/UnitTest/RecoveryToolTests.cs @@ -90,17 +90,17 @@ namespace Duplicati.UnitTest // Download the backend files. string downloadFolder = Path.Combine(this.RESTOREFOLDER, "downloadedFiles"); Directory.CreateDirectory(downloadFolder); - int status = CommandLine.RecoveryTool.Program.RealMain(new[] {"download", $"{backendURL}", $"{downloadFolder}", $"--passphrase={options["passphrase"]}"}); + int status = CommandLine.RecoveryTool.Program.Main(new[] {"download", $"{backendURL}", $"{downloadFolder}", $"--passphrase={options["passphrase"]}"}); Assert.AreEqual(0, status); // Create the index. - status = CommandLine.RecoveryTool.Program.RealMain(new[] {"index", $"{downloadFolder}", $"--build-index-with-files={buildIndexWithFiles}"}); + status = CommandLine.RecoveryTool.Program.Main(new[] {"index", $"{downloadFolder}", $"--build-index-with-files={buildIndexWithFiles}"}); Assert.AreEqual(0, status); // Restore to a different folder. string restoreFolder = Path.Combine(this.RESTOREFOLDER, "restoredFiles"); Directory.CreateDirectory(restoreFolder); - status = CommandLine.RecoveryTool.Program.RealMain(new[] {"restore", $"{downloadFolder}", $"--targetpath={restoreFolder}"}); + status = CommandLine.RecoveryTool.Program.Main(new[] {"restore", $"{downloadFolder}", $"--targetpath={restoreFolder}"}); Assert.AreEqual(0, status); // Since this.DATAFOLDER is a folder, Path.GetFileName will return the name of the diff --git a/Duplicati/WebserverCore/DuplicatiWebserver.cs b/Duplicati/WebserverCore/DuplicatiWebserver.cs index ca227d420..42935a639 100644 --- a/Duplicati/WebserverCore/DuplicatiWebserver.cs +++ b/Duplicati/WebserverCore/DuplicatiWebserver.cs @@ -18,8 +18,8 @@ namespace Duplicati.WebserverCore app.UseAuthMiddleware(); - string webroot = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); - string install_webroot = System.IO.Path.Combine(Library.AutoUpdater.UpdaterManager.InstalledBaseDir, "webroot"); + string webroot = System.IO.Path.GetDirectoryName(Duplicati.Library.Utility.Utility.getEntryAssembly().Location); + string install_webroot = System.IO.Path.Combine(Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR, "webroot"); webroot = System.IO.Path.Combine(webroot, "webroot"); var webroot_fileprovider = new PhysicalFileProvider(webroot); diff --git a/Duplicati/WindowsService/Program.cs b/Duplicati/WindowsService/Program.cs index d0d425c1a..deb3f9d2c 100644 --- a/Duplicati/WindowsService/Program.cs +++ b/Duplicati/WindowsService/Program.cs @@ -1,32 +1,27 @@ -// 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; -using System.Diagnostics; using System.Linq; using System.Reflection; -using System.Security.Principal; using System.ServiceProcess; -using System.Text; -using System.Threading.Tasks; namespace Duplicati.WindowsService { @@ -34,11 +29,6 @@ namespace Duplicati.WindowsService { [STAThread] public static int Main(string[] args) - { - return Duplicati.Library.AutoUpdater.UpdaterManager.RunFromMostRecent(typeof(Program).GetMethod("RealMain"), args, Duplicati.Library.AutoUpdater.AutoUpdateStrategy.Never); - } - - public static void RealMain(string[] args) { var install = args != null && args.Any(x => string.Equals("install", x, StringComparison.OrdinalIgnoreCase)); var uninstall = args != null && args.Any(x => string.Equals("uninstall", x, StringComparison.OrdinalIgnoreCase)); @@ -79,6 +69,7 @@ namespace Duplicati.WindowsService catch (Exception ex) { Console.WriteLine("Duplicati service delete failed. Exception: {0}", ex.Message); + return 1; } } if (install) @@ -93,6 +84,7 @@ namespace Duplicati.WindowsService catch (Exception ex) { Console.WriteLine("Duplicati service installation failed. Exception: {0}", ex.Message); + return 1; } } } @@ -100,6 +92,8 @@ namespace Duplicati.WindowsService { ServiceBase.Run(new ServiceBase[] { new ServiceControl(args) }); } + + return 0; } } }