Files
Libation/Source/LibationUiBase/LibationSetup.cs
MBucari e850465ec1 Add more null safety
Enable project-wide nullable on LibationUiBase and LibationAvalonia

Explicitly parallelize unit tests
2025-12-30 13:17:11 -07:00

242 lines
7.5 KiB
C#

using Dinah.Core;
using Dinah.Core.Logging;
using LibationFileManager;
using LibationUiBase.Forms;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Serilog;
using System;
using System.IO;
using System.Threading.Tasks;
namespace LibationUiBase;
/// <summary>
/// Contains the results of a, initial setup prompt.
/// </summary>
public interface ILibationSetup
{
public bool IsNewUser { get; }
public bool IsReturningUser { get; }
}
/// <summary>
/// Contains the results of a Libation Files install location selection prompt.
/// </summary>
public interface ILibationInstallLocation
{
public string? SelectedDirectory { get; }
}
/// <summary>
/// Provides configuration and delegates for running the Libation setup process, including user prompts for initial
/// setup and selecting installation locations.
/// </summary>
/// <remarks>LibationSetup encapsulates the logic required to ensure that Libation is properly configured before
/// use. This class is used at application startup to ensure that all required settings are present and valid
/// before proceeding.</remarks>
public class LibationSetup
{
/// <summary> Asynchronous delegate to show the setup prompt </summary>
public Func<Task<ILibationSetup>>? SetupPromptAsync { get; init; }
/// <summary> Asynchronous delegate to show the Libation Files selection dialog prompt </summary>
public Func<Task<ILibationInstallLocation?>>? SelectFolderPromptAsync { get; init; }
/// <summary> Synchronous delegate to show the setup prompt </summary>
public Func<ILibationSetup>? SetupPrompt { get; init; }
/// <summary> Synchronous delegate to show the Libation Files selection dialog prompt </summary>
public Func<ILibationInstallLocation?>? SelectFolderPrompt { get; init; }
private LibationFiles Files { get; }
public LibationSetup(LibationFiles libationFiles)
{
Files = libationFiles;
}
/// <summary>
/// Runs Libation setup if needed.
/// Verifies that
/// </summary>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<bool> RunSetupIfNeededAsync()
{
// all returns should be preceded by either:
// - if libationFiles.LibationSettingsAreValid
// - OnCancelled()
if (Files.SettingsAreValid)
return true;
// check for existing settings in default location
// First check if file exists so that, if it doesn't, we don't
// overwrite user's LibationFiles setting in appsettings.json
var defaultSettingsFile = Path.Combine(LibationFiles.DefaultLibationFilesDirectory, LibationFiles.SETTINGS_JSON);
if (File.Exists(defaultSettingsFile) && LibationFiles.SettingsFileIsValid(defaultSettingsFile))
{
Files.SetLibationFiles(LibationFiles.DefaultLibationFilesDirectory);
if (Files.SettingsAreValid)
return true;
}
var setupResult
= SetupPromptAsync is not null ? await SetupPromptAsync()
: SetupPrompt is not null ? SetupPrompt()
: throw new InvalidOperationException("No setup prompt provided");
if (setupResult.IsNewUser)
{
return await CreateDefaultSettingsAsync();
}
else if (setupResult.IsReturningUser)
{
var chooseFolderResult
= SelectFolderPromptAsync is not null ? await SelectFolderPromptAsync()
: SelectFolderPrompt is not null ? SelectFolderPrompt()
: throw new InvalidOperationException("No select folder prompt provided");
if (string.IsNullOrWhiteSpace(chooseFolderResult?.SelectedDirectory))
return false;
Files.SetLibationFiles(chooseFolderResult.SelectedDirectory);
if (Files.SettingsAreValid)
return true;
// path did not result in valid settings
var continueResult = await MessageBoxBase.ShowAsyncImpl(null,
$"""
No valid settings were found at this location.
Would you like to create a new install settings in this folder?
{chooseFolderResult.SelectedDirectory}
""",
"New install?",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question,
MessageBoxDefaultButton.Button1);
return continueResult == DialogResult.Yes && await CreateDefaultSettingsAsync();
}
else
{
return false;
}
}
private async Task<bool> CreateDefaultSettingsAsync()
{
if (!TryCreateDirectory())
{
var result = await MessageBoxBase.ShowAsyncImpl(null,
$"""
Could not create the Libation Settings folder at:
{Files.Location.Path}
Would you like to create a new install settings in this folder?
{LibationFiles.DefaultLibationFilesDirectory}
""",
"Error Creating Libation Settings",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question,
MessageBoxDefaultButton.Button1);
if (result is not DialogResult.Yes)
return false;
Files.SetLibationFiles(LibationFiles.DefaultLibationFilesDirectory);
//We should never not be able to access DefaultLibationFilesDirectory.
//If we can't write here, something is very wrong and we shouldn't even try to continue.
if (!TryCreateDirectory())
{
await MessageBoxBase.ShowAsyncImpl(null,
$"""
An error occurred while creating default settings folder:
{LibationFiles.DefaultLibationFilesDirectory}
""",
"Error Creating Libation Settings",
MessageBoxButtons.OK,
MessageBoxIcon.Error,
MessageBoxDefaultButton.Button1);
return false;
}
}
if (Files.SettingsAreValid)
return true;
try
{
WriteDefaultSettingsFile(Files.SettingsFilePath);
return Files.SettingsAreValid;
}
catch (Exception ex)
{
// We are able to create the LibationFiles directory, but we can't write a settings file in it.
// Examples of this is the root of a system drive (C:\)
Log.Logger.TryLogError(ex, $"Failed to create {LibationFiles.SETTINGS_JSON} in {Files.Location}");
if (!Files.Location.PathWithoutPrefix.Equals(LibationFiles.DefaultLibationFilesDirectory, StringComparison.OrdinalIgnoreCase))
{
var result = await MessageBoxBase.ShowAsyncImpl(null,
$"""
Could not create the Libation Settings file at:
{Files.SettingsFilePath}
Would you like to create a new install settings in this folder?
{LibationFiles.DefaultLibationFilesDirectory}
""",
"Error Creating Libation Settings",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question,
MessageBoxDefaultButton.Button1);
if (result is not DialogResult.Yes)
return false;
// Try again in the default location
Log.Logger.TryLogInformation($"Changing {LibationFiles.LIBATION_FILES_KEY} to {LibationFiles.DefaultLibationFilesDirectory}");
Files.SetLibationFiles(LibationFiles.DefaultLibationFilesDirectory);
return await CreateDefaultSettingsAsync();
}
else
{
await MessageBoxBase.ShowAsyncImpl(null,
$"""
An error occurred while creating default settings file in:
{LibationFiles.DefaultLibationFilesDirectory}
""",
"Error Creating Libation Settings",
MessageBoxButtons.OK,
MessageBoxIcon.Error,
MessageBoxDefaultButton.Button1);
return false;
}
}
}
private bool TryCreateDirectory()
{
try
{
Directory.CreateDirectory(Files.Location);
return true;
}
catch (Exception ex)
{
Log.Logger.TryLogError(ex, $"Failed to create {LibationFiles.LIBATION_FILES_KEY} directory at {Files.Location}");
return false;
}
}
private void WriteDefaultSettingsFile(string settingsFilePath)
{
var booksParent = Configuration.IsWindows ? Files.Location.Path : Configuration.MyMusic;
var jObj = new JObject
{
{ nameof(Configuration.Books), Path.Combine(booksParent, nameof(Configuration.Books)) }
};
var contents = JsonConvert.SerializeObject(jObj, Formatting.Indented);
File.WriteAllText(settingsFilePath, contents);
}
}