Made Manifest Size source of truth.

- Updated ModService->Resolve... to return Uri.
- Removed ModrinthModInfoModel as no longer needed.
This commit is contained in:
2026-04-04 21:21:50 +01:00
parent ef135b1164
commit 823ccf4b87
6 changed files with 91 additions and 63 deletions

View File

@@ -262,7 +262,7 @@ namespace AlayaCore
foreach (ModFileEntry requiredMod in requiredMods)
{
InstalledModEntry? installedMod = installedMods.FirstOrDefault(
ModFileEntry? installedMod = installedMods.FirstOrDefault(
mod => string.Equals(mod.FileName, requiredMod.FileName, StringComparison.OrdinalIgnoreCase));
if (installedMod == null)

View File

@@ -5,7 +5,12 @@ namespace AlayaCore.Models.Manifests.DTO
{
public class InstalledModsManifestDto
{
public InstalledModsManifestDto(List<ModFileEntryDto> installedMods)
{
InstalledMods = installedMods;
}
[JsonProperty("installedMods")]
public List<InstalledModEntryDto> InstalledMods { get; private set; }
public List<ModFileEntryDto> InstalledMods { get; private set; }
}
}

View File

@@ -7,33 +7,33 @@ namespace AlayaCore.Models.Manifests
{
public sealed class InstalledModsManifestModel
{
public IReadOnlyList<InstalledModEntry> Mods { get; }
public IReadOnlyList<ModFileEntry> Mods { get; }
public InstalledModsManifestModel()
{
Mods = Array.Empty<InstalledModEntry>();
Mods = Array.Empty<ModFileEntry>();
}
public InstalledModsManifestModel(IEnumerable<InstalledModEntry> mods)
public InstalledModsManifestModel(IEnumerable<ModFileEntry> mods)
{
if (mods == null)
{
throw new ArgumentNullException(nameof(mods));
}
InstalledModEntry[] array = mods.ToArray();
ModFileEntry[] array = mods.ToArray();
if (array.Any(mod => mod == null))
{
throw new ArgumentException("Mods cannot contain null entries.", nameof(mods));
}
Mods = new ReadOnlyCollection<InstalledModEntry>(array);
Mods = new ReadOnlyCollection<ModFileEntry>(array);
}
public static InstalledModsManifestModel Empty()
{
return new InstalledModsManifestModel(Array.Empty<InstalledModEntry>());
return new InstalledModsManifestModel(Array.Empty<ModFileEntry>());
}
}
}

View File

@@ -9,19 +9,18 @@ namespace AlayaCore.Models.Manifests
public string FileName { get; }
public FileType Type { get; }
public string Sha512Hash { get; }
public long Size { get; }
public ModFileEntry(
string name,
string fileName,
FileType type,
string sha512Hash,
long size)
{
FileName = RequireNonEmpty(name, nameof(name));
FileName = RequireNonEmpty(fileName, nameof(fileName));
Type = type;
Sha512Hash = RequireNonEmpty(sha512Hash, nameof(sha512Hash));
Size = size;
Sha512Hash = RequireSha512Hash(sha512Hash, nameof(sha512Hash));
Size = RequirePositiveSize(size, nameof(size));
}
private static string RequireNonEmpty(string value, string paramName)
@@ -34,9 +33,29 @@ namespace AlayaCore.Models.Manifests
return value;
}
private static string? NormalizeOptional(string? value)
private static string RequireSha512Hash(string value, string paramName)
{
return string.IsNullOrWhiteSpace(value) ? null : value;
string normalized = RequireNonEmpty(value, paramName)
.Trim()
.Replace("-", string.Empty)
.ToLowerInvariant();
if (normalized.Length != 128)
{
throw new ArgumentException("SHA-512 hash must be 128 hexadecimal characters long.", paramName);
}
return normalized;
}
private static long RequirePositiveSize(long value, string paramName)
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(paramName, "Size must be greater than zero.");
}
return value;
}
}
}

View File

@@ -11,7 +11,9 @@ using AlayaCore.Installation;
using AlayaCore.Models;
using AlayaCore.Models.Configuration;
using AlayaCore.Models.Manifests;
using AlayaCore.Models.Manifests.DTO;
using AlayaCore.Utilities.Enums;
using AlayaCore.Utilities.Extensions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@@ -19,7 +21,7 @@ namespace AlayaCore.Services
{
public sealed class ModService : IModService
{
private const string INSTALLED_MODS_MANIFEST_FILE_NAME = "InstalledModsManifest.json";
private const string InstalledModsManifestFileName = "InstalledModsManifest.json";
private readonly IDownloadService _downloadService;
private readonly ModrinthConnectionOptions _options;
@@ -55,7 +57,7 @@ namespace AlayaCore.Services
cancellationToken.ThrowIfCancellationRequested();
IReadOnlyList<InstalledModEntry> installedMods = environment.InstalledModsManifest.Mods;
IReadOnlyList<ModFileEntry> installedMods = environment.InstalledModsManifest.Mods;
List<ModFileEntry> requiredMods = manifest.Files
.Where(file => file.Type == FileType.Mod)
@@ -63,13 +65,13 @@ namespace AlayaCore.Services
RemoveStaleMods(requiredMods);
List<InstalledModEntry> finalInstalledMods = new List<InstalledModEntry>();
List<ModFileEntry> finalInstalledMods = new List<ModFileEntry>();
foreach (ModFileEntry requiredMod in requiredMods)
{
cancellationToken.ThrowIfCancellationRequested();
InstalledModEntry? installedMod = installedMods.FirstOrDefault(
ModFileEntry? installedMod = installedMods.FirstOrDefault(
mod => string.Equals(mod.FileName, requiredMod.FileName, StringComparison.OrdinalIgnoreCase));
string destinationPath = GetModDestinationPath(requiredMod);
@@ -86,25 +88,26 @@ namespace AlayaCore.Services
continue;
}
ModrinthModInfoModel modInfo = await ResolveModUrlAsync(requiredMod, cancellationToken).ConfigureAwait(false);
Uri modUri = await ResolveModUrlAsync(requiredMod, cancellationToken).ConfigureAwait(false);
await _downloadService.DownloadFileAsync(
modInfo.ModUrl,
modUri,
destinationPath,
requiredMod.Sha512Hash,
cancellationToken: cancellationToken).ConfigureAwait(false);
finalInstalledMods.Add(new InstalledModEntry(
finalInstalledMods.Add(new ModFileEntry(
requiredMod.FileName,
requiredMod.Type,
requiredMod.Sha512Hash,
modInfo.Size));
requiredMod.Size));
}
await FlushInstalledModsManifestAsync(finalInstalledMods, cancellationToken).ConfigureAwait(false);
}
private static bool IsInstalledModUpToDate(
InstalledModEntry installedMod,
ModFileEntry installedMod,
ModFileEntry requiredMod)
{
if (installedMod == null)
@@ -126,10 +129,11 @@ namespace AlayaCore.Services
installedMod.Sha512Hash,
requiredMod.Sha512Hash,
StringComparison.OrdinalIgnoreCase)
&& installedMod.Size == requiredMod.Size;
&& installedMod.Size == requiredMod.Size
&& installedMod.Type == requiredMod.Type;
}
private async Task<ModrinthModInfoModel> ResolveModUrlAsync(
private async Task<Uri> ResolveModUrlAsync(
ModFileEntry fileEntry,
CancellationToken cancellationToken = default)
{
@@ -178,7 +182,6 @@ namespace AlayaCore.Services
}
JObject? hashesObject = selectedFile["hashes"] as JObject;
if (hashesObject == null)
{
throw new InvalidDataException(
@@ -186,7 +189,6 @@ namespace AlayaCore.Services
}
string? remoteSha512Hash = hashesObject.Value<string>("sha512");
if (string.IsNullOrWhiteSpace(remoteSha512Hash))
{
throw new InvalidDataException(
@@ -199,24 +201,33 @@ namespace AlayaCore.Services
$"The mod metadata SHA-512 hash for '{fileEntry.FileName}' did not match the required manifest hash.");
}
long? size = selectedFile.Value<long?>("size");
if (!size.HasValue || size.Value <= 0)
{
throw new InvalidDataException(
$"The mod metadata response for '{fileEntry.FileName}' did not contain a valid file size.");
}
if (size.Value != fileEntry.Size)
{
throw new InvalidDataException(
$"The mod metadata size for '{fileEntry.FileName}' did not match the required manifest size.");
}
string? modUrl = selectedFile.Value<string>("url");
if (string.IsNullOrWhiteSpace(modUrl))
{
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' did not contain a valid file URL.");
throw new InvalidDataException(
$"The mod metadata response for '{fileEntry.FileName}' did not contain a valid file URL.");
}
if (!Uri.TryCreate(modUrl, UriKind.Absolute, out Uri? result))
{
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' contained an invalid file URL.");
throw new InvalidDataException(
$"The mod metadata response for '{fileEntry.FileName}' contained an invalid file URL.");
}
long? size = selectedFile.Value<long?>("size");
if (!size.HasValue || size.Value <= 0)
{
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' did not contain a valid file size.");
}
return new ModrinthModInfoModel(result, size.Value);
return result;
}
private string BuildVersionEndpoint(string sha512Hash)
@@ -280,7 +291,7 @@ namespace AlayaCore.Services
}
private async Task FlushInstalledModsManifestAsync(
IEnumerable<InstalledModEntry> installedMods,
IEnumerable<ModFileEntry> installedMods,
CancellationToken cancellationToken = default)
{
if (installedMods == null)
@@ -288,26 +299,18 @@ namespace AlayaCore.Services
throw new ArgumentNullException(nameof(installedMods));
}
List<InstalledModEntry> entries = installedMods.ToList();
List<ModFileEntry> entries = installedMods.ToList();
InstalledModsManifestModel manifest = new InstalledModsManifestModel(entries);
string manifestsDirectory = _manifestOptions.ManifestDirectoryPath;
Directory.CreateDirectory(manifestsDirectory);
string manifestPath = Path.Combine(manifestsDirectory, INSTALLED_MODS_MANIFEST_FILE_NAME);
string manifestPath = Path.Combine(manifestsDirectory, InstalledModsManifestFileName);
string temporaryManifestPath = manifestPath + ".tmp";
string json = JsonConvert.SerializeObject(
new
{
mods = manifest.Mods.Select(mod => new
{
fileName = mod.FileName,
sha512Hash = mod.Sha512Hash,
size = mod.Size
})
},
Formatting.Indented);
InstalledModsManifestDto dto = manifest.ToDto();
string json = JsonConvert.SerializeObject(dto, Formatting.Indented);
await File.WriteAllTextAsync(temporaryManifestPath, json, cancellationToken).ConfigureAwait(false);

View File

@@ -15,17 +15,17 @@ namespace AlayaCore.Utilities.Extensions
}
return new InstalledModsManifestModel(
dto.InstalledMods?.Select(mod => mod.ToModel()) ?? Array.Empty<InstalledModEntry>());
dto.InstalledMods?.Select(mod => mod.ToModel()) ?? Array.Empty<ModFileEntry>());
}
public static InstalledModEntry ToModel(this InstalledModEntryDto dto)
public static InstalledModsManifestDto ToDto(this InstalledModsManifestModel model)
{
if (dto == null)
if (model == null)
{
throw new ArgumentNullException(nameof(dto));
throw new ArgumentNullException(nameof(model));
}
return new InstalledModEntry(dto.FileName, dto.Sha512Hash, dto.Size);
return new InstalledModsManifestDto(model.Mods.Select(mod => mod.ToDto()).ToList());
}
public static LauncherManifestModel ToModel(this LauncherManifestDto dto)
@@ -101,7 +101,8 @@ namespace AlayaCore.Utilities.Extensions
{
Name = model.FileName,
Type = model.Type,
Sha512Hash = model.Sha512Hash
Sha512Hash = model.Sha512Hash,
Size = model.Size
};
}
}