From 6740aadca2ff4cf682692a05e7e381974042e2ac Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Thu, 5 Sep 2024 16:07:26 -0500 Subject: [PATCH 1/5] Use nameof --- Core/Types/CkanModule.cs | 2 +- Core/Versioning/GameVersion.cs | 20 ++++++++++---------- Core/Versioning/GameVersionBound.cs | 17 ++++++----------- GUI/Model/ModList.cs | 3 ++- 4 files changed, 19 insertions(+), 23 deletions(-) diff --git a/Core/Types/CkanModule.cs b/Core/Types/CkanModule.cs index 34787880a..8b775b4f5 100644 --- a/Core/Types/CkanModule.cs +++ b/Core/Types/CkanModule.cs @@ -151,7 +151,7 @@ public class CkanModule : IEquatable // and has the spec_version's in his installed_modules section // We should return this to a simple Required.Always field some time in the future // ~ Postremus, 03.09.2015 - [JsonProperty("spec_version", Order = 1)] + [JsonProperty(nameof(spec_version), Order = 1)] public ModuleVersion spec_version { get diff --git a/Core/Versioning/GameVersion.cs b/Core/Versioning/GameVersion.cs index 51dcf030c..70dda715f 100644 --- a/Core/Versioning/GameVersion.cs +++ b/Core/Versioning/GameVersion.cs @@ -127,7 +127,7 @@ public GameVersion(int major) { if (major < 0) { - throw new ArgumentOutOfRangeException("major"); + throw new ArgumentOutOfRangeException(nameof(major), major.ToString()); } _major = major; @@ -148,12 +148,12 @@ public GameVersion(int major, int minor) { if (major < 0) { - throw new ArgumentOutOfRangeException("major"); + throw new ArgumentOutOfRangeException(nameof(major), major.ToString()); } if (minor < 0) { - throw new ArgumentOutOfRangeException("minor"); + throw new ArgumentOutOfRangeException(nameof(minor), minor.ToString()); } _major = major; @@ -175,17 +175,17 @@ public GameVersion(int major, int minor, int patch) { if (major < 0) { - throw new ArgumentOutOfRangeException("major"); + throw new ArgumentOutOfRangeException(nameof(major), major.ToString()); } if (minor < 0) { - throw new ArgumentOutOfRangeException("minor"); + throw new ArgumentOutOfRangeException(nameof(minor), minor.ToString()); } if (patch < 0) { - throw new ArgumentOutOfRangeException("patch"); + throw new ArgumentOutOfRangeException(nameof(patch), patch.ToString()); } _major = major; @@ -208,22 +208,22 @@ public GameVersion(int major, int minor, int patch, int build) { if (major < 0) { - throw new ArgumentOutOfRangeException("major", major, $"{major}"); + throw new ArgumentOutOfRangeException(nameof(major), major, $"{major}"); } if (minor < 0) { - throw new ArgumentOutOfRangeException("minor", minor, $"{minor}"); + throw new ArgumentOutOfRangeException(nameof(minor), minor, $"{minor}"); } if (patch < 0) { - throw new ArgumentOutOfRangeException("patch", patch, $"{patch}"); + throw new ArgumentOutOfRangeException(nameof(patch), patch, $"{patch}"); } if (build < 0) { - throw new ArgumentOutOfRangeException("build", build, $"{build}"); + throw new ArgumentOutOfRangeException(nameof(build), build, $"{build}"); } _major = major; diff --git a/Core/Versioning/GameVersionBound.cs b/Core/Versioning/GameVersionBound.cs index 94964ee2e..8fbdab31c 100644 --- a/Core/Versioning/GameVersionBound.cs +++ b/Core/Versioning/GameVersionBound.cs @@ -19,7 +19,7 @@ public GameVersionBound(GameVersion value, bool inclusive) { if (!value.IsAny && !value.IsFullyDefined) { - throw new ArgumentException("Version must be either fully undefined or fully defined.", "value"); + throw new ArgumentException("Version must be either fully undefined or fully defined.", nameof(value)); } Value = value; @@ -120,12 +120,12 @@ public static GameVersionBound Lowest(params GameVersionBound?[] versionBounds) if (!versionBounds.Any()) { - throw new ArgumentException("Value cannot be empty.", "versionBounds"); + throw new ArgumentException("Value cannot be empty.", nameof(versionBounds)); } if (versionBounds.Contains(null)) { - throw new ArgumentException("Value cannot contain null.", "versionBounds"); + throw new ArgumentException("Value cannot contain null.", nameof(versionBounds)); } return versionBounds.OfType() @@ -144,19 +144,14 @@ public static GameVersionBound Lowest(params GameVersionBound?[] versionBounds) /// The highest value in . public static GameVersionBound Highest(params GameVersionBound?[] versionBounds) { - if (versionBounds == null) - { - throw new ArgumentNullException(nameof(versionBounds)); - } - - if (!versionBounds.Any()) + if (versionBounds.Length == 0) { - throw new ArgumentException("Value cannot be empty.", "versionBounds"); + throw new ArgumentException("Value cannot be empty.", nameof(versionBounds)); } if (versionBounds.Contains(null)) { - throw new ArgumentException("Value cannot contain null.", "versionBounds"); + throw new ArgumentException("Value cannot contain null.", nameof(versionBounds)); } return versionBounds.OfType() diff --git a/GUI/Model/ModList.cs b/GUI/Model/ModList.cs index 2b5acd219..24c95b0a5 100644 --- a/GUI/Model/ModList.cs +++ b/GUI/Model/ModList.cs @@ -164,7 +164,8 @@ public Tuple, Dictionary, List Date: Thu, 5 Sep 2024 16:08:26 -0500 Subject: [PATCH 2/5] CkanFavourite does nothing anymore --- Core/Types/ExportFileType.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Core/Types/ExportFileType.cs b/Core/Types/ExportFileType.cs index 4b54dca02..2d32f0ba7 100644 --- a/Core/Types/ExportFileType.cs +++ b/Core/Types/ExportFileType.cs @@ -3,7 +3,6 @@ namespace CKAN.Types public enum ExportFileType { Ckan, - CkanFavourite, PlainText, Markdown, BbCode, From 96ee05abb7f9fdd7deff81ee69704847c2c64fe2 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Thu, 5 Sep 2024 16:09:16 -0500 Subject: [PATCH 3/5] Treat modules with changed hashes as unequal --- Core/Types/CkanModule.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Core/Types/CkanModule.cs b/Core/Types/CkanModule.cs index 8b775b4f5..73aee6e35 100644 --- a/Core/Types/CkanModule.cs +++ b/Core/Types/CkanModule.cs @@ -566,10 +566,21 @@ public bool MetadataEquals(CkanModule other) } } } + if (install_size != other.install_size) { return false; } + if (download_hash?.sha256 != null && other.download_hash?.sha256 != null + && download_hash.sha256 != other.download_hash.sha256) + { + return false; + } + if (download_hash?.sha1 != null && other.download_hash?.sha1 != null + && download_hash.sha1 != other.download_hash.sha1) + { + return false; + } if (!RelationshipsAreEquivalent(conflicts, other.conflicts)) { From f4754937e42c0aa705369d4ae1e6f14bdc9af77f Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Thu, 5 Sep 2024 16:11:49 -0500 Subject: [PATCH 4/5] Cmdline export fixes and modpack format --- Cmdline/Action/List.cs | 28 ++-- Core/Exporters/BbCodeExporter.cs | 2 +- Core/Exporters/CkanExporter.cs | 17 +++ .../DelimeterSeparatedValueExporter.cs | 134 ------------------ .../DelimiterSeparatedValueExporter.cs | 123 ++++++++++++++++ Core/Exporters/Exporter.cs | 36 ++--- Core/Exporters/IExporter.cs | 2 +- Core/Exporters/MarkdownExporter.cs | 2 +- Core/Exporters/PlainTextExporter.cs | 2 +- GUI/Main/MainExport.cs | 5 +- 10 files changed, 175 insertions(+), 176 deletions(-) create mode 100644 Core/Exporters/CkanExporter.cs delete mode 100644 Core/Exporters/DelimeterSeparatedValueExporter.cs create mode 100644 Core/Exporters/DelimiterSeparatedValueExporter.cs diff --git a/Cmdline/Action/List.cs b/Cmdline/Action/List.cs index 9dbe1d76e..3100470ab 100644 --- a/Cmdline/Action/List.cs +++ b/Cmdline/Action/List.cs @@ -147,9 +147,10 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) } else { - var stream = Console.OpenStandardOutput(); - new Exporter(exportFileType.Value).Export(registry, stream); - stream.Flush(); + using (var stream = Console.OpenStandardOutput()) + { + new Exporter(exportFileType.Value).Export(regMgr, registry, stream); + } } if (!(options.porcelain) && exportFileType == null) @@ -163,17 +164,16 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) } private static ExportFileType? GetExportFileType(string? export) - { - switch (export?.ToLowerInvariant()) + => export?.ToLowerInvariant() switch { - case "text": return ExportFileType.PlainText; - case "markdown": return ExportFileType.Markdown; - case "bbcode": return ExportFileType.BbCode; - case "csv": return ExportFileType.Csv; - case "tsv": return ExportFileType.Tsv; - default: return null; - } - } + "ckan" => ExportFileType.Ckan, + "text" => ExportFileType.PlainText, + "markdown" => ExportFileType.Markdown, + "bbcode" => ExportFileType.BbCode, + "csv" => ExportFileType.Csv, + "tsv" => ExportFileType.Tsv, + _ => null, + }; private readonly RepositoryDataManager repoData; private readonly IUser user; @@ -186,7 +186,7 @@ internal class ListOptions : InstanceSpecificOptions [Option("porcelain", HelpText = "Dump raw list of modules, good for shell scripting")] public bool porcelain { get; set; } - [Option("export", HelpText = "Export list of modules in specified format to stdout")] + [Option("export", HelpText = "Format of module list: ckan, text, markdown, bbcode, csv, tsv")] public string? export { get; set; } } diff --git a/Core/Exporters/BbCodeExporter.cs b/Core/Exporters/BbCodeExporter.cs index 8f1cac215..1d0f2f047 100644 --- a/Core/Exporters/BbCodeExporter.cs +++ b/Core/Exporters/BbCodeExporter.cs @@ -5,7 +5,7 @@ namespace CKAN.Exporters { public sealed class BbCodeExporter : IExporter { - public void Export(IRegistryQuerier registry, Stream stream) + public void Export(RegistryManager manager, IRegistryQuerier registry, Stream stream) { using (var writer = new StreamWriter(stream)) { diff --git a/Core/Exporters/CkanExporter.cs b/Core/Exporters/CkanExporter.cs new file mode 100644 index 000000000..f8e10c34e --- /dev/null +++ b/Core/Exporters/CkanExporter.cs @@ -0,0 +1,17 @@ +using System.IO; + +namespace CKAN.Exporters +{ + public sealed class CkanExporter : IExporter + { + public void Export(RegistryManager manager, + IRegistryQuerier registry, + Stream stream) + { + using (var writer = new StreamWriter(stream)) + { + writer.Write(CkanModule.ToJson(manager.GenerateModpack(false, false))); + } + } + } +} diff --git a/Core/Exporters/DelimeterSeparatedValueExporter.cs b/Core/Exporters/DelimeterSeparatedValueExporter.cs deleted file mode 100644 index c6c3d462d..000000000 --- a/Core/Exporters/DelimeterSeparatedValueExporter.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using System.IO; -using System.Linq; - -namespace CKAN.Exporters -{ - public sealed class DelimeterSeparatedValueExporter : IExporter - { - private const string WritePattern = "{1}{0}{2}{0}{3}{0}{4}{0}{5}" + - "{0}{6}{0}{7}{0}{8}{0}{9}{0}{10}" + - "{0}{11}{0}{12}{0}{13}{0}{14}{0}{15}" + - "{0}{16}{0}{17}{0}{18}"; - private readonly string _delimeter; - - public DelimeterSeparatedValueExporter(Delimeter delimeter) - { - switch (delimeter) - { - case Delimeter.Comma: - _delimeter = ","; - break; - case Delimeter.Tab: - _delimeter = "\t"; - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - - public void Export(IRegistryQuerier registry, Stream stream) - { - using (var writer = new StreamWriter(stream)) - { - writer.WriteLine(WritePattern, - _delimeter, - "identifier", - "version", - "name", - "abstract", - "description", - "author", - "kind", - "download", - "download_size", - "ksp_version", - "ksp_version_min", - "ksp_version_max", - "license", - "release_status", - "repository", - "homepage", - "bugtracker", - "discussions", - "spacedock", - "curse"); - - foreach (var mod in registry.InstalledModules.OrderBy(i => i.Module.name)) - { - writer.WriteLine(WritePattern, - _delimeter, - mod.Module.identifier, - mod.Module.version, - QuoteIfNecessary(mod.Module.name), - QuoteIfNecessary(mod.Module.@abstract), - QuoteIfNecessary(mod.Module.description), - QuoteIfNecessary(mod.Module.author == null ? "" : string.Join(";", mod.Module.author)), - QuoteIfNecessary(mod.Module.kind), - WriteUri(mod.Module.download?[0]), - mod.Module.download_size, - mod.Module.ksp_version, - mod.Module.ksp_version_min, - mod.Module.ksp_version_max, - QuoteIfNecessary(string.Join(";",mod.Module.license)), - mod.Module.release_status, - WriteRepository(mod.Module.resources), - WriteHomepage(mod.Module.resources), - WriteBugtracker(mod.Module.resources), - WriteDiscussions(mod.Module.resources), - WriteSpaceDock(mod.Module.resources), - WriteCurse(mod.Module.resources)); - } - } - } - - private string WriteUri(Uri? uri) - => uri != null - ? QuoteIfNecessary(uri.ToString()) - : string.Empty; - - private string WriteRepository(ResourcesDescriptor? resources) - => resources != null && resources.repository != null - ? QuoteIfNecessary(resources.repository.ToString()) - : string.Empty; - - private string WriteHomepage(ResourcesDescriptor? resources) - => resources != null && resources.homepage != null - ? QuoteIfNecessary(resources.homepage.ToString()) - : string.Empty; - - private string WriteBugtracker(ResourcesDescriptor? resources) - => resources != null && resources.bugtracker != null - ? QuoteIfNecessary(resources.bugtracker.ToString()) - : string.Empty; - - private string WriteDiscussions(ResourcesDescriptor? resources) - => resources != null && resources.discussions != null - ? QuoteIfNecessary(resources.discussions.ToString()) - : string.Empty; - - private string WriteSpaceDock(ResourcesDescriptor? resources) - => resources != null && resources.spacedock != null - ? QuoteIfNecessary(resources.spacedock.ToString()) - : string.Empty; - - private string WriteCurse(ResourcesDescriptor? resources) - => resources != null && resources.curse != null - ? QuoteIfNecessary(resources.curse.ToString()) - : string.Empty; - - private string QuoteIfNecessary(string? value) - => value == null - ? "" - : value.IndexOf(_delimeter, StringComparison.Ordinal) >= 0 - ? "\"" + value + "\"" - : value; - - public enum Delimeter - { - Comma, - Tab - } - - } -} diff --git a/Core/Exporters/DelimiterSeparatedValueExporter.cs b/Core/Exporters/DelimiterSeparatedValueExporter.cs new file mode 100644 index 000000000..70443da73 --- /dev/null +++ b/Core/Exporters/DelimiterSeparatedValueExporter.cs @@ -0,0 +1,123 @@ +using System; +using System.IO; +using System.Linq; + +namespace CKAN.Exporters +{ + public sealed class DelimiterSeparatedValueExporter : IExporter + { + public DelimiterSeparatedValueExporter(Delimiter delimiter) + { + _delimiter = delimiter switch + { + Delimiter.Comma => ",", + Delimiter.Tab => "\t", + _ => throw new ArgumentOutOfRangeException(nameof(delimiter), + delimiter.ToString()), + }; + } + + public void Export(RegistryManager manager, IRegistryQuerier registry, Stream stream) + { + using (var writer = new StreamWriter(stream)) + { + writer.WriteLine(string.Join(_delimiter, + "identifier", + "version", + "name", + "abstract", + "description", + "author", + "kind", + "download", + "download_size", + "ksp_version", + "ksp_version_min", + "ksp_version_max", + "license", + "release_status", + "repository", + "homepage", + "bugtracker", + "discussions", + "spacedock", + "curse")); + + foreach (var mod in registry.InstalledModules.OrderBy(i => i.Module.name)) + { + writer.WriteLine(string.Join(_delimiter, + mod.Module.identifier, + mod.Module.version, + QuoteIfNecessary(mod.Module.name), + QuoteIfNecessary(mod.Module.@abstract), + QuoteIfNecessary(mod.Module.description), + QuoteIfNecessary(mod.Module.author == null ? "" : string.Join(";", mod.Module.author)), + QuoteIfNecessary(mod.Module.kind), + WriteUri(mod.Module.download?[0]), + mod.Module.download_size, + mod.Module.ksp_version, + mod.Module.ksp_version_min, + mod.Module.ksp_version_max, + QuoteIfNecessary(string.Join(";",mod.Module.license)), + mod.Module.release_status, + WriteRepository(mod.Module.resources), + WriteHomepage(mod.Module.resources), + WriteBugtracker(mod.Module.resources), + WriteDiscussions(mod.Module.resources), + WriteSpaceDock(mod.Module.resources), + WriteCurse(mod.Module.resources))); + } + } + } + + private string WriteUri(Uri? uri) + => uri != null + ? QuoteIfNecessary(uri.ToString()) + : string.Empty; + + private string WriteRepository(ResourcesDescriptor? resources) + => resources != null && resources.repository != null + ? QuoteIfNecessary(resources.repository.ToString()) + : string.Empty; + + private string WriteHomepage(ResourcesDescriptor? resources) + => resources != null && resources.homepage != null + ? QuoteIfNecessary(resources.homepage.ToString()) + : string.Empty; + + private string WriteBugtracker(ResourcesDescriptor? resources) + => resources != null && resources.bugtracker != null + ? QuoteIfNecessary(resources.bugtracker.ToString()) + : string.Empty; + + private string WriteDiscussions(ResourcesDescriptor? resources) + => resources != null && resources.discussions != null + ? QuoteIfNecessary(resources.discussions.ToString()) + : string.Empty; + + private string WriteSpaceDock(ResourcesDescriptor? resources) + => resources != null && resources.spacedock != null + ? QuoteIfNecessary(resources.spacedock.ToString()) + : string.Empty; + + private string WriteCurse(ResourcesDescriptor? resources) + => resources != null && resources.curse != null + ? QuoteIfNecessary(resources.curse.ToString()) + : string.Empty; + + private string QuoteIfNecessary(string? value) + => value == null + ? "" + : value.IndexOf(_delimiter, StringComparison.Ordinal) >= 0 + ? "\"" + value + "\"" + : value; + + public enum Delimiter + { + Comma, + Tab + } + + private readonly string _delimiter; + } +} diff --git a/Core/Exporters/Exporter.cs b/Core/Exporters/Exporter.cs index cc98c2471..80531e812 100644 --- a/Core/Exporters/Exporter.cs +++ b/Core/Exporters/Exporter.cs @@ -1,5 +1,6 @@ using System; using System.IO; + using CKAN.Types; namespace CKAN.Exporters @@ -18,31 +19,24 @@ public sealed class Exporter : IExporter public Exporter(ExportFileType exportFileType) { - switch (exportFileType) + _exporter = exportFileType switch { - case ExportFileType.PlainText: - _exporter = new PlainTextExporter(); - break; - case ExportFileType.Markdown: - _exporter = new MarkdownExporter(); - break; - case ExportFileType.BbCode: - _exporter = new BbCodeExporter(); - break; - case ExportFileType.Csv: - _exporter = new DelimeterSeparatedValueExporter(DelimeterSeparatedValueExporter.Delimeter.Comma); - break; - case ExportFileType.Tsv: - _exporter = new DelimeterSeparatedValueExporter(DelimeterSeparatedValueExporter.Delimeter.Tab); - break; - default: - throw new ArgumentOutOfRangeException(); - } + ExportFileType.Ckan => new CkanExporter(), + ExportFileType.PlainText => new PlainTextExporter(), + ExportFileType.Markdown => new MarkdownExporter(), + ExportFileType.BbCode => new BbCodeExporter(), + ExportFileType.Csv => new DelimiterSeparatedValueExporter(DelimiterSeparatedValueExporter.Delimiter.Comma), + ExportFileType.Tsv => new DelimiterSeparatedValueExporter(DelimiterSeparatedValueExporter.Delimiter.Tab), + _ => throw new ArgumentOutOfRangeException(nameof(exportFileType), + exportFileType.ToString()), + }; } - public void Export(IRegistryQuerier registry, Stream stream) + public void Export(RegistryManager manager, + IRegistryQuerier registry, + Stream stream) { - _exporter.Export(registry, stream); + _exporter.Export(manager, registry, stream); } } } diff --git a/Core/Exporters/IExporter.cs b/Core/Exporters/IExporter.cs index 7e95a1ff5..19dcda9b0 100644 --- a/Core/Exporters/IExporter.cs +++ b/Core/Exporters/IExporter.cs @@ -12,6 +12,6 @@ public interface IExporter /// /// The registry of mods to be exported. /// The output stream to be written to. - void Export(IRegistryQuerier registry, Stream stream); + void Export(RegistryManager manager, IRegistryQuerier registry, Stream stream); } } diff --git a/Core/Exporters/MarkdownExporter.cs b/Core/Exporters/MarkdownExporter.cs index 54f60c87d..c3a67919c 100644 --- a/Core/Exporters/MarkdownExporter.cs +++ b/Core/Exporters/MarkdownExporter.cs @@ -5,7 +5,7 @@ namespace CKAN.Exporters { public sealed class MarkdownExporter : IExporter { - public void Export(IRegistryQuerier registry, Stream stream) + public void Export(RegistryManager manager, IRegistryQuerier registry, Stream stream) { using (var writer = new StreamWriter(stream)) { diff --git a/Core/Exporters/PlainTextExporter.cs b/Core/Exporters/PlainTextExporter.cs index 53abab49c..7b52b39a5 100644 --- a/Core/Exporters/PlainTextExporter.cs +++ b/Core/Exporters/PlainTextExporter.cs @@ -6,7 +6,7 @@ namespace CKAN.Exporters public sealed class PlainTextExporter : IExporter { - public void Export(IRegistryQuerier registry, Stream stream) + public void Export(RegistryManager manager, IRegistryQuerier registry, Stream stream) { using (var writer = new StreamWriter(stream)) { diff --git a/GUI/Main/MainExport.cs b/GUI/Main/MainExport.cs index ecbd8fdb0..4f22f3313 100644 --- a/GUI/Main/MainExport.cs +++ b/GUI/Main/MainExport.cs @@ -85,9 +85,8 @@ private void exportModListToolStripMenuItem_Click(object? sender, EventArgs? e) var fileMode = File.Exists(dlg.FileName) ? FileMode.Truncate : FileMode.CreateNew; using (var stream = new FileStream(dlg.FileName, fileMode)) { - new Exporter(specialExportOptions[dlg.FilterIndex - 1].ExportFileType).Export( - RegistryManager.Instance(CurrentInstance, repoData).registry, - stream); + var regMgr = RegistryManager.Instance(CurrentInstance, repoData); + new Exporter(specialExportOptions[dlg.FilterIndex - 1].ExportFileType).Export(regMgr, regMgr.registry, stream); } } } From 107303ce718c40e7b80c390900f3a2e74899757c Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Thu, 5 Sep 2024 16:23:22 -0500 Subject: [PATCH 5/5] Always use the same SourceForge mirror --- Core/Net/Net.cs | 50 +++++++++++-------- Netkan/Transformers/SourceForgeTransformer.cs | 33 ++++++++++-- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/Core/Net/Net.cs b/Core/Net/Net.cs index 113cc2000..a429ff666 100644 --- a/Core/Net/Net.cs +++ b/Core/Net/Net.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Collections.Generic; using System.Linq; using System.Net; @@ -9,6 +10,7 @@ using ChinhDo.Transactions.FileManager; using log4net; +using CKAN.Extensions; using CKAN.Configuration; namespace CKAN @@ -198,33 +200,37 @@ public static string Download(string url, out string? etag, string? filename = n } public static Uri? ResolveRedirect(Uri url, - string? userAgent = "") + string? userAgent = null, + int maxRedirects = 6) { - const int maxRedirects = 6; - for (int redirects = 0; redirects <= maxRedirects; ++redirects) + var urls = url.TraverseNodes(u => new RedirectWebClient(userAgent) is RedirectWebClient rwClient + && rwClient.OpenRead(u) is Stream s && DisposeStream(s) + && rwClient.ResponseHeaders is WebHeaderCollection headers + && headers["Location"] is string location + ? Uri.IsWellFormedUriString(location, UriKind.Absolute) + ? new Uri(location) + : Uri.IsWellFormedUriString(location, UriKind.Relative) + ? new Uri(u, location) + : throw new Kraken(string.Format(Properties.Resources.NetInvalidLocation, + location)) + : null) + // The first element is the input, so e.g. if we want two redirects, that's three elements + .Take(maxRedirects + 1) + .ToArray(); + if (log.IsDebugEnabled) { - var rwClient = new RedirectWebClient(userAgent); - using (rwClient.OpenRead(url)) { } - var location = rwClient.ResponseHeaders?["Location"]; - if (location == null) + foreach ((Uri from, Uri to) in urls.Zip(urls.Skip(1))) { - return url; - } - else if (Uri.IsWellFormedUriString(location, UriKind.Absolute)) - { - url = new Uri(location); - } - else if (Uri.IsWellFormedUriString(location, UriKind.Relative)) - { - url = new Uri(url, location); - log.DebugFormat("Relative URL {0} is absolute URL {1}", location, url); - } - else - { - throw new Kraken(string.Format(Properties.Resources.NetInvalidLocation, location)); + log.DebugFormat("Redirected {0} to {1}", from, to); } } - return null; + return urls.LastOrDefault(); + } + + private static bool DisposeStream(Stream s) + { + s.Dispose(); + return true; } /// diff --git a/Netkan/Transformers/SourceForgeTransformer.cs b/Netkan/Transformers/SourceForgeTransformer.cs index 6b76c70f8..ad5b6f8e4 100644 --- a/Netkan/Transformers/SourceForgeTransformer.cs +++ b/Netkan/Transformers/SourceForgeTransformer.cs @@ -1,6 +1,8 @@ using System; using System.Linq; +using System.Web; using System.Collections.Generic; +using System.Collections.Specialized; using Newtonsoft.Json.Linq; using log4net; @@ -74,8 +76,18 @@ private static Metadata TransformOne(JObject json, { "bugtracker", mod.BugTrackerLink }, })); // SourceForge doesn't send redirects to user agents it considers browser-like - json.SafeAdd("download", Net.ResolveRedirect(version.Link, "Wget") - ?.OriginalString); + if (Net.ResolveRedirect(version.Link, "Wget", 1) is Uri firstRedir) + { + // SourceForge redirects to different mirrors for load-balancing + // (IF it considers your user agent string a non-browser, which excludes the CKAN client), + // but for us that means CKAN users constantly shifting from one server + // to another in unison as the bot changes the URL in the metadata. + // https://sourceforge.net/p/forge/documentation/Mirrors/ + // Tweak the intermediate redirect URL to use the same mirror every time. + json.SafeAdd("download", Net.ResolveRedirect(SetQueryKey(firstRedir, "use_mirror", mirror), + "Wget", 1) + ?.OriginalString); + } json.SafeAdd(Metadata.UpdatedPropertyName, version.Timestamp); json.Remove("$kref"); @@ -84,7 +96,22 @@ private static Metadata TransformOne(JObject json, return new Metadata(json); } + private static Uri SetQueryKey(Uri url, string key, string value) + { + if (HttpUtility.ParseQueryString(url.Query) is NameValueCollection newQuery) + { + newQuery.Set(key, value); + return new UriBuilder(url) + { + Query = newQuery.ToString(), + }.Uri; + + } + return url; + } + private readonly ISourceForgeApi api; - private static readonly ILog log = LogManager.GetLogger(typeof(GitlabTransformer)); + private const string mirror = "psychz"; // Brooklyn, United States + private static readonly ILog log = LogManager.GetLogger(typeof(GitlabTransformer)); } }