Initial Argus web catalog
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Argus.Infrastructure;
|
||||
|
||||
public static class IdentifierUtility
|
||||
{
|
||||
public static string Normalize(string? value, string fallback)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
var builder = new StringBuilder(value.Length);
|
||||
var lastWasSeparator = false;
|
||||
|
||||
foreach (var character in value.Trim().ToLowerInvariant())
|
||||
{
|
||||
if (char.IsLetterOrDigit(character))
|
||||
{
|
||||
builder.Append(character);
|
||||
lastWasSeparator = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character is '-' or '_' or '.')
|
||||
{
|
||||
if (!lastWasSeparator)
|
||||
{
|
||||
builder.Append(character);
|
||||
lastWasSeparator = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var normalized = builder.ToString().Trim('-', '_', '.');
|
||||
return string.IsNullOrWhiteSpace(normalized) ? fallback : normalized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Argus.Infrastructure;
|
||||
|
||||
public sealed class PackageStorageService(StoragePaths storagePaths)
|
||||
{
|
||||
public string ResolvePhysicalPath(string storedRelativePath)
|
||||
{
|
||||
var normalizedRelativePath = storedRelativePath.Replace('/', Path.DirectorySeparatorChar);
|
||||
return Path.Combine(storagePaths.PackagesRootPath, normalizedRelativePath);
|
||||
}
|
||||
|
||||
public void TryDelete(string storedRelativePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var physicalPath = ResolvePhysicalPath(storedRelativePath);
|
||||
if (File.Exists(physicalPath))
|
||||
{
|
||||
File.Delete(physicalPath);
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
namespace Argus.Infrastructure;
|
||||
|
||||
public static class SemanticVersionUtility
|
||||
{
|
||||
public static int Compare(string? left, string? right)
|
||||
{
|
||||
var leftParsed = SemanticVersion.TryParse(left, out var leftVersion);
|
||||
var rightParsed = SemanticVersion.TryParse(right, out var rightVersion);
|
||||
|
||||
if (leftParsed && rightParsed)
|
||||
{
|
||||
return leftVersion.CompareTo(rightVersion);
|
||||
}
|
||||
|
||||
if (leftParsed)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (rightParsed)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return string.Compare(left, right, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private sealed class SemanticVersion : IComparable<SemanticVersion>
|
||||
{
|
||||
private readonly IReadOnlyList<Identifier> _preReleaseIdentifiers;
|
||||
|
||||
private SemanticVersion(int major, int minor, int patch, IReadOnlyList<Identifier> preReleaseIdentifiers)
|
||||
{
|
||||
Major = major;
|
||||
Minor = minor;
|
||||
Patch = patch;
|
||||
_preReleaseIdentifiers = preReleaseIdentifiers;
|
||||
}
|
||||
|
||||
public int Major { get; }
|
||||
|
||||
public int Minor { get; }
|
||||
|
||||
public int Patch { get; }
|
||||
|
||||
public bool IsPreRelease => _preReleaseIdentifiers.Count > 0;
|
||||
|
||||
public static bool TryParse(string? value, out SemanticVersion version)
|
||||
{
|
||||
version = null!;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var trimmed = value.Trim();
|
||||
if (trimmed.Length > 1 && (trimmed[0] == 'v' || trimmed[0] == 'V') && char.IsDigit(trimmed[1]))
|
||||
{
|
||||
trimmed = trimmed[1..];
|
||||
}
|
||||
|
||||
var buildSeparatorIndex = trimmed.IndexOf('+');
|
||||
if (buildSeparatorIndex >= 0)
|
||||
{
|
||||
trimmed = trimmed[..buildSeparatorIndex];
|
||||
}
|
||||
|
||||
string corePart;
|
||||
string? preReleasePart = null;
|
||||
var preReleaseSeparatorIndex = trimmed.IndexOf('-');
|
||||
if (preReleaseSeparatorIndex >= 0)
|
||||
{
|
||||
corePart = trimmed[..preReleaseSeparatorIndex];
|
||||
preReleasePart = trimmed[(preReleaseSeparatorIndex + 1)..];
|
||||
}
|
||||
else
|
||||
{
|
||||
corePart = trimmed;
|
||||
}
|
||||
|
||||
var coreSegments = corePart.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (coreSegments.Length is < 1 or > 3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryParseNumeric(coreSegments[0], out var major))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var minor = 0;
|
||||
if (coreSegments.Length >= 2 && !TryParseNumeric(coreSegments[1], out minor))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var patch = 0;
|
||||
if (coreSegments.Length == 3 && !TryParseNumeric(coreSegments[2], out patch))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var preReleaseIdentifiers = new List<Identifier>();
|
||||
if (!string.IsNullOrWhiteSpace(preReleasePart))
|
||||
{
|
||||
foreach (var segment in preReleasePart.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
if (!Identifier.TryParse(segment, out var identifier))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
preReleaseIdentifiers.Add(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
version = new SemanticVersion(major, minor, patch, preReleaseIdentifiers);
|
||||
return true;
|
||||
}
|
||||
|
||||
public int CompareTo(SemanticVersion? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var majorComparison = Major.CompareTo(other.Major);
|
||||
if (majorComparison != 0)
|
||||
{
|
||||
return majorComparison;
|
||||
}
|
||||
|
||||
var minorComparison = Minor.CompareTo(other.Minor);
|
||||
if (minorComparison != 0)
|
||||
{
|
||||
return minorComparison;
|
||||
}
|
||||
|
||||
var patchComparison = Patch.CompareTo(other.Patch);
|
||||
if (patchComparison != 0)
|
||||
{
|
||||
return patchComparison;
|
||||
}
|
||||
|
||||
if (!IsPreRelease && !other.IsPreRelease)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!IsPreRelease)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!other.IsPreRelease)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var count = Math.Max(_preReleaseIdentifiers.Count, other._preReleaseIdentifiers.Count);
|
||||
for (var index = 0; index < count; index++)
|
||||
{
|
||||
if (index >= _preReleaseIdentifiers.Count)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (index >= other._preReleaseIdentifiers.Count)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var identifierComparison = _preReleaseIdentifiers[index].CompareTo(other._preReleaseIdentifiers[index]);
|
||||
if (identifierComparison != 0)
|
||||
{
|
||||
return identifierComparison;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static bool TryParseNumeric(string value, out int parsed) =>
|
||||
int.TryParse(value, out parsed) && parsed >= 0;
|
||||
|
||||
private readonly record struct Identifier(bool IsNumeric, long NumericValue, string TextValue) : IComparable<Identifier>
|
||||
{
|
||||
public static bool TryParse(string value, out Identifier identifier)
|
||||
{
|
||||
identifier = default;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var trimmed = value.Trim();
|
||||
foreach (var character in trimmed)
|
||||
{
|
||||
if (!char.IsLetterOrDigit(character) && character != '-')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (long.TryParse(trimmed, out var numericValue))
|
||||
{
|
||||
identifier = new Identifier(true, numericValue, trimmed);
|
||||
return true;
|
||||
}
|
||||
|
||||
identifier = new Identifier(false, 0, trimmed);
|
||||
return true;
|
||||
}
|
||||
|
||||
public int CompareTo(Identifier other)
|
||||
{
|
||||
if (IsNumeric && other.IsNumeric)
|
||||
{
|
||||
return NumericValue.CompareTo(other.NumericValue);
|
||||
}
|
||||
|
||||
if (IsNumeric)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (other.IsNumeric)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return string.Compare(TextValue, other.TextValue, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Argus.Infrastructure;
|
||||
|
||||
public static class SlugUtility
|
||||
{
|
||||
public static string Normalize(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var builder = new StringBuilder(value.Length);
|
||||
var lastWasHyphen = false;
|
||||
|
||||
foreach (var character in value.Trim().ToLowerInvariant())
|
||||
{
|
||||
if (char.IsLetterOrDigit(character))
|
||||
{
|
||||
builder.Append(character);
|
||||
lastWasHyphen = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!lastWasHyphen)
|
||||
{
|
||||
builder.Append('-');
|
||||
lastWasHyphen = true;
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString().Trim('-');
|
||||
}
|
||||
|
||||
public static bool IsValid(string? value) =>
|
||||
!string.IsNullOrWhiteSpace(value) &&
|
||||
value.Length <= 100 &&
|
||||
string.Equals(value, Normalize(value), StringComparison.Ordinal);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Data;
|
||||
using Argus.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Argus.Infrastructure;
|
||||
|
||||
public static class SqliteSchemaMigrator
|
||||
{
|
||||
public static async Task EnsureCurrentSchemaAsync(
|
||||
ArgusDbContext dbContext,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await dbContext.Database.EnsureCreatedAsync(cancellationToken);
|
||||
|
||||
await ExecuteNonQueryAsync(
|
||||
dbContext,
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS "IX_Apps_IsListed_Name"
|
||||
ON "Apps" ("IsListed", "Name")
|
||||
""",
|
||||
cancellationToken);
|
||||
|
||||
await ExecuteNonQueryAsync(
|
||||
dbContext,
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS "IX_Releases_PublishedAt"
|
||||
ON "Releases" ("PublishedAt")
|
||||
""",
|
||||
cancellationToken);
|
||||
|
||||
await ExecuteNonQueryAsync(
|
||||
dbContext,
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS "IX_Releases_CatalogAppId_PublishedAt"
|
||||
ON "Releases" ("CatalogAppId", "PublishedAt")
|
||||
""",
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task ExecuteNonQueryAsync(
|
||||
ArgusDbContext dbContext,
|
||||
string commandText,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var connection = dbContext.Database.GetDbConnection();
|
||||
var mustClose = connection.State != ConnectionState.Open;
|
||||
|
||||
if (mustClose)
|
||||
{
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = commandText;
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (mustClose)
|
||||
{
|
||||
await connection.CloseAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Argus.Infrastructure;
|
||||
|
||||
public sealed class StoragePaths(IHostEnvironment hostEnvironment)
|
||||
{
|
||||
public string DataRootPath => Path.Combine(hostEnvironment.ContentRootPath, "Data");
|
||||
|
||||
public string PackagesRootPath => Path.Combine(DataRootPath, "Packages");
|
||||
|
||||
public void EnsureCreated()
|
||||
{
|
||||
Directory.CreateDirectory(DataRootPath);
|
||||
Directory.CreateDirectory(PackagesRootPath);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user