365 lines
13 KiB
C#
365 lines
13 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Net.Http;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using AlayaCore.Abstractions.Interfaces.Clients;
|
|
using AlayaCore.Abstractions.Interfaces.Services;
|
|
using AlayaCore.Models.Configuration;
|
|
using AlayaCore.Models.Manifests;
|
|
using AlayaCore.Models.Manifests.DTO;
|
|
using AlayaCore.Utilities.Extensions;
|
|
using Newtonsoft.Json;
|
|
|
|
namespace AlayaCore.Services
|
|
{
|
|
public sealed class ManifestService : IManifestService
|
|
{
|
|
private const string CORE_MANIFEST_FILE_NAME = "CoreManifest.json";
|
|
private const string LAUNCHER_MANIFEST_FILE_NAME = "LauncherManifest.json";
|
|
private const string INSTALLED_MODS_MANIFEST_FILE_NAME = "InstalledModsManifest.json";
|
|
|
|
private readonly IDownloadService _downloadService;
|
|
private readonly IHttpClient _httpClient;
|
|
private readonly ManifestServiceOptions _options;
|
|
|
|
public ManifestService(
|
|
IDownloadService downloadService,
|
|
IHttpClient httpClient,
|
|
ManifestServiceOptions options)
|
|
{
|
|
_downloadService = downloadService ?? throw new ArgumentNullException(nameof(downloadService));
|
|
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
|
}
|
|
|
|
public Task<ManifestModel> GetCoreManifestAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
string destinationPath = GetCoreManifestPath();
|
|
|
|
return DownloadAndLoadManifestAsync<ManifestDto, ManifestModel>(
|
|
_options.CoreManifestUri,
|
|
destinationPath,
|
|
_options.CoreManifestSha512Hash,
|
|
static dto => dto.ToModel(),
|
|
cancellationToken);
|
|
}
|
|
|
|
public async Task<InstalledModsManifestModel> GetInstalledModsManifestAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
string path = GetInstalledModsManifestPath();
|
|
|
|
if (!File.Exists(path))
|
|
{
|
|
return InstalledModsManifestModel.Empty();
|
|
}
|
|
|
|
string json = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false);
|
|
|
|
if (string.IsNullOrWhiteSpace(json))
|
|
{
|
|
return InstalledModsManifestModel.Empty();
|
|
}
|
|
|
|
InstalledModsManifestModel? manifest = DeserializeAndMapManifest<InstalledModsManifestDto, InstalledModsManifestModel>(
|
|
json,
|
|
path,
|
|
static dto => dto.ToModel(),
|
|
swallowDeserializationErrors: true);
|
|
|
|
return manifest ?? InstalledModsManifestModel.Empty();
|
|
}
|
|
|
|
public Task<LauncherManifestModel> GetLauncherManifestAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
string destinationPath = GetLauncherManifestPath();
|
|
|
|
return DownloadAndLoadManifestAsync<LauncherManifestDto, LauncherManifestModel>(
|
|
_options.LauncherManifestUri,
|
|
destinationPath,
|
|
_options.LauncherManifestSha512Hash,
|
|
static dto => dto.ToModel(),
|
|
cancellationToken);
|
|
}
|
|
|
|
public Task<ManifestModel?> GetLocalCoreManifestAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
return LoadLocalManifestAsync<ManifestDto, ManifestModel>(
|
|
GetCoreManifestPath(),
|
|
static dto => dto.ToModel(),
|
|
cancellationToken);
|
|
}
|
|
|
|
public Task<LauncherManifestModel?> GetLocalLauncherManifestAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
return LoadLocalManifestAsync<LauncherManifestDto, LauncherManifestModel>(
|
|
GetLauncherManifestPath(),
|
|
static dto => dto.ToModel(),
|
|
cancellationToken);
|
|
}
|
|
|
|
public async Task<Version> GetRemoteCoreManifestVersionAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
using HttpResponseMessage response = await _httpClient.GetAsync(
|
|
_options.CoreManifestUri,
|
|
HttpCompletionOption.ResponseContentRead,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
|
|
|
if (string.IsNullOrWhiteSpace(json))
|
|
{
|
|
throw new InvalidDataException(
|
|
$"Remote core manifest response from '{_options.CoreManifestUri}' was empty.");
|
|
}
|
|
|
|
ManifestModel remoteManifest = DeserializeAndMapManifest<ManifestDto, ManifestModel>(
|
|
json,
|
|
_options.CoreManifestUri.ToString(),
|
|
static dto => dto.ToModel(),
|
|
swallowDeserializationErrors: false)!;
|
|
|
|
if (remoteManifest.AlayaVersion == null)
|
|
{
|
|
throw new InvalidDataException(
|
|
$"Remote core manifest from '{_options.CoreManifestUri}' does not contain a valid version.");
|
|
}
|
|
|
|
return remoteManifest.AlayaVersion;
|
|
}
|
|
|
|
public async Task<string> GetRemoteLauncherManifestHashAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
using HttpResponseMessage response = await _httpClient.GetAsync(
|
|
_options.LauncherManifestUri,
|
|
HttpCompletionOption.ResponseContentRead,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
|
|
|
if (string.IsNullOrWhiteSpace(json))
|
|
{
|
|
throw new InvalidDataException(
|
|
$"Remote launcher manifest response from '{_options.LauncherManifestUri}' was empty.");
|
|
}
|
|
|
|
LauncherManifestModel remoteManifest = DeserializeAndMapManifest<LauncherManifestDto, LauncherManifestModel>(
|
|
json,
|
|
_options.LauncherManifestUri.ToString(),
|
|
static dto => dto.ToModel(),
|
|
swallowDeserializationErrors: false)!;
|
|
|
|
if (string.IsNullOrWhiteSpace(remoteManifest.Sha512Hash))
|
|
{
|
|
throw new InvalidDataException(
|
|
$"Remote launcher manifest from '{_options.LauncherManifestUri}' does not contain a valid SHA-512 hash.");
|
|
}
|
|
|
|
return remoteManifest.Sha512Hash.Trim();
|
|
}
|
|
|
|
public string GetLauncherManifestPath()
|
|
{
|
|
return Path.Combine(_options.ManifestDirectoryPath, LAUNCHER_MANIFEST_FILE_NAME);
|
|
}
|
|
|
|
public string GetCoreManifestPath()
|
|
{
|
|
return Path.Combine(_options.ManifestDirectoryPath, CORE_MANIFEST_FILE_NAME);
|
|
}
|
|
|
|
public string GetInstalledModsManifestPath()
|
|
{
|
|
return Path.Combine(_options.ManifestDirectoryPath, INSTALLED_MODS_MANIFEST_FILE_NAME);
|
|
}
|
|
|
|
private async Task<TModel?> LoadLocalManifestAsync<TDto, TModel>(
|
|
string path,
|
|
Func<TDto, TModel> map,
|
|
CancellationToken cancellationToken)
|
|
where TDto : class
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
{
|
|
throw new ArgumentException("Manifest path cannot be null, empty, or whitespace.", nameof(path));
|
|
}
|
|
|
|
if (map == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(map));
|
|
}
|
|
|
|
if (!File.Exists(path))
|
|
{
|
|
return default;
|
|
}
|
|
|
|
string json = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false);
|
|
|
|
if (string.IsNullOrWhiteSpace(json))
|
|
{
|
|
return default;
|
|
}
|
|
|
|
return DeserializeAndMapManifest<TDto, TModel>(
|
|
json,
|
|
path,
|
|
map,
|
|
swallowDeserializationErrors: true);
|
|
}
|
|
|
|
private async Task<TModel> DownloadAndLoadManifestAsync<TDto, TModel>(
|
|
Uri manifestUri,
|
|
string destinationPath,
|
|
string sha512Hash,
|
|
Func<TDto, TModel> map,
|
|
CancellationToken cancellationToken)
|
|
where TDto : class
|
|
{
|
|
if (manifestUri == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(manifestUri));
|
|
}
|
|
|
|
if (!manifestUri.IsAbsoluteUri)
|
|
{
|
|
throw new ArgumentException("Manifest URI must be absolute.", nameof(manifestUri));
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(destinationPath))
|
|
{
|
|
throw new ArgumentException("Destination path cannot be null, empty, or whitespace.", nameof(destinationPath));
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(sha512Hash))
|
|
{
|
|
throw new ArgumentException("SHA-512 hash cannot be null, empty, or whitespace.", nameof(sha512Hash));
|
|
}
|
|
|
|
if (map == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(map));
|
|
}
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
EnsureDirectoryExists(destinationPath);
|
|
|
|
await _downloadService.DownloadFileAsync(
|
|
manifestUri,
|
|
destinationPath,
|
|
sha512Hash,
|
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
|
|
|
if (!File.Exists(destinationPath))
|
|
{
|
|
throw new FileNotFoundException("Manifest file was not found after download.", destinationPath);
|
|
}
|
|
|
|
string json = await File.ReadAllTextAsync(destinationPath, cancellationToken).ConfigureAwait(false);
|
|
|
|
if (string.IsNullOrWhiteSpace(json))
|
|
{
|
|
throw new InvalidDataException($"Manifest file '{destinationPath}' was empty.");
|
|
}
|
|
|
|
return DeserializeAndMapManifest<TDto, TModel>(
|
|
json,
|
|
destinationPath,
|
|
map,
|
|
swallowDeserializationErrors: false)!;
|
|
}
|
|
|
|
private static TModel? DeserializeAndMapManifest<TDto, TModel>(
|
|
string json,
|
|
string sourceName,
|
|
Func<TDto, TModel> map,
|
|
bool swallowDeserializationErrors)
|
|
where TDto : class
|
|
{
|
|
if (string.IsNullOrWhiteSpace(json))
|
|
{
|
|
if (swallowDeserializationErrors)
|
|
{
|
|
return default;
|
|
}
|
|
|
|
throw new InvalidDataException($"Manifest source '{sourceName}' was empty.");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(sourceName))
|
|
{
|
|
throw new ArgumentException("Source name cannot be null, empty, or whitespace.", nameof(sourceName));
|
|
}
|
|
|
|
if (map == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(map));
|
|
}
|
|
|
|
TDto? dto;
|
|
try
|
|
{
|
|
dto = JsonConvert.DeserializeObject<TDto>(json);
|
|
}
|
|
catch (JsonException ex)
|
|
{
|
|
if (swallowDeserializationErrors)
|
|
{
|
|
return default;
|
|
}
|
|
|
|
throw new JsonSerializationException(
|
|
$"Failed to deserialize manifest source '{sourceName}' to {typeof(TDto).Name}.",
|
|
ex);
|
|
}
|
|
|
|
if (dto == null)
|
|
{
|
|
if (swallowDeserializationErrors)
|
|
{
|
|
return default;
|
|
}
|
|
|
|
throw new JsonSerializationException(
|
|
$"Failed to deserialize manifest source '{sourceName}' to {typeof(TDto).Name}.");
|
|
}
|
|
|
|
try
|
|
{
|
|
return map(dto);
|
|
}
|
|
catch (Exception ex) when (!(ex is OperationCanceledException))
|
|
{
|
|
if (swallowDeserializationErrors)
|
|
{
|
|
return default;
|
|
}
|
|
|
|
throw new InvalidDataException(
|
|
$"Manifest source '{sourceName}' was deserialized but could not be mapped to {typeof(TModel).Name}.",
|
|
ex);
|
|
}
|
|
}
|
|
|
|
private static void EnsureDirectoryExists(string filePath)
|
|
{
|
|
string? directoryPath = Path.GetDirectoryName(filePath);
|
|
|
|
if (string.IsNullOrWhiteSpace(directoryPath))
|
|
{
|
|
throw new InvalidOperationException("A valid destination directory is required.");
|
|
}
|
|
|
|
Directory.CreateDirectory(directoryPath);
|
|
}
|
|
}
|
|
} |