// 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;
}