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)
|
foreach (ModFileEntry requiredMod in requiredMods)
|
||||||
{
|
{
|
||||||
InstalledModEntry? installedMod = installedMods.FirstOrDefault(
|
ModFileEntry? installedMod = installedMods.FirstOrDefault(
|
||||||
mod => string.Equals(mod.FileName, requiredMod.FileName, StringComparison.OrdinalIgnoreCase));
|
mod => string.Equals(mod.FileName, requiredMod.FileName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (installedMod == null)
|
if (installedMod == null)
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ namespace AlayaCore.Models.Manifests.DTO
|
|||||||
{
|
{
|
||||||
public class InstalledModsManifestDto
|
public class InstalledModsManifestDto
|
||||||
{
|
{
|
||||||
|
public InstalledModsManifestDto(List<ModFileEntryDto> installedMods)
|
||||||
|
{
|
||||||
|
InstalledMods = installedMods;
|
||||||
|
}
|
||||||
|
|
||||||
[JsonProperty("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 sealed class InstalledModsManifestModel
|
||||||
{
|
{
|
||||||
public IReadOnlyList<InstalledModEntry> Mods { get; }
|
public IReadOnlyList<ModFileEntry> Mods { get; }
|
||||||
|
|
||||||
public InstalledModsManifestModel()
|
public InstalledModsManifestModel()
|
||||||
{
|
{
|
||||||
Mods = Array.Empty<InstalledModEntry>();
|
Mods = Array.Empty<ModFileEntry>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public InstalledModsManifestModel(IEnumerable<InstalledModEntry> mods)
|
public InstalledModsManifestModel(IEnumerable<ModFileEntry> mods)
|
||||||
{
|
{
|
||||||
if (mods == null)
|
if (mods == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(mods));
|
throw new ArgumentNullException(nameof(mods));
|
||||||
}
|
}
|
||||||
|
|
||||||
InstalledModEntry[] array = mods.ToArray();
|
ModFileEntry[] array = mods.ToArray();
|
||||||
|
|
||||||
if (array.Any(mod => mod == null))
|
if (array.Any(mod => mod == null))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Mods cannot contain null entries.", nameof(mods));
|
throw new ArgumentException("Mods cannot contain null entries.", nameof(mods));
|
||||||
}
|
}
|
||||||
|
|
||||||
Mods = new ReadOnlyCollection<InstalledModEntry>(array);
|
Mods = new ReadOnlyCollection<ModFileEntry>(array);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InstalledModsManifestModel Empty()
|
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 string FileName { get; }
|
||||||
public FileType Type { get; }
|
public FileType Type { get; }
|
||||||
public string Sha512Hash { get; }
|
public string Sha512Hash { get; }
|
||||||
|
|
||||||
public long Size { get; }
|
public long Size { get; }
|
||||||
|
|
||||||
public ModFileEntry(
|
public ModFileEntry(
|
||||||
string name,
|
string fileName,
|
||||||
FileType type,
|
FileType type,
|
||||||
string sha512Hash,
|
string sha512Hash,
|
||||||
long size)
|
long size)
|
||||||
{
|
{
|
||||||
FileName = RequireNonEmpty(name, nameof(name));
|
FileName = RequireNonEmpty(fileName, nameof(fileName));
|
||||||
Type = type;
|
Type = type;
|
||||||
Sha512Hash = RequireNonEmpty(sha512Hash, nameof(sha512Hash));
|
Sha512Hash = RequireSha512Hash(sha512Hash, nameof(sha512Hash));
|
||||||
Size = size;
|
Size = RequirePositiveSize(size, nameof(size));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string RequireNonEmpty(string value, string paramName)
|
private static string RequireNonEmpty(string value, string paramName)
|
||||||
@@ -34,9 +33,29 @@ namespace AlayaCore.Models.Manifests
|
|||||||
return value;
|
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;
|
||||||
using AlayaCore.Models.Configuration;
|
using AlayaCore.Models.Configuration;
|
||||||
using AlayaCore.Models.Manifests;
|
using AlayaCore.Models.Manifests;
|
||||||
|
using AlayaCore.Models.Manifests.DTO;
|
||||||
using AlayaCore.Utilities.Enums;
|
using AlayaCore.Utilities.Enums;
|
||||||
|
using AlayaCore.Utilities.Extensions;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
@@ -19,7 +21,7 @@ namespace AlayaCore.Services
|
|||||||
{
|
{
|
||||||
public sealed class ModService : IModService
|
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 IDownloadService _downloadService;
|
||||||
private readonly ModrinthConnectionOptions _options;
|
private readonly ModrinthConnectionOptions _options;
|
||||||
@@ -55,7 +57,7 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
IReadOnlyList<InstalledModEntry> installedMods = environment.InstalledModsManifest.Mods;
|
IReadOnlyList<ModFileEntry> installedMods = environment.InstalledModsManifest.Mods;
|
||||||
|
|
||||||
List<ModFileEntry> requiredMods = manifest.Files
|
List<ModFileEntry> requiredMods = manifest.Files
|
||||||
.Where(file => file.Type == FileType.Mod)
|
.Where(file => file.Type == FileType.Mod)
|
||||||
@@ -63,13 +65,13 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
RemoveStaleMods(requiredMods);
|
RemoveStaleMods(requiredMods);
|
||||||
|
|
||||||
List<InstalledModEntry> finalInstalledMods = new List<InstalledModEntry>();
|
List<ModFileEntry> finalInstalledMods = new List<ModFileEntry>();
|
||||||
|
|
||||||
foreach (ModFileEntry requiredMod in requiredMods)
|
foreach (ModFileEntry requiredMod in requiredMods)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
InstalledModEntry? installedMod = installedMods.FirstOrDefault(
|
ModFileEntry? installedMod = installedMods.FirstOrDefault(
|
||||||
mod => string.Equals(mod.FileName, requiredMod.FileName, StringComparison.OrdinalIgnoreCase));
|
mod => string.Equals(mod.FileName, requiredMod.FileName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
string destinationPath = GetModDestinationPath(requiredMod);
|
string destinationPath = GetModDestinationPath(requiredMod);
|
||||||
@@ -86,25 +88,26 @@ namespace AlayaCore.Services
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ModrinthModInfoModel modInfo = await ResolveModUrlAsync(requiredMod, cancellationToken).ConfigureAwait(false);
|
Uri modUri = await ResolveModUrlAsync(requiredMod, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
await _downloadService.DownloadFileAsync(
|
await _downloadService.DownloadFileAsync(
|
||||||
modInfo.ModUrl,
|
modUri,
|
||||||
destinationPath,
|
destinationPath,
|
||||||
requiredMod.Sha512Hash,
|
requiredMod.Sha512Hash,
|
||||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
finalInstalledMods.Add(new InstalledModEntry(
|
finalInstalledMods.Add(new ModFileEntry(
|
||||||
requiredMod.FileName,
|
requiredMod.FileName,
|
||||||
|
requiredMod.Type,
|
||||||
requiredMod.Sha512Hash,
|
requiredMod.Sha512Hash,
|
||||||
modInfo.Size));
|
requiredMod.Size));
|
||||||
}
|
}
|
||||||
|
|
||||||
await FlushInstalledModsManifestAsync(finalInstalledMods, cancellationToken).ConfigureAwait(false);
|
await FlushInstalledModsManifestAsync(finalInstalledMods, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsInstalledModUpToDate(
|
private static bool IsInstalledModUpToDate(
|
||||||
InstalledModEntry installedMod,
|
ModFileEntry installedMod,
|
||||||
ModFileEntry requiredMod)
|
ModFileEntry requiredMod)
|
||||||
{
|
{
|
||||||
if (installedMod == null)
|
if (installedMod == null)
|
||||||
@@ -126,10 +129,11 @@ namespace AlayaCore.Services
|
|||||||
installedMod.Sha512Hash,
|
installedMod.Sha512Hash,
|
||||||
requiredMod.Sha512Hash,
|
requiredMod.Sha512Hash,
|
||||||
StringComparison.OrdinalIgnoreCase)
|
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,
|
ModFileEntry fileEntry,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -168,17 +172,16 @@ namespace AlayaCore.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
JObject? selectedFile = filesArray
|
JObject? selectedFile = filesArray
|
||||||
.OfType<JObject>()
|
.OfType<JObject>()
|
||||||
.FirstOrDefault(file => file.Value<bool?>("primary") == true)
|
.FirstOrDefault(file => file.Value<bool?>("primary") == true)
|
||||||
?? filesArray.OfType<JObject>().FirstOrDefault();
|
?? filesArray.OfType<JObject>().FirstOrDefault();
|
||||||
|
|
||||||
if (selectedFile == null)
|
if (selectedFile == null)
|
||||||
{
|
{
|
||||||
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' did not contain a usable file entry.");
|
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' did not contain a usable file entry.");
|
||||||
}
|
}
|
||||||
|
|
||||||
JObject? hashesObject = selectedFile["hashes"] as JObject;
|
JObject? hashesObject = selectedFile["hashes"] as JObject;
|
||||||
|
|
||||||
if (hashesObject == null)
|
if (hashesObject == null)
|
||||||
{
|
{
|
||||||
throw new InvalidDataException(
|
throw new InvalidDataException(
|
||||||
@@ -186,7 +189,6 @@ namespace AlayaCore.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
string? remoteSha512Hash = hashesObject.Value<string>("sha512");
|
string? remoteSha512Hash = hashesObject.Value<string>("sha512");
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(remoteSha512Hash))
|
if (string.IsNullOrWhiteSpace(remoteSha512Hash))
|
||||||
{
|
{
|
||||||
throw new InvalidDataException(
|
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.");
|
$"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");
|
string? modUrl = selectedFile.Value<string>("url");
|
||||||
if (string.IsNullOrWhiteSpace(modUrl))
|
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))
|
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");
|
return result;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string BuildVersionEndpoint(string sha512Hash)
|
private string BuildVersionEndpoint(string sha512Hash)
|
||||||
{
|
{
|
||||||
string baseUrl = _options.BaseApiUrl?.TrimEnd('/')
|
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}";
|
return $"{baseUrl}/version_file/{sha512Hash}";
|
||||||
}
|
}
|
||||||
@@ -280,7 +291,7 @@ namespace AlayaCore.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task FlushInstalledModsManifestAsync(
|
private async Task FlushInstalledModsManifestAsync(
|
||||||
IEnumerable<InstalledModEntry> installedMods,
|
IEnumerable<ModFileEntry> installedMods,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (installedMods == null)
|
if (installedMods == null)
|
||||||
@@ -288,26 +299,18 @@ namespace AlayaCore.Services
|
|||||||
throw new ArgumentNullException(nameof(installedMods));
|
throw new ArgumentNullException(nameof(installedMods));
|
||||||
}
|
}
|
||||||
|
|
||||||
List<InstalledModEntry> entries = installedMods.ToList();
|
List<ModFileEntry> entries = installedMods.ToList();
|
||||||
InstalledModsManifestModel manifest = new InstalledModsManifestModel(entries);
|
InstalledModsManifestModel manifest = new InstalledModsManifestModel(entries);
|
||||||
|
|
||||||
string manifestsDirectory = _manifestOptions.ManifestDirectoryPath;
|
string manifestsDirectory = _manifestOptions.ManifestDirectoryPath;
|
||||||
Directory.CreateDirectory(manifestsDirectory);
|
Directory.CreateDirectory(manifestsDirectory);
|
||||||
|
|
||||||
string manifestPath = Path.Combine(manifestsDirectory, INSTALLED_MODS_MANIFEST_FILE_NAME);
|
string manifestPath = Path.Combine(manifestsDirectory, InstalledModsManifestFileName);
|
||||||
string temporaryManifestPath = manifestPath + ".tmp";
|
string temporaryManifestPath = manifestPath + ".tmp";
|
||||||
|
|
||||||
string json = JsonConvert.SerializeObject(
|
InstalledModsManifestDto dto = manifest.ToDto();
|
||||||
new
|
|
||||||
{
|
string json = JsonConvert.SerializeObject(dto, Formatting.Indented);
|
||||||
mods = manifest.Mods.Select(mod => new
|
|
||||||
{
|
|
||||||
fileName = mod.FileName,
|
|
||||||
sha512Hash = mod.Sha512Hash,
|
|
||||||
size = mod.Size
|
|
||||||
})
|
|
||||||
},
|
|
||||||
Formatting.Indented);
|
|
||||||
|
|
||||||
await File.WriteAllTextAsync(temporaryManifestPath, json, cancellationToken).ConfigureAwait(false);
|
await File.WriteAllTextAsync(temporaryManifestPath, json, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
|||||||
@@ -15,17 +15,17 @@ namespace AlayaCore.Utilities.Extensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new InstalledModsManifestModel(
|
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)
|
public static LauncherManifestModel ToModel(this LauncherManifestDto dto)
|
||||||
@@ -101,7 +101,8 @@ namespace AlayaCore.Utilities.Extensions
|
|||||||
{
|
{
|
||||||
Name = model.FileName,
|
Name = model.FileName,
|
||||||
Type = model.Type,
|
Type = model.Type,
|
||||||
Sha512Hash = model.Sha512Hash
|
Sha512Hash = model.Sha512Hash,
|
||||||
|
Size = model.Size
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user