Fix release pruning and remove QKeeper reference

This commit is contained in:
Курнат Андрей
2026-07-10 15:32:48 +03:00
parent 5f80c4bf6a
commit 9b3dcbc9b9
2 changed files with 114 additions and 15 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ Do not put SSH passwords, private keys, or local `.env` files into application r
Every app must choose stable values:
- `slug`: public Argus id. Use lowercase letters, digits, and hyphens only, for example `keeper-android` or `my-desktop-app`.
- `slug`: public Argus id. Use lowercase letters, digits, and hyphens only, for example `sample-android` or `my-desktop-app`.
- `name`: display name.
- `summary`: short catalog text.
- `description`: full catalog text.
+113 -14
View File
@@ -1,3 +1,5 @@
using System.Data;
using System.Globalization;
using System.Net;
using Argus.Contracts;
using Argus.Data;
@@ -331,11 +333,11 @@ static IOrderedEnumerable<AppRelease> OrderReleases(IEnumerable<AppRelease> rele
.ThenByDescending(x => x.PublishedAt)
.ThenByDescending(x => x.Id);
static void DeleteStoredPackages(PackageStorageService storageService, IEnumerable<AppRelease> releases)
static void DeleteStoredPackages(PackageStorageService storageService, IEnumerable<string> storedRelativePaths)
{
foreach (var release in releases)
foreach (var storedRelativePath in storedRelativePaths)
{
storageService.TryDelete(release.StoredRelativePath);
storageService.TryDelete(storedRelativePath);
}
}
@@ -351,15 +353,9 @@ static async Task PruneOldReleasesAsync(
throw new ArgumentOutOfRangeException(nameof(maxRetainedReleasesPerApp));
}
IQueryable<AppRelease> releasesQuery = dbContext.Releases;
if (catalogAppId is { } appId)
{
releasesQuery = releasesQuery.Where(x => x.CatalogAppId == appId);
}
var releases = await releasesQuery.ToListAsync(cancellationToken);
var releases = await LoadReleasePruneCandidatesAsync(dbContext, catalogAppId, cancellationToken);
var releasesToPrune = releases
.GroupBy(x => x.CatalogAppId)
.GroupBy(x => x.CatalogAppId, StringComparer.OrdinalIgnoreCase)
.SelectMany(group => group
.OrderByDescending(x => x.PublishedAt)
.ThenByDescending(x => x.Id)
@@ -371,9 +367,105 @@ static async Task PruneOldReleasesAsync(
return;
}
dbContext.Releases.RemoveRange(releasesToPrune);
await dbContext.SaveChangesAsync(cancellationToken);
DeleteStoredPackages(storageService, releasesToPrune);
await DeleteReleaseRowsAsync(dbContext, releasesToPrune.Select(x => x.RowId), cancellationToken);
DeleteStoredPackages(storageService, releasesToPrune.Select(x => x.StoredRelativePath));
}
static async Task<IReadOnlyList<ReleasePruneCandidate>> LoadReleasePruneCandidatesAsync(
ArgusDbContext dbContext,
Guid? catalogAppId,
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 =
"""
SELECT rowid, "CatalogAppId", "StoredRelativePath", "PublishedAt", "Id"
FROM "Releases"
WHERE $catalogAppId IS NULL OR lower("CatalogAppId") = lower($catalogAppId)
""";
var catalogAppIdParameter = command.CreateParameter();
catalogAppIdParameter.ParameterName = "$catalogAppId";
catalogAppIdParameter.Value = catalogAppId?.ToString("D") ?? (object)DBNull.Value;
command.Parameters.Add(catalogAppIdParameter);
var candidates = new List<ReleasePruneCandidate>();
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
candidates.Add(new ReleasePruneCandidate(
reader.GetInt64(0),
reader.GetString(1),
reader.GetString(2),
DateTimeOffset.Parse(reader.GetString(3), CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind),
Guid.Parse(reader.GetString(4))));
}
return candidates;
}
finally
{
if (mustClose)
{
await connection.CloseAsync();
}
}
}
static async Task DeleteReleaseRowsAsync(
ArgusDbContext dbContext,
IEnumerable<long> rowIds,
CancellationToken cancellationToken)
{
var connection = dbContext.Database.GetDbConnection();
var mustClose = connection.State != ConnectionState.Open;
if (mustClose)
{
await connection.OpenAsync(cancellationToken);
}
await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
try
{
await using var command = connection.CreateCommand();
command.Transaction = transaction;
command.CommandText = """DELETE FROM "Releases" WHERE rowid = $rowid""";
var rowIdParameter = command.CreateParameter();
rowIdParameter.ParameterName = "$rowid";
command.Parameters.Add(rowIdParameter);
foreach (var rowId in rowIds)
{
rowIdParameter.Value = rowId;
await command.ExecuteNonQueryAsync(cancellationToken);
}
await transaction.CommitAsync(cancellationToken);
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
finally
{
if (mustClose)
{
await connection.CloseAsync();
}
}
}
static void SetPackageCacheHeaders(HttpContext httpContext, bool immutable)
@@ -388,3 +480,10 @@ static EntityTagHeaderValue CreatePackageEntityTag(string sha256) =>
static string? NormalizeFilter(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : IdentifierUtility.Normalize(value, string.Empty);
sealed record ReleasePruneCandidate(
long RowId,
string CatalogAppId,
string StoredRelativePath,
DateTimeOffset PublishedAt,
Guid Id);