From 80fabd73c01a02f68538962fdb26dd0282474c96 Mon Sep 17 00:00:00 2001 From: Killers Date: Fri, 24 Oct 2025 15:27:07 +0200 Subject: [PATCH 01/11] Pog --- LabApi/LabApi.csproj | 4 + .../Features/Configuration/LabApiConfig.cs | 12 + .../Loader/Features/Nuget/NugetDependency.cs | 84 +++ LabApi/Loader/Features/Nuget/NugetPackage.cs | 170 ++++++ .../Features/Nuget/NugetPackageIndex.cs | 26 + .../Features/Nuget/NugetPackageResource.cs | 37 ++ .../Features/Nuget/NugetPackagesManager.cs | 506 ++++++++++++++++++ LabApi/Loader/PluginLoader.cs | 94 +++- 8 files changed, 926 insertions(+), 7 deletions(-) create mode 100644 LabApi/Loader/Features/Nuget/NugetDependency.cs create mode 100644 LabApi/Loader/Features/Nuget/NugetPackage.cs create mode 100644 LabApi/Loader/Features/Nuget/NugetPackageIndex.cs create mode 100644 LabApi/Loader/Features/Nuget/NugetPackageResource.cs create mode 100644 LabApi/Loader/Features/Nuget/NugetPackagesManager.cs diff --git a/LabApi/LabApi.csproj b/LabApi/LabApi.csproj index edd29d88..0b9a0f5a 100644 --- a/LabApi/LabApi.csproj +++ b/LabApi/LabApi.csproj @@ -62,12 +62,16 @@ + + + + diff --git a/LabApi/Loader/Features/Configuration/LabApiConfig.cs b/LabApi/Loader/Features/Configuration/LabApiConfig.cs index 47bea231..9f0ed16e 100644 --- a/LabApi/Loader/Features/Configuration/LabApiConfig.cs +++ b/LabApi/Loader/Features/Configuration/LabApiConfig.cs @@ -27,4 +27,16 @@ public class LabApiConfig /// [Description("Whether to allow loading plugins even if they were built for a different major version of LabAPI.")] public bool LoadUnsupportedPlugins { get; set; } + + /// + /// Gets or sets the list of NuGet package source URLs used when resolving + /// and downloading dependencies from NuGet repositories. + /// + /// + /// Each entry in this list represents a NuGet feed endpoint (for example, + /// the official https://api.nuget.org/v3/index.json source). + /// Multiple sources can be specified to support private or custom feeds. + /// + [Description("List of NuGet package sources to use when resolving dependencies via NuGet.")] + public string[] NugetPackageSources { get; set; } = ["https://api.nuget.org/v3/index.json"]; } diff --git a/LabApi/Loader/Features/Nuget/NugetDependency.cs b/LabApi/Loader/Features/Nuget/NugetDependency.cs new file mode 100644 index 00000000..d73a8c26 --- /dev/null +++ b/LabApi/Loader/Features/Nuget/NugetDependency.cs @@ -0,0 +1,84 @@ +using LabApi.Features.Console; +using System; + +namespace LabApi.Loader.Features.Nuget; + +/// +/// Represents a dependency entry within a NuGet package, +/// including its identifier and version, and provides +/// helper methods for installation and status checking. +/// +public class NugetDependency +{ + /// + /// Gets or sets the unique identifier (name) of the NuGet dependency. + /// + public required string Id { get; set; } + + /// + /// Gets or sets the semantic version string of the dependency (e.g. "1.2.3"). + /// + public required string Version { get; set; } + + /// + /// Installs this NuGet dependency by downloading it from the configured source. + /// + /// + /// This method delegates to . + /// + public void Install() => NugetPackagesManager.DownloadNugetPackage(Id, Version); + + /// + /// Determines whether this dependency is already installed + /// or loaded in the current AppDomain. + /// + /// + /// true if the dependency is installed or the corresponding assembly is already loaded; + /// otherwise, false. + /// + public bool IsInstalled() + { + if (NugetPackagesManager.Packages.TryGetValue($"{Id}.{Version}", out NugetPackage package)) + { + return string.Equals(package.Version, Version, StringComparison.OrdinalIgnoreCase); + } + + if (IsAssemblyAlreadyLoaded(Id)) + { + return true; + } + + return false; + } + + /// + /// Checks whether an assembly with the given identifier is already loaded + /// into the current application domain. + /// + /// The dependency or assembly identifier to check. + /// + /// true if an assembly with the specified ID is already loaded; + /// otherwise, false. + /// + private bool IsAssemblyAlreadyLoaded(string id) + { + try + { + foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) + { + string asmName = asm.GetName().Name ?? string.Empty; + + if (asmName.Equals(id, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + catch (Exception ex) + { + Logger.Warn($"{NugetPackagesManager.Prefix} Failed to check if assembly '{id}' is already loaded: {ex.Message}"); + } + + return false; + } +} diff --git a/LabApi/Loader/Features/Nuget/NugetPackage.cs b/LabApi/Loader/Features/Nuget/NugetPackage.cs new file mode 100644 index 00000000..2a28d8ba --- /dev/null +++ b/LabApi/Loader/Features/Nuget/NugetPackage.cs @@ -0,0 +1,170 @@ +using LabApi.Features.Console; +using LabApi.Loader.Features.Misc; +using LabApi.Loader.Features.Paths; +using LabApi.Loader.Features.Plugins; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace LabApi.Loader.Features.Nuget; + +/// +/// Represents a NuGet package loaded by LabApi, including its metadata, +/// content, and dependency information. +/// +public class NugetPackage +{ + /// + /// Gets or sets the unique package identifier (name). + /// + public string Id { get; set; } = string.Empty; + + /// + /// Gets or sets the package version string (e.g. "1.2.3"). + /// + public string Version { get; set; } = string.Empty; + + /// + /// Gets or sets the tag metadata from the .nuspec file (used to identify plugins, etc.). + /// + public string Tags { get; set; } = string.Empty; + + /// + /// Gets or sets the compiled assembly content of the package (if applicable). + /// + public byte[]? RawAssembly { get; set; } = null; + + public string FilePath { get; set; } + + /// + /// Gets or sets the name of the NuGet package file (e.g. "MyPlugin.1.0.0.nupkg"). + /// + public required string FileName { get; set; } + + /// + /// Gets or sets the raw file contents of the NuGet package (.nupkg file). + /// + public required byte[] FileContent { get; set; } + + /// + /// Gets or sets the list of dependencies defined by this package. + /// + public List Dependencies { get; set; } = new List(); + + /// + /// Gets a value indicating whether this package is marked as a LabApi plugin. + /// Determined by the presence of the "labapi-plugin" tag. + /// + public bool IsPlugin => Tags + .ToLower() + .Contains("labapi-plugin"); + + /// + /// Gets or sets if package is already loaded. + /// + public bool IsLoaded { get; set; } + + /// + /// Extracts the NuGet package file (.nupkg) to the appropriate directory + /// (plugins or dependencies), depending on whether it is a plugin. + /// + /// + /// The full path to the extracted file if successful; otherwise, null. + /// + public string? Extract() + { + string? folder = GetFinalFolder(IsPlugin); + + if (folder == null) + { + Logger.Warn($"{NugetPackagesManager.Prefix} Could not extract package '{Id}' v{Version} to {(IsPlugin ? "plugins" : "dependencies")} folder: no valid path found!"); + return null; + } + + string targetFile = Path.Combine(folder, FileName); + + File.WriteAllBytes(targetFile, FileContent); + + FilePath = targetFile; + + return targetFile; + } + + /// + /// Loads package. + /// + public void Load() + { + if (IsLoaded) + { + return; + } + + if (RawAssembly?.Length == 0) + { + Logger.Warn($"{NugetPackagesManager.Prefix} Package '{Id}' v{Version} does not contain a valid assembly, skipping..."); + return; + } + + Assembly assembly = Assembly.Load(RawAssembly); + + if (IsPlugin) + { + try + { + AssemblyUtils.ResolveEmbeddedResources(assembly); + } + catch (Exception e) + { + Logger.Error($"{NugetPackagesManager.Prefix} Failed to resolve embedded resources for package '{Id}' v{Version}"); + Logger.Error(e); + } + + try + { + PluginLoader.InstantiatePlugins(assembly.GetTypes(), assembly, FilePath); + } + catch (Exception e) + { + Logger.Error($"{NugetPackagesManager.Prefix} Couldn't load the plugin inside package '{Id}' v{Version}"); + Logger.Error(e); + } + } + else + { + PluginLoader.Dependencies.Add(assembly); + } + + Logger.Info($"{NugetPackagesManager.Prefix} Package '{Id}' v{Version} was loaded!"); + + IsLoaded = true; + } + + /// + /// Resolves and returns the final folder path for the package extraction, + /// creating directories if necessary. + /// + /// + /// Indicates whether to resolve the path for a plugin (true) + /// or a dependency (false). + /// + /// + /// The full directory path where the package should be extracted, + /// or null if no valid path could be determined. + /// + private string? GetFinalFolder(bool isPlugin) + { + foreach (string path in isPlugin ? PluginLoader.Config.PluginPaths : PluginLoader.Config.DependencyPaths) + { + string resolvedPath = PluginLoader.ResolvePath(path); + string fullPath = Path.Combine(isPlugin ? PathManager.Plugins.FullName : PathManager.Dependencies.FullName, resolvedPath); + + Directory.CreateDirectory(fullPath); + + return fullPath; + } + + return null; + } +} diff --git a/LabApi/Loader/Features/Nuget/NugetPackageIndex.cs b/LabApi/Loader/Features/Nuget/NugetPackageIndex.cs new file mode 100644 index 00000000..c7cb5de9 --- /dev/null +++ b/LabApi/Loader/Features/Nuget/NugetPackageIndex.cs @@ -0,0 +1,26 @@ +using System.Runtime.Serialization; + +namespace LabApi.Loader.Features.Nuget; + +/// +/// Represents the root structure of a NuGet service index (index.json), +/// which describes the available service endpoints for a NuGet source. +/// +/// +/// The NuGet service index (usually located at https://api.nuget.org/v3/index.json) +/// provides metadata about the repository’s available APIs, such as +/// PackageBaseAddress, SearchQueryService, and others. +/// +public class NugetPackageIndex +{ + /// + /// Gets or sets the list of service resources exposed by the NuGet source. + /// + /// + /// Each resource entry describes an endpoint (for example, + /// a package base address or search service) and its supported version. + /// These are typically represented by the type. + /// + [DataMember(Name = "resources")] + public NugetPackageResource[] Resources { get; set; } = []; +} diff --git a/LabApi/Loader/Features/Nuget/NugetPackageResource.cs b/LabApi/Loader/Features/Nuget/NugetPackageResource.cs new file mode 100644 index 00000000..0b7e2d25 --- /dev/null +++ b/LabApi/Loader/Features/Nuget/NugetPackageResource.cs @@ -0,0 +1,37 @@ +using System.Runtime.Serialization; + +namespace LabApi.Loader.Features.Nuget; + +/// +/// Represents a single resource entry within a NuGet service index (index.json). +/// +/// +/// Each resource describes a specific NuGet service endpoint and its purpose, +/// such as the PackageBaseAddress (used to download packages) or +/// SearchQueryService (used to search packages). +/// +public class NugetPackageResource +{ + /// + /// Gets or sets the type of the NuGet service resource. + /// + /// + /// The @type field defines the role of the resource, for example: + /// + /// PackageBaseAddress/3.0.0 — base URL for downloading package files + /// + /// + [DataMember(Name = "@type")] + public string Type { get; set; } = string.Empty; + + /// + /// Gets or sets the absolute URL of the service endpoint. + /// + /// + /// The @id value is typically a fully qualified HTTPS URL that identifies + /// the service’s base address. For example: + /// https://api.nuget.org/v3-flatcontainer/ or + /// + [DataMember(Name = "@id")] + public string Id { get; set; } = string.Empty; +} diff --git a/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs b/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs new file mode 100644 index 00000000..5ecc31f6 --- /dev/null +++ b/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs @@ -0,0 +1,506 @@ +using LabApi.Features.Console; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Runtime.Serialization; +using System.Xml.Linq; +using Utf8Json; +using static LabApi.Loader.PluginLoader; + +namespace LabApi.Loader.Features.Nuget; + +/// +/// Provides functionality for reading, downloading, extracting, +/// and managing NuGet packages within the LabApi loader system. +/// +public class NugetPackagesManager +{ + /// + /// Prefix used for console log messages originating from NuGet operations. + /// + public const string Prefix = "[NUGET]"; + + private static readonly Dictionary _packageBaseAddressCache = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Defines the ordered list of human-readable framework names (e.g. ".NETFramework4.8"), + /// used to determine preferred dependency groups in nuspec metadata. + /// + private static string[] _frameworkPriority = new[] + { + ".NETFramework4.8", + ".NETFramework4.7.2", + ".NETFramework4.7.1", + ".NETFramework4.7", + ".NETFramework4.6.2", + ".NETFramework4.6.1", + ".NETFramework4.6", + ".NETFramework4.5.2", + ".NETFramework4.5.1", + ".NETFramework4.5", + ".NETFramework4.0", + ".NETStandard2.1", + ".NETStandard2.0", + ".NETStandard1.6", + ".NETStandard1.5", + ".NETStandard1.4", + ".NETStandard1.3", + ".NETStandard1.2", + ".NETStandard1.1", + ".NETStandard1.0", + }; + + /// + /// Defines the ordered list of NuGet Target Framework Monikers (TFMs) + /// used to prioritize assembly selection inside .nupkg archives. + /// + private static string[] _frameworkVersionPriorities = new[] + { + "net48", + "net472", + "net471", + "net47", + "net462", + "net461", + "net46", + "net452", + "net451", + "net45", + "net40", + "netstandard2.1", + "netstandard2.0", + "netstandard1.6", + "netstandard1.5", + "netstandard1.4", + "netstandard1.3", + "netstandard1.2", + "netstandard1.1", + "netstandard1.0", + }; + + /// + /// Gets the dictionary of all loaded NuGet dependencies, + /// indexed by their package ID (case-insensitive). + /// + /// + /// This collection stores all NuGet packages that have been + /// successfully loaded or installed by the loader. + /// Keys represent the dependency ID, and values contain the corresponding + /// instance and its metadata. + /// + public static Dictionary Packages { get; } = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Resolves any missing NuGet dependencies by checking all loaded packages + /// and automatically downloading missing ones from configured NuGet sources. + /// + public static void ResolveMissingNugetDependencies() + { + Logger.Info($"{Prefix} Resolving missing NuGet dependencies..."); + + int resolvedCount = 0; + + Queue packagesToResolve = new Queue(Packages.Values); + + while (packagesToResolve.Count != 0) + { + NugetPackage package = packagesToResolve.Dequeue(); + + foreach (NugetDependency dep in package.Dependencies) + { + if (dep.IsInstalled()) + { + continue; + } + + Logger.Warn($"{Prefix} Package '{package.Id}' v{package.Version} has missing dependency '{dep.Id}' v{dep.Version}, attempting to resolve..."); + + try + { + packagesToResolve.Enqueue(DownloadNugetPackage(dep.Id, dep.Version)); + resolvedCount++; + } + catch (Exception ex) + { + Logger.Error($"{Prefix} Failed to resolve dependency '{dep.Id}' v{dep.Version}"); + Logger.Error(ex); + } + } + } + } + + /// + /// Reads and parses a NuGet package from the specified file path. + /// + /// The full file system path to the .nupkg file. + /// A populated instance. + /// Thrown when the specified package file does not exist. + public static NugetPackage ReadPackage(string path) + { + if (!File.Exists(path)) + { + throw new FileNotFoundException($"Package file not found: {path}"); + } + + byte[] data = File.ReadAllBytes(path); + string name = Path.GetFileName(path); + + return ReadPackage(name, path, data); + } + + /// + /// Reads and parses a NuGet package from a byte array. + /// + /// The file name of the package. + /// The binary contents of the .nupkg file. + /// A populated instance. + public static NugetPackage ReadPackage(string name, string fullPath, byte[] data) + { + using (MemoryStream ms = new MemoryStream(data)) + { + return ReadPackage(name, fullPath, ms); + } + } + + /// + /// Reads and parses a NuGet package from a memory stream. + /// + /// The package file name. + /// A memory stream containing the .nupkg content. + /// A populated with metadata, dependencies, and assembly data loaded. + public static NugetPackage ReadPackage(string name, string fullPath, MemoryStream stream) + { + NugetPackage package = new NugetPackage() + { + FilePath = fullPath, + FileName = name, + FileContent = stream.ToArray(), + }; + + using (ZipArchive archive = new ZipArchive(stream)) + { + ZipArchiveEntry bestEntry = GetBestVersion(archive); + + if (bestEntry != null) + { + using (MemoryStream ms = new MemoryStream()) + { + using (Stream entryStream = bestEntry.Open()) + { + entryStream.CopyTo(ms); + } + + package.RawAssembly = ms.ToArray(); + } + } + + string? nuspecXml = ReadNuspecFromNupkg(archive); + + if (nuspecXml == null) + { + return package; + } + + GetMetadata(package, nuspecXml); + } + + return package; + } + + /// + /// Downloads a NuGet package from the official NuGet repository and installs it + /// into the appropriate directory (plugins or dependencies). + /// + /// The ID of the package to download. + /// The version of the package to download. + public static NugetPackage DownloadNugetPackage(string packageId, string version) + { + string[] sources = PluginLoader.Config.NugetPackageSources; + + if (sources == null || sources.Length == 0) + { + Logger.Error($"{Prefix} No NuGet package sources defined in configuration!"); + return null; + } + + byte[]? packageData = null; + string? successfulSource = null; + + foreach (string source in sources) + { + string baseAddress = GetCachedPackageBaseAddress(source); + string idLower = packageId.ToLowerInvariant(); + string verLower = version.ToLowerInvariant(); + + if (string.IsNullOrEmpty(baseAddress)) + { + continue; + } + + string downloadUrl = $"{baseAddress.TrimEnd('/')}/{idLower}/{verLower}/{idLower}.{verLower}.nupkg"; + + try + { + using (WebClient web = new WebClient()) + { + if (Uri.TryCreate(source, UriKind.Absolute, out Uri? uri) && !string.IsNullOrEmpty(uri.UserInfo)) + { + string userInfo = uri.UserInfo; + string[] parts = userInfo.Split(':', 2); + + string username = parts.Length > 0 ? Uri.UnescapeDataString(parts[0]) : string.Empty; + string password = parts.Length > 1 ? Uri.UnescapeDataString(parts[1]) : string.Empty; + + string token = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")); + web.Headers[HttpRequestHeader.Authorization] = $"Basic {token}"; + } + + packageData = web.DownloadData(downloadUrl); + successfulSource = source; + break; + } + } + catch (WebException ex) + { + Logger.Warn($"{Prefix} Failed to download '{packageId}' v{version} from {downloadUrl}"); + if (ex.Response is HttpWebResponse resp) + { + Logger.Error($"{Prefix} HTTP {(int)resp.StatusCode} - {resp.StatusDescription}"); + } + } + catch (Exception ex) + { + Logger.Warn($"{Prefix} Unexpected error while downloading from '{source}': {ex.Message}"); + } + } + + if (packageData == null) + { + Logger.Error($"{Prefix} Failed to download package '{packageId}' v{version} from all configured sources."); + return null; + } + + // Proceed to install + NugetPackage package = ReadPackage($"{packageId}.{version}.nupkg", string.Empty, packageData); + string? path = package.Extract(); + + if (path == null) + { + Logger.Error($"{Prefix} Failed to extract package '{packageId}' v{version}"); + return null; + } + + Packages.Add($"{package.Id}.{package.Version}", package); + + Logger.Info($"{Prefix} Successfully downloaded '{packageId}' v{version} from {successfulSource}"); + return package; + } + + /// + /// Resolves and caches the PackageBaseAddress for a given NuGet v3 source. + /// If the source does not expose a valid index.json or lacks the resource, returns an empty string. + /// + /// The NuGet feed base URL (e.g. https://api.nuget.org/v3/index.json). + /// The resolved PackageBaseAddress URL, or an empty string if unavailable. + private static string GetCachedPackageBaseAddress(string sourceUrl) + { + if (_packageBaseAddressCache.TryGetValue(sourceUrl, out string cached)) + { + return cached; + } + + string normalized = sourceUrl.TrimEnd('/'); + + string indexUrl = normalized.EndsWith("index.json", StringComparison.OrdinalIgnoreCase) + ? normalized + : $"{normalized}/index.json"; + + using (WebClient client = new WebClient()) + { + if (Uri.TryCreate(indexUrl, UriKind.Absolute, out Uri? uri) && !string.IsNullOrEmpty(uri.UserInfo)) + { + string userInfo = uri.UserInfo; + string[] parts = userInfo.Split(':', 2); + + string username = parts.Length > 0 ? Uri.UnescapeDataString(parts[0]) : string.Empty; + string password = parts.Length > 1 ? Uri.UnescapeDataString(parts[1]) : string.Empty; + + string token = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")); + client.Headers[HttpRequestHeader.Authorization] = $"Basic {token}"; + + UriBuilder cleanUri = new(uri) + { + UserName = string.Empty, + Password = string.Empty + }; + indexUrl = cleanUri.Uri.ToString(); + } + + string data; + try + { + data = client.DownloadString(indexUrl); + } + catch (WebException ex) + { + Logger.Warn($"{Prefix} Failed to read packages source from '{indexUrl}'"); + if (ex.Response is HttpWebResponse resp) + { + Logger.Error($"{Prefix} HTTP {(int)resp.StatusCode} - {resp.StatusDescription}"); + } + + _packageBaseAddressCache[sourceUrl] = string.Empty; + return string.Empty; + } + + NugetPackageIndex index = JsonSerializer.Deserialize(data); + + foreach (NugetPackageResource resource in index.Resources) + { + if (!resource.Type.Contains("PackageBaseAddress")) + { + continue; + } + + _packageBaseAddressCache[sourceUrl] = resource.Id; + return resource.Id; + } + } + + _packageBaseAddressCache[sourceUrl] = string.Empty; + return string.Empty; + } + + /// + /// Selects the most appropriate assembly file from a NuGet archive + /// based on the internal framework version priority list. + /// + /// The opened representing the .nupkg file. + /// The that best matches the preferred framework; otherwise null. + private static ZipArchiveEntry GetBestVersion(ZipArchive archive) + { + IEnumerable libEntries = archive.Entries.Where(x => x.FullName.StartsWith("lib/net") && x.FullName.EndsWith(".dll")); + + Dictionary sortedEntries = new Dictionary(); + + foreach (ZipArchiveEntry entry in libEntries) + { + string[] name = entry.FullName.Split('/'); + + string frameworkVersion = name[1]; + + if (!_frameworkVersionPriorities.Contains(frameworkVersion)) + { + continue; + } + + int index = _frameworkVersionPriorities.IndexOf(frameworkVersion); + + sortedEntries.Add(index, entry); + } + + KeyValuePair bestEntry = sortedEntries.OrderBy(x => x.Key).FirstOrDefault(); + + return bestEntry.Value; + } + + /// + /// Extracts and returns the XML content of the .nuspec file from within a NuGet archive. + /// + /// The instance representing the package. + /// The XML string of the .nuspec file, or null if not found. + private static string? ReadNuspecFromNupkg(ZipArchive entry) + { + ZipArchiveEntry nuspecEntry = entry.Entries.FirstOrDefault(e => e.FullName.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase)); + + if (nuspecEntry == null) + { + return null; + } + + using (StreamReader reader = new StreamReader(nuspecEntry.Open())) + { + return reader.ReadToEnd(); + } + } + + /// + /// Reads metadata and dependency information from the .nuspec XML + /// and populates the corresponding fields in the instance. + /// + /// The package instance to populate. + /// The raw XML content of the .nuspec file. + private static void GetMetadata(NugetPackage package, string nuspecXml) + { + XDocument doc = XDocument.Parse(nuspecXml); + XNamespace ns = doc.Root.GetDefaultNamespace(); + + XElement metadata = doc.Descendants(ns + "metadata").FirstOrDefault(); + + if (metadata == null) + { + return; + } + + package.Id = metadata.Element(ns + "id")?.Value ?? string.Empty; + package.Version = metadata.Element(ns + "version")?.Value ?? string.Empty; + package.Tags = metadata.Element(ns + "tags")?.Value ?? string.Empty; + + XElement depsElement = metadata.Element(ns + "dependencies"); + + if (depsElement == null) + { + return; + } + + IEnumerable groups = depsElement.Elements(ns + "group"); + XElement? selectedGroup = null; + + foreach (string framework in _frameworkPriority) + { + selectedGroup = groups.FirstOrDefault(g => + string.Equals(g.Attribute("targetFramework")?.Value, framework, StringComparison.OrdinalIgnoreCase)); + + if (selectedGroup != null) + { + break; + } + } + + if (selectedGroup == null) + { + selectedGroup = groups.LastOrDefault(); + } + + List dependencies = new List(); + + if (selectedGroup != null) + { + string? groupFramework = selectedGroup.Attribute("targetFramework")?.Value; + foreach (XElement dep in selectedGroup.Elements(ns + "dependency")) + { + dependencies.Add(new NugetDependency + { + Id = dep.Attribute("id")?.Value ?? string.Empty, + Version = dep.Attribute("version")?.Value ?? string.Empty, + }); + } + } + + foreach (XElement dep in depsElement.Elements(ns + "dependency")) + { + dependencies.Add(new NugetDependency + { + Id = dep.Attribute("id")?.Value ?? string.Empty, + Version = dep.Attribute("version")?.Value ?? string.Empty, + }); + } + + package.Dependencies = dependencies; + } +} diff --git a/LabApi/Loader/PluginLoader.cs b/LabApi/Loader/PluginLoader.cs index 0e86b10f..04ee53f5 100644 --- a/LabApi/Loader/PluginLoader.cs +++ b/LabApi/Loader/PluginLoader.cs @@ -5,6 +5,7 @@ using LabApi.Features.Wrappers; using LabApi.Loader.Features.Configuration; using LabApi.Loader.Features.Misc; +using LabApi.Loader.Features.Nuget; using LabApi.Loader.Features.Paths; using LabApi.Loader.Features.Plugins; using LabApi.Loader.Features.Plugins.Enums; @@ -25,6 +26,7 @@ public static partial class PluginLoader { private const string LoggerPrefix = "[LOADER]"; private const string DllSearchPattern = "*.dll"; + private const string NupkgSearchPattern = "*.nupkg"; private const string PdbFileExtension = ".pdb"; private const string LabApiConfigName = "LabApi-{0}.yml"; @@ -75,6 +77,8 @@ public static void Initialize() // We register all the commands in LabAPI to avoid plugin command conflicts. CommandLoader.RegisterCommands(); + ReadNugetPackages(); + // We first load all the dependencies and store them in the dependencies list LoadAllDependencies(); @@ -100,6 +104,16 @@ public static void LoadAllDependencies() // We load all the dependencies from the configured dependency directories Logger.Info($"{LoggerPrefix} Loading all dependencies"); + foreach (NugetPackage package in NugetPackagesManager.Packages.Values) + { + if (package.IsPlugin) + { + continue; + } + + package.Load(); + } + foreach (string dependencyPath in Config.DependencyPaths) { string resolvedPath = ResolvePath(dependencyPath); @@ -121,13 +135,8 @@ public static void LoadDependencies(IEnumerable files) { try { - // We load the assembly from the specified file. Assembly assembly = Assembly.Load(File.ReadAllBytes(file.FullName)); - - // And we add the assembly to the dependencies list. Dependencies.Add(assembly); - - // We finally log that the dependency has been loaded. Logger.Info($"{LoggerPrefix} Successfully loaded {assembly.FullName}"); } catch (Exception e) @@ -138,6 +147,67 @@ public static void LoadDependencies(IEnumerable files) } } + /// + /// Loads dependency info from a NuGet package (.nupkg). + /// + private static void ReadNugetPackage(FileInfo file) + { + try + { + NugetPackage package = NugetPackagesManager.ReadPackage(file.FullName); + + string id = $"{package.Id}.{package.Version}"; + + if (NugetPackagesManager.Packages.ContainsKey(id)) + { + Logger.Warn($"{LoggerPrefix} Duplicate NuGet package dependency '{id}' found in '{file.FullName}', skipping..."); + return; + } + + NugetPackagesManager.Packages.Add(id, package); + return; + } + catch (Exception e) + { + Logger.Error($"{LoggerPrefix} Failed to read package '{file.FullName}'"); + Logger.Error(e); + } + + return; + } + + private static void ReadNugetPackages() + { + List files = new List(); + + foreach (string dependencyPath in Config.DependencyPaths) + { + string resolvedPath = ResolvePath(dependencyPath); + string fullPath = Path.Combine(PathManager.Dependencies.FullName, resolvedPath); + + Directory.CreateDirectory(fullPath); + + files.AddRange(new DirectoryInfo(fullPath).GetFiles(NupkgSearchPattern)); + } + + foreach (string pluginPath in Config.PluginPaths) + { + string resolvedPath = ResolvePath(pluginPath); + string fullPath = Path.Combine(PathManager.Plugins.FullName, resolvedPath); + + Directory.CreateDirectory(fullPath); + + files.AddRange(new DirectoryInfo(fullPath).GetFiles(NupkgSearchPattern)); + } + + foreach (FileInfo file in files) + { + ReadNugetPackage(file); + } + + NugetPackagesManager.ResolveMissingNugetDependencies(); + } + /// /// Loads all plugins from the configured plugin paths in . /// Each path is relative to and supports port substitution. @@ -161,6 +231,16 @@ public static void LoadAllPlugins() } } + foreach (NugetPackage package in NugetPackagesManager.Packages.Values) + { + if (!package.IsPlugin) + { + continue; + } + + package.Load(); + } + // Then we finally enable all the plugins Logger.Info($"{LoggerPrefix} Enabling all plugins"); EnablePlugins(Plugins.Keys.OrderBy(static plugin => plugin.Priority)); @@ -353,7 +433,7 @@ private static void ResolveTransparentlyModdedFlag() } } - private static string ResolvePath(string path) + public static string ResolvePath(string path) { return path.Replace("$port", Server.Port.ToString()); } @@ -428,7 +508,7 @@ Are you running an older version of the server? return false; } - private static void InstantiatePlugins(Type[] types, Assembly assembly, string filePath) + internal static void InstantiatePlugins(Type[] types, Assembly assembly, string filePath) { foreach (Type type in types) { From e165e1bfaee6c012e757414be58f4d8ba213f8d5 Mon Sep 17 00:00:00 2001 From: Killers0992 Date: Fri, 24 Oct 2025 21:17:51 +0200 Subject: [PATCH 02/11] Apply 6 suggestion(s) to 2 file(s) Co-authored-by: Axwabo --- LabApi/LabApi.csproj | 2 +- .../Features/Nuget/NugetPackagesManager.cs | 148 ++++++++---------- 2 files changed, 69 insertions(+), 81 deletions(-) diff --git a/LabApi/LabApi.csproj b/LabApi/LabApi.csproj index 0b9a0f5a..33c31332 100644 --- a/LabApi/LabApi.csproj +++ b/LabApi/LabApi.csproj @@ -62,7 +62,7 @@ - + diff --git a/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs b/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs index 5ecc31f6..6e354490 100644 --- a/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs +++ b/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs @@ -161,10 +161,8 @@ public static NugetPackage ReadPackage(string path) /// A populated instance. public static NugetPackage ReadPackage(string name, string fullPath, byte[] data) { - using (MemoryStream ms = new MemoryStream(data)) - { - return ReadPackage(name, fullPath, ms); - } + using MemoryStream ms = new MemoryStream(data); + return ReadPackage(name, fullPath, ms); } /// @@ -182,33 +180,28 @@ public static NugetPackage ReadPackage(string name, string fullPath, MemoryStrea FileContent = stream.ToArray(), }; - using (ZipArchive archive = new ZipArchive(stream)) + using ZipArchive archive = new ZipArchive(stream); + ZipArchiveEntry bestEntry = GetBestVersion(archive); + + if (bestEntry != null) { - ZipArchiveEntry bestEntry = GetBestVersion(archive); + using MemoryStream ms = new MemoryStream(); + using Stream entryStream = bestEntry.Open(); - if (bestEntry != null) - { - using (MemoryStream ms = new MemoryStream()) - { - using (Stream entryStream = bestEntry.Open()) - { - entryStream.CopyTo(ms); - } + entryStream.CopyTo(ms); - package.RawAssembly = ms.ToArray(); - } - } - - string? nuspecXml = ReadNuspecFromNupkg(archive); + package.RawAssembly = ms.ToArray(); + } - if (nuspecXml == null) - { - return package; - } + string? nuspecXml = ReadNuspecFromNupkg(archive); - GetMetadata(package, nuspecXml); + if (nuspecXml == null) + { + return package; } + GetMetadata(package, nuspecXml); + return package; } @@ -246,24 +239,22 @@ public static NugetPackage DownloadNugetPackage(string packageId, string version try { - using (WebClient web = new WebClient()) + using WebClient web = new WebClient(); + if (Uri.TryCreate(source, UriKind.Absolute, out Uri? uri) && !string.IsNullOrEmpty(uri.UserInfo)) { - if (Uri.TryCreate(source, UriKind.Absolute, out Uri? uri) && !string.IsNullOrEmpty(uri.UserInfo)) - { - string userInfo = uri.UserInfo; - string[] parts = userInfo.Split(':', 2); - - string username = parts.Length > 0 ? Uri.UnescapeDataString(parts[0]) : string.Empty; - string password = parts.Length > 1 ? Uri.UnescapeDataString(parts[1]) : string.Empty; + string userInfo = uri.UserInfo; + string[] parts = userInfo.Split(':', 2); - string token = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")); - web.Headers[HttpRequestHeader.Authorization] = $"Basic {token}"; - } + string username = parts.Length > 0 ? Uri.UnescapeDataString(parts[0]) : string.Empty; + string password = parts.Length > 1 ? Uri.UnescapeDataString(parts[1]) : string.Empty; - packageData = web.DownloadData(downloadUrl); - successfulSource = source; - break; + string token = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")); + web.Headers[HttpRequestHeader.Authorization] = $"Basic {token}"; } + + packageData = web.DownloadData(downloadUrl); + successfulSource = source; + break; } catch (WebException ex) { @@ -320,56 +311,55 @@ private static string GetCachedPackageBaseAddress(string sourceUrl) ? normalized : $"{normalized}/index.json"; - using (WebClient client = new WebClient()) + using WebClient client = new WebClient(); + + if (Uri.TryCreate(indexUrl, UriKind.Absolute, out Uri? uri) && !string.IsNullOrEmpty(uri.UserInfo)) { - if (Uri.TryCreate(indexUrl, UriKind.Absolute, out Uri? uri) && !string.IsNullOrEmpty(uri.UserInfo)) - { - string userInfo = uri.UserInfo; - string[] parts = userInfo.Split(':', 2); + string userInfo = uri.UserInfo; + string[] parts = userInfo.Split(':', 2); - string username = parts.Length > 0 ? Uri.UnescapeDataString(parts[0]) : string.Empty; - string password = parts.Length > 1 ? Uri.UnescapeDataString(parts[1]) : string.Empty; + string username = parts.Length > 0 ? Uri.UnescapeDataString(parts[0]) : string.Empty; + string password = parts.Length > 1 ? Uri.UnescapeDataString(parts[1]) : string.Empty; - string token = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")); - client.Headers[HttpRequestHeader.Authorization] = $"Basic {token}"; + string token = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")); + client.Headers[HttpRequestHeader.Authorization] = $"Basic {token}"; - UriBuilder cleanUri = new(uri) - { - UserName = string.Empty, - Password = string.Empty - }; - indexUrl = cleanUri.Uri.ToString(); - } + UriBuilder cleanUri = new(uri) + { + UserName = string.Empty, + Password = string.Empty + }; + indexUrl = cleanUri.Uri.ToString(); + } - string data; - try + string data; + try + { + data = client.DownloadString(indexUrl); + } + catch (WebException ex) + { + Logger.Warn($"{Prefix} Failed to read packages source from '{indexUrl}'"); + if (ex.Response is HttpWebResponse resp) { - data = client.DownloadString(indexUrl); + Logger.Error($"{Prefix} HTTP {(int)resp.StatusCode} - {resp.StatusDescription}"); } - catch (WebException ex) - { - Logger.Warn($"{Prefix} Failed to read packages source from '{indexUrl}'"); - if (ex.Response is HttpWebResponse resp) - { - Logger.Error($"{Prefix} HTTP {(int)resp.StatusCode} - {resp.StatusDescription}"); - } - _packageBaseAddressCache[sourceUrl] = string.Empty; - return string.Empty; - } + _packageBaseAddressCache[sourceUrl] = string.Empty; + return string.Empty; + } - NugetPackageIndex index = JsonSerializer.Deserialize(data); + NugetPackageIndex index = JsonSerializer.Deserialize(data); - foreach (NugetPackageResource resource in index.Resources) + foreach (NugetPackageResource resource in index.Resources) + { + if (!resource.Type.Contains("PackageBaseAddress")) { - if (!resource.Type.Contains("PackageBaseAddress")) - { - continue; - } - - _packageBaseAddressCache[sourceUrl] = resource.Id; - return resource.Id; + continue; } + + _packageBaseAddressCache[sourceUrl] = resource.Id; + return resource.Id; } _packageBaseAddressCache[sourceUrl] = string.Empty; @@ -423,10 +413,8 @@ private static ZipArchiveEntry GetBestVersion(ZipArchive archive) return null; } - using (StreamReader reader = new StreamReader(nuspecEntry.Open())) - { - return reader.ReadToEnd(); - } + using StreamReader reader = new StreamReader(nuspecEntry.Open()); + return reader.ReadToEnd(); } /// From 41bdbbfb401ba96d349ee341c842439bddecb64a Mon Sep 17 00:00:00 2001 From: Killers Date: Fri, 24 Oct 2025 21:19:20 +0200 Subject: [PATCH 03/11] Fix smix --- LabApi/Loader/Features/Nuget/NugetPackage.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/LabApi/Loader/Features/Nuget/NugetPackage.cs b/LabApi/Loader/Features/Nuget/NugetPackage.cs index 2a28d8ba..73e3fdca 100644 --- a/LabApi/Loader/Features/Nuget/NugetPackage.cs +++ b/LabApi/Loader/Features/Nuget/NugetPackage.cs @@ -74,7 +74,7 @@ public class NugetPackage /// public string? Extract() { - string? folder = GetFinalFolder(IsPlugin); + string? folder = GetFinalFolder(); if (folder == null) { @@ -145,20 +145,16 @@ public void Load() /// Resolves and returns the final folder path for the package extraction, /// creating directories if necessary. /// - /// - /// Indicates whether to resolve the path for a plugin (true) - /// or a dependency (false). - /// /// /// The full directory path where the package should be extracted, /// or null if no valid path could be determined. /// - private string? GetFinalFolder(bool isPlugin) + private string? GetFinalFolder() { - foreach (string path in isPlugin ? PluginLoader.Config.PluginPaths : PluginLoader.Config.DependencyPaths) + foreach (string path in IsPlugin ? PluginLoader.Config.PluginPaths : PluginLoader.Config.DependencyPaths) { string resolvedPath = PluginLoader.ResolvePath(path); - string fullPath = Path.Combine(isPlugin ? PathManager.Plugins.FullName : PathManager.Dependencies.FullName, resolvedPath); + string fullPath = Path.Combine(IsPlugin ? PathManager.Plugins.FullName : PathManager.Dependencies.FullName, resolvedPath); Directory.CreateDirectory(fullPath); From 46c4b21ad1e3b4f2a220be4b0b1960784261630d Mon Sep 17 00:00:00 2001 From: Killers Date: Fri, 24 Oct 2025 21:27:15 +0200 Subject: [PATCH 04/11] Yeet Nuget -> NuGet --- .../Loader/Features/Nuget/NugetDependency.cs | 12 ++--- LabApi/Loader/Features/Nuget/NugetPackage.cs | 17 +++---- .../Features/Nuget/NugetPackageIndex.cs | 8 +-- .../Features/Nuget/NugetPackageResource.cs | 4 +- .../Features/Nuget/NugetPackagesManager.cs | 49 +++++++++---------- LabApi/Loader/PluginLoader.cs | 14 +++--- 6 files changed, 50 insertions(+), 54 deletions(-) diff --git a/LabApi/Loader/Features/Nuget/NugetDependency.cs b/LabApi/Loader/Features/Nuget/NugetDependency.cs index d73a8c26..5c396541 100644 --- a/LabApi/Loader/Features/Nuget/NugetDependency.cs +++ b/LabApi/Loader/Features/Nuget/NugetDependency.cs @@ -1,14 +1,14 @@ using LabApi.Features.Console; using System; -namespace LabApi.Loader.Features.Nuget; +namespace LabApi.Loader.Features.NuGet; /// /// Represents a dependency entry within a NuGet package, /// including its identifier and version, and provides /// helper methods for installation and status checking. /// -public class NugetDependency +public class NuGetDependency { /// /// Gets or sets the unique identifier (name) of the NuGet dependency. @@ -24,9 +24,9 @@ public class NugetDependency /// Installs this NuGet dependency by downloading it from the configured source. /// /// - /// This method delegates to . + /// This method delegates to . /// - public void Install() => NugetPackagesManager.DownloadNugetPackage(Id, Version); + public void Install() => NuGetPackagesManager.DownloadNugetPackage(Id, Version); /// /// Determines whether this dependency is already installed @@ -38,7 +38,7 @@ public class NugetDependency /// public bool IsInstalled() { - if (NugetPackagesManager.Packages.TryGetValue($"{Id}.{Version}", out NugetPackage package)) + if (NuGetPackagesManager.Packages.TryGetValue($"{Id}.{Version}", out NuGetPackage package)) { return string.Equals(package.Version, Version, StringComparison.OrdinalIgnoreCase); } @@ -76,7 +76,7 @@ private bool IsAssemblyAlreadyLoaded(string id) } catch (Exception ex) { - Logger.Warn($"{NugetPackagesManager.Prefix} Failed to check if assembly '{id}' is already loaded: {ex.Message}"); + Logger.Warn($"{NuGetPackagesManager.Prefix} Failed to check if assembly '{id}' is already loaded: {ex.Message}"); } return false; diff --git a/LabApi/Loader/Features/Nuget/NugetPackage.cs b/LabApi/Loader/Features/Nuget/NugetPackage.cs index 73e3fdca..8929ff5e 100644 --- a/LabApi/Loader/Features/Nuget/NugetPackage.cs +++ b/LabApi/Loader/Features/Nuget/NugetPackage.cs @@ -1,19 +1,18 @@ using LabApi.Features.Console; using LabApi.Loader.Features.Misc; using LabApi.Loader.Features.Paths; -using LabApi.Loader.Features.Plugins; using System; using System.Collections.Generic; using System.IO; using System.Reflection; -namespace LabApi.Loader.Features.Nuget; +namespace LabApi.Loader.Features.NuGet; /// /// Represents a NuGet package loaded by LabApi, including its metadata, /// content, and dependency information. /// -public class NugetPackage +public class NuGetPackage { /// /// Gets or sets the unique package identifier (name). @@ -50,7 +49,7 @@ public class NugetPackage /// /// Gets or sets the list of dependencies defined by this package. /// - public List Dependencies { get; set; } = new List(); + public List Dependencies { get; set; } = new List(); /// /// Gets a value indicating whether this package is marked as a LabApi plugin. @@ -78,7 +77,7 @@ public class NugetPackage if (folder == null) { - Logger.Warn($"{NugetPackagesManager.Prefix} Could not extract package '{Id}' v{Version} to {(IsPlugin ? "plugins" : "dependencies")} folder: no valid path found!"); + Logger.Warn($"{NuGetPackagesManager.Prefix} Could not extract package '{Id}' v{Version} to {(IsPlugin ? "plugins" : "dependencies")} folder: no valid path found!"); return null; } @@ -103,7 +102,7 @@ public void Load() if (RawAssembly?.Length == 0) { - Logger.Warn($"{NugetPackagesManager.Prefix} Package '{Id}' v{Version} does not contain a valid assembly, skipping..."); + Logger.Warn($"{NuGetPackagesManager.Prefix} Package '{Id}' v{Version} does not contain a valid assembly, skipping..."); return; } @@ -117,7 +116,7 @@ public void Load() } catch (Exception e) { - Logger.Error($"{NugetPackagesManager.Prefix} Failed to resolve embedded resources for package '{Id}' v{Version}"); + Logger.Error($"{NuGetPackagesManager.Prefix} Failed to resolve embedded resources for package '{Id}' v{Version}"); Logger.Error(e); } @@ -127,7 +126,7 @@ public void Load() } catch (Exception e) { - Logger.Error($"{NugetPackagesManager.Prefix} Couldn't load the plugin inside package '{Id}' v{Version}"); + Logger.Error($"{NuGetPackagesManager.Prefix} Couldn't load the plugin inside package '{Id}' v{Version}"); Logger.Error(e); } } @@ -136,7 +135,7 @@ public void Load() PluginLoader.Dependencies.Add(assembly); } - Logger.Info($"{NugetPackagesManager.Prefix} Package '{Id}' v{Version} was loaded!"); + Logger.Info($"{NuGetPackagesManager.Prefix} Package '{Id}' v{Version} was loaded!"); IsLoaded = true; } diff --git a/LabApi/Loader/Features/Nuget/NugetPackageIndex.cs b/LabApi/Loader/Features/Nuget/NugetPackageIndex.cs index c7cb5de9..14648ecb 100644 --- a/LabApi/Loader/Features/Nuget/NugetPackageIndex.cs +++ b/LabApi/Loader/Features/Nuget/NugetPackageIndex.cs @@ -1,6 +1,6 @@ using System.Runtime.Serialization; -namespace LabApi.Loader.Features.Nuget; +namespace LabApi.Loader.Features.NuGet; /// /// Represents the root structure of a NuGet service index (index.json), @@ -11,7 +11,7 @@ namespace LabApi.Loader.Features.Nuget; /// provides metadata about the repository’s available APIs, such as /// PackageBaseAddress, SearchQueryService, and others. /// -public class NugetPackageIndex +public class NuGetPackageIndex { /// /// Gets or sets the list of service resources exposed by the NuGet source. @@ -19,8 +19,8 @@ public class NugetPackageIndex /// /// Each resource entry describes an endpoint (for example, /// a package base address or search service) and its supported version. - /// These are typically represented by the type. + /// These are typically represented by the type. /// [DataMember(Name = "resources")] - public NugetPackageResource[] Resources { get; set; } = []; + public NuGetPackageResource[] Resources { get; set; } = []; } diff --git a/LabApi/Loader/Features/Nuget/NugetPackageResource.cs b/LabApi/Loader/Features/Nuget/NugetPackageResource.cs index 0b7e2d25..9cb8c34c 100644 --- a/LabApi/Loader/Features/Nuget/NugetPackageResource.cs +++ b/LabApi/Loader/Features/Nuget/NugetPackageResource.cs @@ -1,6 +1,6 @@ using System.Runtime.Serialization; -namespace LabApi.Loader.Features.Nuget; +namespace LabApi.Loader.Features.NuGet; /// /// Represents a single resource entry within a NuGet service index (index.json). @@ -10,7 +10,7 @@ namespace LabApi.Loader.Features.Nuget; /// such as the PackageBaseAddress (used to download packages) or /// SearchQueryService (used to search packages). /// -public class NugetPackageResource +public class NuGetPackageResource { /// /// Gets or sets the type of the NuGet service resource. diff --git a/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs b/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs index 6e354490..50205863 100644 --- a/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs +++ b/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs @@ -1,24 +1,20 @@ using LabApi.Features.Console; using System; using System.Collections.Generic; -using System.ComponentModel; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; -using System.Reflection; -using System.Runtime.Serialization; using System.Xml.Linq; using Utf8Json; -using static LabApi.Loader.PluginLoader; -namespace LabApi.Loader.Features.Nuget; +namespace LabApi.Loader.Features.NuGet; /// /// Provides functionality for reading, downloading, extracting, /// and managing NuGet packages within the LabApi loader system. /// -public class NugetPackagesManager +public class NuGetPackagesManager { /// /// Prefix used for console log messages originating from NuGet operations. @@ -91,9 +87,9 @@ public class NugetPackagesManager /// This collection stores all NuGet packages that have been /// successfully loaded or installed by the loader. /// Keys represent the dependency ID, and values contain the corresponding - /// instance and its metadata. + /// instance and its metadata. /// - public static Dictionary Packages { get; } = new(StringComparer.OrdinalIgnoreCase); + public static Dictionary Packages { get; } = new(StringComparer.OrdinalIgnoreCase); /// /// Resolves any missing NuGet dependencies by checking all loaded packages @@ -105,13 +101,13 @@ public static void ResolveMissingNugetDependencies() int resolvedCount = 0; - Queue packagesToResolve = new Queue(Packages.Values); + Queue packagesToResolve = new Queue(Packages.Values); while (packagesToResolve.Count != 0) { - NugetPackage package = packagesToResolve.Dequeue(); + NuGetPackage package = packagesToResolve.Dequeue(); - foreach (NugetDependency dep in package.Dependencies) + foreach (NuGetDependency dep in package.Dependencies) { if (dep.IsInstalled()) { @@ -138,9 +134,9 @@ public static void ResolveMissingNugetDependencies() /// Reads and parses a NuGet package from the specified file path. /// /// The full file system path to the .nupkg file. - /// A populated instance. + /// A populated instance. /// Thrown when the specified package file does not exist. - public static NugetPackage ReadPackage(string path) + public static NuGetPackage ReadPackage(string path) { if (!File.Exists(path)) { @@ -158,8 +154,8 @@ public static NugetPackage ReadPackage(string path) /// /// The file name of the package. /// The binary contents of the .nupkg file. - /// A populated instance. - public static NugetPackage ReadPackage(string name, string fullPath, byte[] data) + /// A populated instance. + public static NuGetPackage ReadPackage(string name, string fullPath, byte[] data) { using MemoryStream ms = new MemoryStream(data); return ReadPackage(name, fullPath, ms); @@ -169,11 +165,12 @@ public static NugetPackage ReadPackage(string name, string fullPath, byte[] data /// Reads and parses a NuGet package from a memory stream. /// /// The package file name. + /// The full path of a package. /// A memory stream containing the .nupkg content. - /// A populated with metadata, dependencies, and assembly data loaded. - public static NugetPackage ReadPackage(string name, string fullPath, MemoryStream stream) + /// A populated with metadata, dependencies, and assembly data loaded. + public static NuGetPackage ReadPackage(string name, string fullPath, MemoryStream stream) { - NugetPackage package = new NugetPackage() + NuGetPackage package = new NuGetPackage() { FilePath = fullPath, FileName = name, @@ -211,7 +208,7 @@ public static NugetPackage ReadPackage(string name, string fullPath, MemoryStrea /// /// The ID of the package to download. /// The version of the package to download. - public static NugetPackage DownloadNugetPackage(string packageId, string version) + public static NuGetPackage DownloadNugetPackage(string packageId, string version) { string[] sources = PluginLoader.Config.NugetPackageSources; @@ -277,7 +274,7 @@ public static NugetPackage DownloadNugetPackage(string packageId, string version } // Proceed to install - NugetPackage package = ReadPackage($"{packageId}.{version}.nupkg", string.Empty, packageData); + NuGetPackage package = ReadPackage($"{packageId}.{version}.nupkg", string.Empty, packageData); string? path = package.Extract(); if (path == null) @@ -349,9 +346,9 @@ private static string GetCachedPackageBaseAddress(string sourceUrl) return string.Empty; } - NugetPackageIndex index = JsonSerializer.Deserialize(data); + NuGetPackageIndex index = JsonSerializer.Deserialize(data); - foreach (NugetPackageResource resource in index.Resources) + foreach (NuGetPackageResource resource in index.Resources) { if (!resource.Type.Contains("PackageBaseAddress")) { @@ -423,7 +420,7 @@ private static ZipArchiveEntry GetBestVersion(ZipArchive archive) /// /// The package instance to populate. /// The raw XML content of the .nuspec file. - private static void GetMetadata(NugetPackage package, string nuspecXml) + private static void GetMetadata(NuGetPackage package, string nuspecXml) { XDocument doc = XDocument.Parse(nuspecXml); XNamespace ns = doc.Root.GetDefaultNamespace(); @@ -465,14 +462,14 @@ private static void GetMetadata(NugetPackage package, string nuspecXml) selectedGroup = groups.LastOrDefault(); } - List dependencies = new List(); + List dependencies = new List(); if (selectedGroup != null) { string? groupFramework = selectedGroup.Attribute("targetFramework")?.Value; foreach (XElement dep in selectedGroup.Elements(ns + "dependency")) { - dependencies.Add(new NugetDependency + dependencies.Add(new NuGetDependency { Id = dep.Attribute("id")?.Value ?? string.Empty, Version = dep.Attribute("version")?.Value ?? string.Empty, @@ -482,7 +479,7 @@ private static void GetMetadata(NugetPackage package, string nuspecXml) foreach (XElement dep in depsElement.Elements(ns + "dependency")) { - dependencies.Add(new NugetDependency + dependencies.Add(new NuGetDependency { Id = dep.Attribute("id")?.Value ?? string.Empty, Version = dep.Attribute("version")?.Value ?? string.Empty, diff --git a/LabApi/Loader/PluginLoader.cs b/LabApi/Loader/PluginLoader.cs index 04ee53f5..8e406340 100644 --- a/LabApi/Loader/PluginLoader.cs +++ b/LabApi/Loader/PluginLoader.cs @@ -5,7 +5,7 @@ using LabApi.Features.Wrappers; using LabApi.Loader.Features.Configuration; using LabApi.Loader.Features.Misc; -using LabApi.Loader.Features.Nuget; +using LabApi.Loader.Features.NuGet; using LabApi.Loader.Features.Paths; using LabApi.Loader.Features.Plugins; using LabApi.Loader.Features.Plugins.Enums; @@ -104,7 +104,7 @@ public static void LoadAllDependencies() // We load all the dependencies from the configured dependency directories Logger.Info($"{LoggerPrefix} Loading all dependencies"); - foreach (NugetPackage package in NugetPackagesManager.Packages.Values) + foreach (NuGetPackage package in NuGetPackagesManager.Packages.Values) { if (package.IsPlugin) { @@ -154,17 +154,17 @@ private static void ReadNugetPackage(FileInfo file) { try { - NugetPackage package = NugetPackagesManager.ReadPackage(file.FullName); + NuGetPackage package = NuGetPackagesManager.ReadPackage(file.FullName); string id = $"{package.Id}.{package.Version}"; - if (NugetPackagesManager.Packages.ContainsKey(id)) + if (NuGetPackagesManager.Packages.ContainsKey(id)) { Logger.Warn($"{LoggerPrefix} Duplicate NuGet package dependency '{id}' found in '{file.FullName}', skipping..."); return; } - NugetPackagesManager.Packages.Add(id, package); + NuGetPackagesManager.Packages.Add(id, package); return; } catch (Exception e) @@ -205,7 +205,7 @@ private static void ReadNugetPackages() ReadNugetPackage(file); } - NugetPackagesManager.ResolveMissingNugetDependencies(); + NuGetPackagesManager.ResolveMissingNugetDependencies(); } /// @@ -231,7 +231,7 @@ public static void LoadAllPlugins() } } - foreach (NugetPackage package in NugetPackagesManager.Packages.Values) + foreach (NuGetPackage package in NuGetPackagesManager.Packages.Values) { if (!package.IsPlugin) { From 3cf2b15bf8bb940ab3b2c4d867ed605d6f82a345 Mon Sep 17 00:00:00 2001 From: Killers Date: Sat, 1 Nov 2025 15:38:29 +0100 Subject: [PATCH 05/11] brain rot --- LabApi/LabApi.csproj | 1 + .../Features/Configuration/LabApiConfig.cs | 12 ++ .../Models/NuGetDependency.cs} | 8 +- .../Models/NuGetPackage.cs} | 2 +- .../Models/NuGetPackageIndex.cs} | 2 +- .../Models/NuGetPackageResource.cs} | 2 +- .../Features/Nuget/NugetPackagesManager.cs | 118 +++++++++++------- LabApi/Loader/PluginLoader.cs | 1 + 8 files changed, 94 insertions(+), 52 deletions(-) rename LabApi/Loader/Features/{Nuget/NugetDependency.cs => NuGet/Models/NuGetDependency.cs} (87%) rename LabApi/Loader/Features/{Nuget/NugetPackage.cs => NuGet/Models/NuGetPackage.cs} (99%) rename LabApi/Loader/Features/{Nuget/NugetPackageIndex.cs => NuGet/Models/NuGetPackageIndex.cs} (95%) rename LabApi/Loader/Features/{Nuget/NugetPackageResource.cs => NuGet/Models/NuGetPackageResource.cs} (96%) diff --git a/LabApi/LabApi.csproj b/LabApi/LabApi.csproj index 33c31332..04e4906d 100644 --- a/LabApi/LabApi.csproj +++ b/LabApi/LabApi.csproj @@ -63,6 +63,7 @@ + diff --git a/LabApi/Loader/Features/Configuration/LabApiConfig.cs b/LabApi/Loader/Features/Configuration/LabApiConfig.cs index 9f0ed16e..e9206027 100644 --- a/LabApi/Loader/Features/Configuration/LabApiConfig.cs +++ b/LabApi/Loader/Features/Configuration/LabApiConfig.cs @@ -39,4 +39,16 @@ public class LabApiConfig /// [Description("List of NuGet package sources to use when resolving dependencies via NuGet.")] public string[] NugetPackageSources { get; set; } = ["https://api.nuget.org/v3/index.json"]; + + /// + /// Gets or sets a value indicating whether dependencies should be automatically + /// downloaded from the configured NuGet sources when they are missing or outdated. + /// + /// + /// When set to , the system attempts to retrieve and install + /// required packages automatically during dependency resolution. + /// Disabling this option may require manual dependency management. + /// + [Description("Automatically download dependencies from NuGet when missing or outdated.")] + public bool AutomaticallyDownloadDependencies { get; set; } = true; } diff --git a/LabApi/Loader/Features/Nuget/NugetDependency.cs b/LabApi/Loader/Features/NuGet/Models/NuGetDependency.cs similarity index 87% rename from LabApi/Loader/Features/Nuget/NugetDependency.cs rename to LabApi/Loader/Features/NuGet/Models/NuGetDependency.cs index 5c396541..bf953050 100644 --- a/LabApi/Loader/Features/Nuget/NugetDependency.cs +++ b/LabApi/Loader/Features/NuGet/Models/NuGetDependency.cs @@ -1,7 +1,7 @@ using LabApi.Features.Console; using System; -namespace LabApi.Loader.Features.NuGet; +namespace LabApi.Loader.Features.NuGet.Models; /// /// Represents a dependency entry within a NuGet package, @@ -23,10 +23,14 @@ public class NuGetDependency /// /// Installs this NuGet dependency by downloading it from the configured source. /// + /// + /// A instance representing the downloaded package, + /// or if the installation failed or the package could not be found. + /// /// /// This method delegates to . /// - public void Install() => NuGetPackagesManager.DownloadNugetPackage(Id, Version); + public NuGetPackage? Install() => NuGetPackagesManager.DownloadNugetPackage(Id, Version); /// /// Determines whether this dependency is already installed diff --git a/LabApi/Loader/Features/Nuget/NugetPackage.cs b/LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs similarity index 99% rename from LabApi/Loader/Features/Nuget/NugetPackage.cs rename to LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs index 8929ff5e..4a7696de 100644 --- a/LabApi/Loader/Features/Nuget/NugetPackage.cs +++ b/LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs @@ -6,7 +6,7 @@ using System.IO; using System.Reflection; -namespace LabApi.Loader.Features.NuGet; +namespace LabApi.Loader.Features.NuGet.Models; /// /// Represents a NuGet package loaded by LabApi, including its metadata, diff --git a/LabApi/Loader/Features/Nuget/NugetPackageIndex.cs b/LabApi/Loader/Features/NuGet/Models/NuGetPackageIndex.cs similarity index 95% rename from LabApi/Loader/Features/Nuget/NugetPackageIndex.cs rename to LabApi/Loader/Features/NuGet/Models/NuGetPackageIndex.cs index 14648ecb..79a788cc 100644 --- a/LabApi/Loader/Features/Nuget/NugetPackageIndex.cs +++ b/LabApi/Loader/Features/NuGet/Models/NuGetPackageIndex.cs @@ -1,6 +1,6 @@ using System.Runtime.Serialization; -namespace LabApi.Loader.Features.NuGet; +namespace LabApi.Loader.Features.NuGet.Models; /// /// Represents the root structure of a NuGet service index (index.json), diff --git a/LabApi/Loader/Features/Nuget/NugetPackageResource.cs b/LabApi/Loader/Features/NuGet/Models/NuGetPackageResource.cs similarity index 96% rename from LabApi/Loader/Features/Nuget/NugetPackageResource.cs rename to LabApi/Loader/Features/NuGet/Models/NuGetPackageResource.cs index 9cb8c34c..1b8ff500 100644 --- a/LabApi/Loader/Features/Nuget/NugetPackageResource.cs +++ b/LabApi/Loader/Features/NuGet/Models/NuGetPackageResource.cs @@ -1,6 +1,6 @@ using System.Runtime.Serialization; -namespace LabApi.Loader.Features.NuGet; +namespace LabApi.Loader.Features.NuGet.Models; /// /// Represents a single resource entry within a NuGet service index (index.json). diff --git a/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs b/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs index 50205863..bb53597c 100644 --- a/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs +++ b/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs @@ -1,10 +1,13 @@ using LabApi.Features.Console; +using LabApi.Loader.Features.Configuration; +using LabApi.Loader.Features.NuGet.Models; using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; +using System.Net.Http; using System.Xml.Linq; using Utf8Json; @@ -21,6 +24,8 @@ public class NuGetPackagesManager /// public const string Prefix = "[NUGET]"; + private static HttpClient _client = new HttpClient(); + private static readonly Dictionary _packageBaseAddressCache = new(StringComparer.OrdinalIgnoreCase); /// @@ -114,11 +119,23 @@ public static void ResolveMissingNugetDependencies() continue; } - Logger.Warn($"{Prefix} Package '{package.Id}' v{package.Version} has missing dependency '{dep.Id}' v{dep.Version}, attempting to resolve..."); + Logger.Warn($"{Prefix} Package '{package.Id}' v{package.Version} has missing dependency '{dep.Id}' v{dep.Version}{(PluginLoader.Config.AutomaticallyDownloadDependencies ? ", attempting to resolve..." : ".")}"); + + if (!PluginLoader.Config.AutomaticallyDownloadDependencies) + { + continue; + } try { - packagesToResolve.Enqueue(DownloadNugetPackage(dep.Id, dep.Version)); + NuGetPackage? downloadedPackage = dep.Install(); + + if (downloadedPackage == null) + { + continue; + } + + packagesToResolve.Enqueue(downloadedPackage); resolvedCount++; } catch (Exception ex) @@ -208,7 +225,7 @@ public static NuGetPackage ReadPackage(string name, string fullPath, MemoryStrea /// /// The ID of the package to download. /// The version of the package to download. - public static NuGetPackage DownloadNugetPackage(string packageId, string version) + public static NuGetPackage? DownloadNugetPackage(string packageId, string version) { string[] sources = PluginLoader.Config.NugetPackageSources; @@ -236,30 +253,26 @@ public static NuGetPackage DownloadNugetPackage(string packageId, string version try { - using WebClient web = new WebClient(); - if (Uri.TryCreate(source, UriKind.Absolute, out Uri? uri) && !string.IsNullOrEmpty(uri.UserInfo)) - { - string userInfo = uri.UserInfo; - string[] parts = userInfo.Split(':', 2); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, downloadUrl); - string username = parts.Length > 0 ? Uri.UnescapeDataString(parts[0]) : string.Empty; - string password = parts.Length > 1 ? Uri.UnescapeDataString(parts[1]) : string.Empty; + using HttpResponseMessage response = _client.SendAsync(request).GetAwaiter().GetResult(); - string token = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")); - web.Headers[HttpRequestHeader.Authorization] = $"Basic {token}"; + if (response.IsSuccessStatusCode) + { + packageData = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult(); + successfulSource = source; + break; + } + else + { + Logger.Warn($"{Prefix} Failed to download '{packageId}' v{version} from {downloadUrl}"); + Logger.Error($"{Prefix} HTTP {(int)response.StatusCode} - {response.ReasonPhrase}"); } - - packageData = web.DownloadData(downloadUrl); - successfulSource = source; - break; } - catch (WebException ex) + catch (HttpRequestException ex) { Logger.Warn($"{Prefix} Failed to download '{packageId}' v{version} from {downloadUrl}"); - if (ex.Response is HttpWebResponse resp) - { - Logger.Error($"{Prefix} HTTP {(int)resp.StatusCode} - {resp.StatusDescription}"); - } + Logger.Error($"{Prefix} HTTP error: {ex.Message}"); } catch (Exception ex) { @@ -308,39 +321,18 @@ private static string GetCachedPackageBaseAddress(string sourceUrl) ? normalized : $"{normalized}/index.json"; - using WebClient client = new WebClient(); - - if (Uri.TryCreate(indexUrl, UriKind.Absolute, out Uri? uri) && !string.IsNullOrEmpty(uri.UserInfo)) - { - string userInfo = uri.UserInfo; - string[] parts = userInfo.Split(':', 2); - - string username = parts.Length > 0 ? Uri.UnescapeDataString(parts[0]) : string.Empty; - string password = parts.Length > 1 ? Uri.UnescapeDataString(parts[1]) : string.Empty; - - string token = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")); - client.Headers[HttpRequestHeader.Authorization] = $"Basic {token}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, indexUrl); - UriBuilder cleanUri = new(uri) - { - UserName = string.Empty, - Password = string.Empty - }; - indexUrl = cleanUri.Uri.ToString(); - } + using HttpResponseMessage response = _client.SendAsync(request).GetAwaiter().GetResult(); string data; - try + if (response.IsSuccessStatusCode) { - data = client.DownloadString(indexUrl); + data = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); } - catch (WebException ex) + else { Logger.Warn($"{Prefix} Failed to read packages source from '{indexUrl}'"); - if (ex.Response is HttpWebResponse resp) - { - Logger.Error($"{Prefix} HTTP {(int)resp.StatusCode} - {resp.StatusDescription}"); - } _packageBaseAddressCache[sourceUrl] = string.Empty; return string.Empty; @@ -363,6 +355,38 @@ private static string GetCachedPackageBaseAddress(string sourceUrl) return string.Empty; } + /// + /// Configures Basic Authentication on the provided based on the + /// user information embedded in the specified URL, and returns a sanitized URL with credentials removed. + /// + /// The to configure. + /// The original URL that may contain embedded credentials. + /// The sanitized URL with user credentials removed. + public static string ConfigureBasicAuth(HttpClient client, string url) + { + if (Uri.TryCreate(url, UriKind.Absolute, out Uri? uri) && !string.IsNullOrEmpty(uri.UserInfo)) + { + string[] parts = uri.UserInfo.Split(':', 2); + + string username = parts.Length > 0 ? Uri.UnescapeDataString(parts[0]) : string.Empty; + string password = parts.Length > 1 ? Uri.UnescapeDataString(parts[1]) : string.Empty; + + string token = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")); + client.DefaultRequestHeaders.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", token); + + UriBuilder cleanUri = new(uri) + { + UserName = string.Empty, + Password = string.Empty, + }; + + return cleanUri.Uri.ToString(); + } + + return url; + } + /// /// Selects the most appropriate assembly file from a NuGet archive /// based on the internal framework version priority list. diff --git a/LabApi/Loader/PluginLoader.cs b/LabApi/Loader/PluginLoader.cs index 8e406340..55118fa1 100644 --- a/LabApi/Loader/PluginLoader.cs +++ b/LabApi/Loader/PluginLoader.cs @@ -6,6 +6,7 @@ using LabApi.Loader.Features.Configuration; using LabApi.Loader.Features.Misc; using LabApi.Loader.Features.NuGet; +using LabApi.Loader.Features.NuGet.Models; using LabApi.Loader.Features.Paths; using LabApi.Loader.Features.Plugins; using LabApi.Loader.Features.Plugins.Enums; From 7796e2efd2da9b52785faf3f60f4f44de134672a Mon Sep 17 00:00:00 2001 From: Killers Date: Sat, 1 Nov 2025 15:42:45 +0100 Subject: [PATCH 06/11] Apply suggestion --- .../Features/NuGet/Models/NuGetDependency.cs | 3 ++- .../Loader/Features/NuGet/Models/NuGetPackage.cs | 14 +++++++++++--- .../Loader/Features/Nuget/NugetPackagesManager.cs | 2 +- LabApi/Loader/PluginLoader.cs | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/LabApi/Loader/Features/NuGet/Models/NuGetDependency.cs b/LabApi/Loader/Features/NuGet/Models/NuGetDependency.cs index bf953050..f473db23 100644 --- a/LabApi/Loader/Features/NuGet/Models/NuGetDependency.cs +++ b/LabApi/Loader/Features/NuGet/Models/NuGetDependency.cs @@ -1,5 +1,6 @@ using LabApi.Features.Console; using System; +using System.Reflection; namespace LabApi.Loader.Features.NuGet.Models; @@ -68,7 +69,7 @@ private bool IsAssemblyAlreadyLoaded(string id) { try { - foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) + foreach (Assembly? asm in AppDomain.CurrentDomain.GetAssemblies()) { string asmName = asm.GetName().Name ?? string.Empty; diff --git a/LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs b/LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs index 4a7696de..3e93385e 100644 --- a/LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs +++ b/LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs @@ -14,6 +14,11 @@ namespace LabApi.Loader.Features.NuGet.Models; /// public class NuGetPackage { + /// + /// Identifies the plugin tag used to mark NuGet package as plugin. + /// + private const string LabApiPluginTag = "labapi-plugin"; + /// /// Gets or sets the unique package identifier (name). /// @@ -34,7 +39,10 @@ public class NuGetPackage /// public byte[]? RawAssembly { get; set; } = null; - public string FilePath { get; set; } + /// + /// Gets or sets the full path of NuGet package. + /// + public string FilePath { get; set; } = string.Empty; /// /// Gets or sets the name of the NuGet package file (e.g. "MyPlugin.1.0.0.nupkg"). @@ -57,10 +65,10 @@ public class NuGetPackage /// public bool IsPlugin => Tags .ToLower() - .Contains("labapi-plugin"); + .Contains(LabApiPluginTag); /// - /// Gets or sets if package is already loaded. + /// /// Gets or sets a value indicating whether the package is already loaded. /// public bool IsLoaded { get; set; } diff --git a/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs b/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs index bb53597c..ad1742f9 100644 --- a/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs +++ b/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs @@ -100,7 +100,7 @@ public class NuGetPackagesManager /// Resolves any missing NuGet dependencies by checking all loaded packages /// and automatically downloading missing ones from configured NuGet sources. /// - public static void ResolveMissingNugetDependencies() + public static void ResolveMissingNuGetDependencies() { Logger.Info($"{Prefix} Resolving missing NuGet dependencies..."); diff --git a/LabApi/Loader/PluginLoader.cs b/LabApi/Loader/PluginLoader.cs index 55118fa1..d06fd87f 100644 --- a/LabApi/Loader/PluginLoader.cs +++ b/LabApi/Loader/PluginLoader.cs @@ -206,7 +206,7 @@ private static void ReadNugetPackages() ReadNugetPackage(file); } - NuGetPackagesManager.ResolveMissingNugetDependencies(); + NuGetPackagesManager.ResolveMissingNuGetDependencies(); } /// From 13e59e616a1295759ad6558cc2cb17a6cdfe338e Mon Sep 17 00:00:00 2001 From: Killers Date: Sat, 1 Nov 2025 15:43:28 +0100 Subject: [PATCH 07/11] Fix comment --- LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs b/LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs index 3e93385e..9eb9ff7f 100644 --- a/LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs +++ b/LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs @@ -68,7 +68,7 @@ public class NuGetPackage .Contains(LabApiPluginTag); /// - /// /// Gets or sets a value indicating whether the package is already loaded. + /// Gets or sets a value indicating whether the package is already loaded. /// public bool IsLoaded { get; set; } From c9cd07292e23d41361a4233227187c544940d8e3 Mon Sep 17 00:00:00 2001 From: Killers Date: Sat, 1 Nov 2025 15:45:56 +0100 Subject: [PATCH 08/11] Fix order --- LabApi/Loader/PluginLoader.cs | 124 +++++++++++++++++----------------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/LabApi/Loader/PluginLoader.cs b/LabApi/Loader/PluginLoader.cs index d06fd87f..8c536d0a 100644 --- a/LabApi/Loader/PluginLoader.cs +++ b/LabApi/Loader/PluginLoader.cs @@ -78,7 +78,7 @@ public static void Initialize() // We register all the commands in LabAPI to avoid plugin command conflicts. CommandLoader.RegisterCommands(); - ReadNugetPackages(); + RegisterNuGetPackage(); // We first load all the dependencies and store them in the dependencies list LoadAllDependencies(); @@ -148,67 +148,6 @@ public static void LoadDependencies(IEnumerable files) } } - /// - /// Loads dependency info from a NuGet package (.nupkg). - /// - private static void ReadNugetPackage(FileInfo file) - { - try - { - NuGetPackage package = NuGetPackagesManager.ReadPackage(file.FullName); - - string id = $"{package.Id}.{package.Version}"; - - if (NuGetPackagesManager.Packages.ContainsKey(id)) - { - Logger.Warn($"{LoggerPrefix} Duplicate NuGet package dependency '{id}' found in '{file.FullName}', skipping..."); - return; - } - - NuGetPackagesManager.Packages.Add(id, package); - return; - } - catch (Exception e) - { - Logger.Error($"{LoggerPrefix} Failed to read package '{file.FullName}'"); - Logger.Error(e); - } - - return; - } - - private static void ReadNugetPackages() - { - List files = new List(); - - foreach (string dependencyPath in Config.DependencyPaths) - { - string resolvedPath = ResolvePath(dependencyPath); - string fullPath = Path.Combine(PathManager.Dependencies.FullName, resolvedPath); - - Directory.CreateDirectory(fullPath); - - files.AddRange(new DirectoryInfo(fullPath).GetFiles(NupkgSearchPattern)); - } - - foreach (string pluginPath in Config.PluginPaths) - { - string resolvedPath = ResolvePath(pluginPath); - string fullPath = Path.Combine(PathManager.Plugins.FullName, resolvedPath); - - Directory.CreateDirectory(fullPath); - - files.AddRange(new DirectoryInfo(fullPath).GetFiles(NupkgSearchPattern)); - } - - foreach (FileInfo file in files) - { - ReadNugetPackage(file); - } - - NuGetPackagesManager.ResolveMissingNuGetDependencies(); - } - /// /// Loads all plugins from the configured plugin paths in . /// Each path is relative to and supports port substitution. @@ -372,6 +311,67 @@ public static void EnablePlugin(Plugin plugin) } } + /// + /// Loads dependency info from a NuGet package (.nupkg). + /// + private static void ReadNugetPackage(FileInfo file) + { + try + { + NuGetPackage package = NuGetPackagesManager.ReadPackage(file.FullName); + + string id = $"{package.Id}.{package.Version}"; + + if (NuGetPackagesManager.Packages.ContainsKey(id)) + { + Logger.Warn($"{LoggerPrefix} Duplicate NuGet package dependency '{id}' found in '{file.FullName}', skipping..."); + return; + } + + NuGetPackagesManager.Packages.Add(id, package); + return; + } + catch (Exception e) + { + Logger.Error($"{LoggerPrefix} Failed to read package '{file.FullName}'"); + Logger.Error(e); + } + + return; + } + + private static void RegisterNuGetPackage() + { + List files = new List(); + + foreach (string dependencyPath in Config.DependencyPaths) + { + string resolvedPath = ResolvePath(dependencyPath); + string fullPath = Path.Combine(PathManager.Dependencies.FullName, resolvedPath); + + Directory.CreateDirectory(fullPath); + + files.AddRange(new DirectoryInfo(fullPath).GetFiles(NupkgSearchPattern)); + } + + foreach (string pluginPath in Config.PluginPaths) + { + string resolvedPath = ResolvePath(pluginPath); + string fullPath = Path.Combine(PathManager.Plugins.FullName, resolvedPath); + + Directory.CreateDirectory(fullPath); + + files.AddRange(new DirectoryInfo(fullPath).GetFiles(NupkgSearchPattern)); + } + + foreach (FileInfo file in files) + { + ReadNugetPackage(file); + } + + NuGetPackagesManager.ResolveMissingNuGetDependencies(); + } + /// /// Loads or creates the LabAPI configuration file. /// From 30d42b3665e0129a52f017481acc715e90d7256c Mon Sep 17 00:00:00 2001 From: Killers Date: Sat, 1 Nov 2025 15:46:55 +0100 Subject: [PATCH 09/11] Fix methods order --- LabApi/Loader/PluginLoader.cs | 60 +++++++++++++++++------------------ 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/LabApi/Loader/PluginLoader.cs b/LabApi/Loader/PluginLoader.cs index 8c536d0a..378f3ea5 100644 --- a/LabApi/Loader/PluginLoader.cs +++ b/LabApi/Loader/PluginLoader.cs @@ -311,35 +311,6 @@ public static void EnablePlugin(Plugin plugin) } } - /// - /// Loads dependency info from a NuGet package (.nupkg). - /// - private static void ReadNugetPackage(FileInfo file) - { - try - { - NuGetPackage package = NuGetPackagesManager.ReadPackage(file.FullName); - - string id = $"{package.Id}.{package.Version}"; - - if (NuGetPackagesManager.Packages.ContainsKey(id)) - { - Logger.Warn($"{LoggerPrefix} Duplicate NuGet package dependency '{id}' found in '{file.FullName}', skipping..."); - return; - } - - NuGetPackagesManager.Packages.Add(id, package); - return; - } - catch (Exception e) - { - Logger.Error($"{LoggerPrefix} Failed to read package '{file.FullName}'"); - Logger.Error(e); - } - - return; - } - private static void RegisterNuGetPackage() { List files = new List(); @@ -366,12 +337,41 @@ private static void RegisterNuGetPackage() foreach (FileInfo file in files) { - ReadNugetPackage(file); + RegisterNuGetPackage(file); } NuGetPackagesManager.ResolveMissingNuGetDependencies(); } + /// + /// Loads package info from a NuGet package (.nupkg). + /// + private static void RegisterNuGetPackage(FileInfo file) + { + try + { + NuGetPackage package = NuGetPackagesManager.ReadPackage(file.FullName); + + string id = $"{package.Id}.{package.Version}"; + + if (NuGetPackagesManager.Packages.ContainsKey(id)) + { + Logger.Warn($"{LoggerPrefix} Duplicate NuGet package dependency '{id}' found in '{file.FullName}', skipping..."); + return; + } + + NuGetPackagesManager.Packages.Add(id, package); + return; + } + catch (Exception e) + { + Logger.Error($"{LoggerPrefix} Failed to read package '{file.FullName}'"); + Logger.Error(e); + } + + return; + } + /// /// Loads or creates the LabAPI configuration file. /// From 31143c15d88049e989ff43f9d57612a1d1783e2e Mon Sep 17 00:00:00 2001 From: Killers Date: Mon, 10 Nov 2025 18:31:23 +0100 Subject: [PATCH 10/11] Updot --- .../Features/Nuget/NugetPackagesManager.cs | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs b/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs index ad1742f9..db54484b 100644 --- a/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs +++ b/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs @@ -355,38 +355,6 @@ private static string GetCachedPackageBaseAddress(string sourceUrl) return string.Empty; } - /// - /// Configures Basic Authentication on the provided based on the - /// user information embedded in the specified URL, and returns a sanitized URL with credentials removed. - /// - /// The to configure. - /// The original URL that may contain embedded credentials. - /// The sanitized URL with user credentials removed. - public static string ConfigureBasicAuth(HttpClient client, string url) - { - if (Uri.TryCreate(url, UriKind.Absolute, out Uri? uri) && !string.IsNullOrEmpty(uri.UserInfo)) - { - string[] parts = uri.UserInfo.Split(':', 2); - - string username = parts.Length > 0 ? Uri.UnescapeDataString(parts[0]) : string.Empty; - string password = parts.Length > 1 ? Uri.UnescapeDataString(parts[1]) : string.Empty; - - string token = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")); - client.DefaultRequestHeaders.Authorization = - new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", token); - - UriBuilder cleanUri = new(uri) - { - UserName = string.Empty, - Password = string.Empty, - }; - - return cleanUri.Uri.ToString(); - } - - return url; - } - /// /// Selects the most appropriate assembly file from a NuGet archive /// based on the internal framework version priority list. From 1e7533838bfa4bf27abeda73c19c69c7326d332d Mon Sep 17 00:00:00 2001 From: Killers Date: Mon, 10 Nov 2025 18:34:53 +0100 Subject: [PATCH 11/11] Fixes --- LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs | 2 +- LabApi/Loader/Features/Nuget/NugetPackagesManager.cs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs b/LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs index 9eb9ff7f..0edc773c 100644 --- a/LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs +++ b/LabApi/Loader/Features/NuGet/Models/NuGetPackage.cs @@ -79,7 +79,7 @@ public class NuGetPackage /// /// The full path to the extracted file if successful; otherwise, null. /// - public string? Extract() + public string? ExtractToFolder() { string? folder = GetFinalFolder(); diff --git a/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs b/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs index db54484b..4b310a90 100644 --- a/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs +++ b/LabApi/Loader/Features/Nuget/NugetPackagesManager.cs @@ -170,6 +170,7 @@ public static NuGetPackage ReadPackage(string path) /// Reads and parses a NuGet package from a byte array. /// /// The file name of the package. + /// The file full path. /// The binary contents of the .nupkg file. /// A populated instance. public static NuGetPackage ReadPackage(string name, string fullPath, byte[] data) @@ -288,7 +289,9 @@ public static NuGetPackage ReadPackage(string name, string fullPath, MemoryStrea // Proceed to install NuGetPackage package = ReadPackage($"{packageId}.{version}.nupkg", string.Empty, packageData); - string? path = package.Extract(); + + // Extracts nuget package to specific folder if thats plugin or dependency. + string? path = package.ExtractToFolder(); if (path == null) {