Made Manifest Size source of truth.
- Updated ModService->Resolve... to return Uri. - Removed ModrinthModInfoModel as no longer needed.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
@@ -168,9 +172,9 @@ namespace AlayaCore.Services
|
||||
}
|
||||
|
||||
JObject? selectedFile = filesArray
|
||||
.OfType<JObject>()
|
||||
.FirstOrDefault(file => file.Value<bool?>("primary") == true)
|
||||
?? filesArray.OfType<JObject>().FirstOrDefault();
|
||||
.OfType<JObject>()
|
||||
.FirstOrDefault(file => file.Value<bool?>("primary") == true)
|
||||
?? filesArray.OfType<JObject>().FirstOrDefault();
|
||||
|
||||
if (selectedFile == null)
|
||||
{
|
||||
@@ -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,30 +201,39 @@ 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)
|
||||
{
|
||||
string baseUrl = _options.BaseApiUrl?.TrimEnd('/')
|
||||
?? throw new InvalidOperationException("Modrinth base API URL is not configured.");
|
||||
?? throw new InvalidOperationException("Modrinth base API URL is not configured.");
|
||||
|
||||
return $"{baseUrl}/version_file/{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);
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user