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; /// /// 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. /// 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"; /// /// Directory pointed to by appsettings.json /// public LongPath Location { get; private set; } /// /// Returns true if exists and the property has a non-null, non-empty value. /// Does not verify the existence of the directory. /// public bool SettingsAreValid => SettingsFileIsValid(SettingsFilePath); /// /// Found Location of appsettings.json. This file must exist or be able to be created for Libation to start. /// internal string? AppsettingsJsonFile { get; } /// /// File path to Settings.json inside /// 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); } /// /// Set the location of the Libation Files directory, updating appsettings.json. /// 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 }); } } /// /// Returns true if exists and the property has a non-null, non-empty value. /// Does not verify the existence of the directory. /// /// File path to the Settings.json file 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()); } 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; } } /// /// Try to find appsettings.json in the following locations: /// /// /// [App Directory] /// /// /// %LocalAppData%\Libation /// /// /// %AppData%\Libation /// /// /// %Temp%\Libation /// /// /// /// If not found, try to create it in each of the same locations in-order until successful. /// /// This method must complete successfully for Libation to continue. /// /// appsettings.json file path /// appsettings.json could not be found or created. 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}"); } /// /// Get the LibationFiles directory from appsettings.json, expanding environment variables as needed. /// /// /// /// The appsettings.json file does not contain a "LibationFiles" key 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() 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; } } } }