// Copyright (C) 2025, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. #nullable enable using System; using System.IO; using System.Linq; using Duplicati.Library.Common.IO; using Duplicati.Library.Utility; namespace Duplicati.Library.AutoUpdater; /// /// Manages the data folder for the application /// public static class DataFolderManager { /// /// The folder where the machine id is placed /// public static readonly string DATAFOLDER; /// /// The installation ID filename stored in /// private const string INSTALL_FILE = "installation.txt"; /// /// The machine ID filename stored in /// private const string MACHINE_FILE = "machineid.txt"; /// /// The option name for portable mode /// public const string PORTABLE_MODE_OPTION = "portable-mode"; /// /// The option anme for the server data folder /// public const string SERVER_DATAFOLDER_OPTION = "server-datafolder"; /// /// The app name to use for variables /// private static readonly string APPNAME = AutoUpdateSettings.AppName; /// /// The name of the environment variable that allows overriding the path to the data folder used by Duplicati /// public static readonly string DATAFOLDER_ENV_NAME = $"{APPNAME}_HOME".ToUpperInvariant(); /// /// Name of the database file /// public static readonly string SERVER_DATABASE_FILENAME = $"{APPNAME}-server.sqlite"; /// /// Flag to indicate if the application is running in portable mode /// public static readonly bool PORTABLE_MODE; /// /// Flag to indicate if the data folder was overriden /// public static readonly bool OVERRIDEN_DATAFOLDER; /// /// Replication of the argument parsing from the main Duplicati codebase /// /// The option to extract /// null if the option is not found, otherwise the value of the option private static string? ExtractOptionSlim(string option) { var opt = $"--{option}"; var args = Environment.GetCommandLineArgs().Skip(1).ToArray(); var match = args.Select((token, index) => new { token, index }) .LastOrDefault(x => string.Equals(x.token, opt, StringComparison.OrdinalIgnoreCase) || x.token.StartsWith(opt + "=", StringComparison.OrdinalIgnoreCase) ); // Not found, try the environment variable if (string.IsNullOrWhiteSpace(match?.token)) return Environment.GetEnvironmentVariable($"{AutoUpdateSettings.AppName}__{option.Replace('-', '_')}".ToUpperInvariant()); // Found in the form --option=value if (match.token.StartsWith(opt + "=", StringComparison.OrdinalIgnoreCase)) return match.token.Substring(opt.Length + 1).Trim('"'); // Found in the form --option value if (match.index + 1 < args.Length) { var value = args[match.index + 1]; if (!value.StartsWith("--")) return value; } // Found, but no value, just the option return ""; } /// /// Replication of the boolean parsing from the main Duplicati codebase /// /// The value to parse /// true if the value is a truthy value, otherwise false private static bool ParseBoolSlim(string? value) { // In debug builds, we default to portable mode if (value == null) #if DEBUG return true; #else return false; #endif if ( value.Equals("false", StringComparison.OrdinalIgnoreCase) || value.Equals("0", StringComparison.OrdinalIgnoreCase) || value.Equals("no", StringComparison.OrdinalIgnoreCase) || value.Equals("off", StringComparison.OrdinalIgnoreCase) ) return false; return true; } static DataFolderManager() { // Trigger portable mode, if the flag is set PORTABLE_MODE = ParseBoolSlim(ExtractOptionSlim(PORTABLE_MODE_OPTION)); // The environment variable is a legacy setting var envOverride = Environment.GetEnvironmentVariable(DATAFOLDER_ENV_NAME); // These are mainly supported by the Server var datafolderArg = ExtractOptionSlim(SERVER_DATAFOLDER_OPTION); // Prefer the command line argument over the environment variable if (!string.IsNullOrWhiteSpace(datafolderArg)) { OVERRIDEN_DATAFOLDER = true; DATAFOLDER = Util.AppendDirSeparator(Environment.ExpandEnvironmentVariables(datafolderArg).Trim('"')); } // Portable mode is prefered over the environment variable else if (PORTABLE_MODE) { OVERRIDEN_DATAFOLDER = true; DATAFOLDER = Util.AppendDirSeparator(Path.Combine(UpdaterManager.INSTALLATIONDIR, "data")); } // Use the legacy environment variable, if set else if (!string.IsNullOrWhiteSpace(envOverride)) { OVERRIDEN_DATAFOLDER = true; DATAFOLDER = Util.AppendDirSeparator(Environment.ExpandEnvironmentVariables(envOverride).Trim('"')); } // Use the default location else { OVERRIDEN_DATAFOLDER = false; DATAFOLDER = Util.AppendDirSeparator(DataFolderLocator.GetDefaultStorageFolderInternal(SERVER_DATABASE_FILENAME, APPNAME)); } if (Directory.Exists(DATAFOLDER)) { if (!File.Exists(Path.Combine(DATAFOLDER, Util.InsecurePermissionsMarkerFile))) SystemIO.IO_OS.DirectorySetPermissionUserRWOnly(DATAFOLDER); } else { Directory.CreateDirectory(DATAFOLDER); SystemIO.IO_OS.DirectorySetPermissionUserRWOnly(DATAFOLDER); } if (!File.Exists(Path.Combine(DATAFOLDER, INSTALL_FILE))) { // In case there was already a machine id file from 2.0.8.1 or older, copy it to the new location if (File.Exists(Path.Combine(DATAFOLDER, "updates", INSTALL_FILE))) File.Copy(Path.Combine(DATAFOLDER, "updates", INSTALL_FILE), Path.Combine(DATAFOLDER, INSTALL_FILE), true); else File.WriteAllText(Path.Combine(DATAFOLDER, INSTALL_FILE), AutoUpdateSettings.UpdateInstallFileText); } if (!File.Exists(Path.Combine(DATAFOLDER, MACHINE_FILE))) File.WriteAllText(Path.Combine(DATAFOLDER, MACHINE_FILE), AutoUpdateSettings.UpdateMachineFileText(InstallID)); } /// /// The unique machine installation ID /// public static string InstallID => _installID.Value; /// /// The unique machine ID, lazy evaluated /// private static readonly Lazy _installID = new(() => { try { return File.ReadAllLines(Path.Combine(DATAFOLDER!, INSTALL_FILE)).FirstOrDefault(x => !string.IsNullOrWhiteSpace(x))?.Trim() ?? ""; } catch { } return ""; }); /// /// The unique machine ID /// public static string MachineID => _machineID.Value; /// /// The unique machine ID, lazy evaluated /// private static readonly Lazy _machineID = new(() => { string? machinedId = null; try { machinedId = File.ReadAllLines(Path.Combine(DATAFOLDER!, MACHINE_FILE)).FirstOrDefault(x => !string.IsNullOrWhiteSpace(x))?.Trim() ?? ""; } catch { } return string.IsNullOrWhiteSpace(machinedId) ? InstallID : machinedId; }); /// /// The machine name, lazy evaluated /// private static readonly Lazy _machineName = new(MachineNameReader.GetMachineName); /// /// The machine name /// public static readonly string MachineName = _machineName.Value; }