Files
Libation/Source/LibationFileManager/LibationFiles.cs
MBucari ce2b81036f Add license and settings overrides to LibationCli
- Add `LIBATION_FILES_DIR` environment variable to specify LibationFiles directory instead of appsettings.json
- OptionsBase supports overriding setting
  - Added `EphemeralSettings` which are loaded from Settings.json once and can be modified with the `--override` command parameter
- Added `get-setting` command
  - Prints (editable) settings and their values. Prints specified settings, or all settings if none specified
  - `--listEnumValues` option will list all names for a speficied enum-type settings. If no setting names are specified, prints all enum values for all enum settings.
  - Prints in a text-based table or bare with `-b` switch
- Added `get-license` command which requests a content license and prints it as a json to stdout
- Improved `liberate` command
  - Added `-force` option to force liberation without validation.
  - Added support to download with a license file supplied to stdin
  - Improve startup performance when downloading explicit ASIN(s)
  - Fix long-standing bug where cover art was not being downloading
2025-11-19 23:47:41 -07:00

283 lines
9.2 KiB
C#

using Dinah.Core.Logging;
using FileManager;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Serilog;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("AppScaffolding")]
[assembly: InternalsVisibleTo("LibationUiBase.Tests")]
#nullable enable
namespace LibationFileManager;
/// <summary>
/// Provides access to Libation's configuration and settings file locations, including methods for validating and
/// updating the Libation files directory and Settings.json file. An instance is bount to a single appsettings.json file.
/// </summary>
public class LibationFiles
{
internal static string? s_DefaultLibationFilesDirectory;
public static string DefaultLibationFilesDirectory => s_DefaultLibationFilesDirectory ??= Configuration.IsWindows ? Configuration.UserProfile : Configuration.LocalAppData;
public const string LIBATION_FILES_KEY = "LibationFiles";
public const string SETTINGS_JSON = "Settings.json";
public const string LIBATION_FILES_DIR = "LIBATION_FILES_DIR";
/// <summary>
/// Directory pointed to by appsettings.json
/// </summary>
public LongPath Location { get; private set; }
/// <summary>
/// Returns true if <see cref="SettingsFilePath"/> exists and the <see cref="Configuration.Books"/> property has a non-null, non-empty value.
/// Does not verify the existence of the <see cref="Configuration.Books"/> directory.
/// </summary>
public bool SettingsAreValid => SettingsFileIsValid(SettingsFilePath);
/// <summary>
/// Found Location of appsettings.json. This file must exist or be able to be created for Libation to start.
/// </summary>
internal string? AppsettingsJsonFile { get; }
/// <summary>
/// File path to Settings.json inside <see cref="Location"/>
/// </summary>
public string SettingsFilePath => Path.Combine(Location, SETTINGS_JSON);
internal LibationFiles()
{
var libationFilesDir = Environment.GetEnvironmentVariable(LIBATION_FILES_DIR);
if (Directory.Exists(libationFilesDir))
{
Location = libationFilesDir;
}
else
{
AppsettingsJsonFile = GetOrCreateAppsettingsFile();
Location = GetLibationFilesFromAppsettings(AppsettingsJsonFile);
}
}
internal LibationFiles(string appSettingsFile)
{
AppsettingsJsonFile = appSettingsFile;
Location = GetLibationFilesFromAppsettings(AppsettingsJsonFile);
}
/// <summary>
/// Set the location of the Libation Files directory, updating appsettings.json.
/// </summary>
public void SetLibationFiles(LongPath libationFilesDirectory)
{
if (AppsettingsJsonFile is null)
{
Environment.SetEnvironmentVariable(LIBATION_FILES_DIR, libationFilesDirectory);
return;
}
var startingContents = File.ReadAllText(AppsettingsJsonFile);
var jObj = JObject.Parse(startingContents);
jObj[LIBATION_FILES_KEY] = (string)(Location = libationFilesDirectory);
var endingContents = JsonConvert.SerializeObject(jObj, Formatting.Indented);
if (startingContents == endingContents)
return;
try
{
// now it's set in the file again but no settings have moved yet
File.WriteAllText(AppsettingsJsonFile, endingContents);
Log.Logger.TryLogInformation("Libation files changed {@DebugInfo}", new { AppsettingsJsonFile, LIBATION_FILES_KEY, libationFilesDirectory });
}
catch (Exception ex)
{
Log.Logger.TryLogError(ex, "Failed to change Libation files location {@DebugInfo}", new { AppsettingsJsonFile, LIBATION_FILES_KEY, libationFilesDirectory });
}
}
/// <summary>
/// Returns true if <paramref name="settingsFile"/> exists and the <see cref="Configuration.Books"/> property has a non-null, non-empty value.
/// Does not verify the existence of the <see cref="Configuration.Books"/> directory.
/// </summary>
/// <param name="settingsFile">File path to the Settings.json file</param>
public static bool SettingsFileIsValid(string settingsFile)
{
if (!Directory.Exists(Path.GetDirectoryName(settingsFile)) || !File.Exists(settingsFile))
return false;
try
{
var settingsJson = JObject.Parse(File.ReadAllText(settingsFile));
return !string.IsNullOrWhiteSpace(settingsJson[nameof(Configuration.Books)]?.Value<string>());
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Failed to load settings file: {@SettingsFile}", settingsFile);
try
{
Log.Logger.Information("Deleting invalid settings file: {@SettingsFile}", settingsFile);
FileUtility.SaferDelete(settingsFile);
Log.Logger.Information("Creating a new, empty setting file: {@SettingsFile}", settingsFile);
try
{
File.WriteAllText(settingsFile, "{}");
}
catch (Exception createEx)
{
Log.Logger.Error(createEx, "Failed to create new settings file: {@SettingsFile}", settingsFile);
}
}
catch (Exception deleteEx)
{
Log.Logger.Error(deleteEx, "Failed to delete the invalid settings file: {@SettingsFile}", settingsFile);
}
return false;
}
}
/// <summary>
/// Try to find appsettings.json in the following locations:
/// <list type="number">
/// <item>
/// <description>[App Directory]</description>
/// </item>
/// <item>
/// <description>%LocalAppData%\Libation</description>
/// </item>
/// <item>
/// <description>%AppData%\Libation</description>
/// </item>
/// <item>
/// <description>%Temp%\Libation</description>
/// </item>
/// </list>
///
/// If not found, try to create it in each of the same locations in-order until successful.
///
/// <para>This method must complete successfully for Libation to continue.</para>
/// </summary>
/// <returns>appsettings.json file path</returns>
/// <exception cref="ApplicationException">appsettings.json could not be found or created.</exception>
private static string GetOrCreateAppsettingsFile()
{
const string appsettings_filename = "appsettings.json";
//Possible appsettings.json locations, in order of preference.
string[] possibleAppsettingsDirectories = new[]
{
Configuration.ProcessDirectory,
Configuration.LocalAppData,
Configuration.UserProfile,
Configuration.WinTemp,
};
//Try to find and validate appsettings.json in each folder
foreach (var dir in possibleAppsettingsDirectories)
{
var appsettingsFile = Path.Combine(dir, appsettings_filename);
if (File.Exists(appsettingsFile))
{
try
{
var appSettings = JObject.Parse(File.ReadAllText(appsettingsFile));
if (appSettings.ContainsKey(LIBATION_FILES_KEY)
&& appSettings[LIBATION_FILES_KEY] is JValue jval
&& jval.Value is string settingsPath
&& !string.IsNullOrWhiteSpace(settingsPath))
return appsettingsFile;
}
catch { }
}
}
//Valid appsettings.json not found. Try to create it in each folder.
var endingContents = new JObject { { LIBATION_FILES_KEY, DefaultLibationFilesDirectory } }.ToString(Formatting.Indented);
foreach (var dir in possibleAppsettingsDirectories)
{
//Don't try to create appsettings.json in the program files directory on *.nix systems.
//However, still _look_ for one there for backwards compatibility with previous installations
if (!Configuration.IsWindows && dir == Configuration.ProcessDirectory)
continue;
var appsettingsFile = Path.Combine(dir, appsettings_filename);
try
{
Directory.CreateDirectory(dir);
File.WriteAllText(appsettingsFile, endingContents);
return appsettingsFile;
}
catch (Exception ex)
{
Log.Logger.TryLogError(ex, $"Failed to create {appsettingsFile}");
}
}
throw new ApplicationException($"Could not locate or create {appsettings_filename}");
}
/// <summary>
/// Get the LibationFiles directory from appsettings.json, expanding environment variables as needed.
/// </summary>
/// <param name="appsettingsPath"></param>
/// <returns></returns>
/// <exception cref="InvalidDataException">The appsettings.json file does not contain a "LibationFiles" key</exception>
private static string GetLibationFilesFromAppsettings(LongPath appsettingsPath)
{
// do not check whether directory exists. special/meta directory (eg: AppDir) is valid
// verify from live file. no try/catch. want failures to be visible
var jObjFinal = JObject.Parse(File.ReadAllText(appsettingsPath));
if (jObjFinal[LIBATION_FILES_KEY]?.Value<string>() is not string libationFiles)
throw new InvalidDataException($"{LIBATION_FILES_KEY} not found in {appsettingsPath}");
if (Configuration.IsWindows)
{
libationFiles = Environment.ExpandEnvironmentVariables(libationFiles);
}
else
{
//If the shell command fails and returns null, proceed with the verbatim
//LIBATION_FILES_KEY path and hope for the best. If Libation can't find
//anything at this path it will set LIBATION_FILES_KEY to UserProfile
libationFiles = runShellCommand("echo " + libationFiles) ?? libationFiles;
}
return libationFiles;
static string? runShellCommand(string command)
{
var psi = new ProcessStartInfo
{
FileName = "/bin/sh",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
ArgumentList =
{
"-c",
command
}
};
try
{
var proc = Process.Start(psi);
proc?.WaitForExit();
return proc?.StandardOutput?.ReadToEnd()?.Trim();
}
catch (Exception e)
{
Log.Error(e, "Failed to run shell command. {@Arguments}", psi.ArgumentList);
return null;
}
}
}
}