Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement required interactions with beatmap mirrors #14

Merged
merged 4 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public BeatmapSubmissionControllerTest(IntegrationTestWebApplicationFactory<Prog
services.AddTransient<IBeatmapStorage>(_ => beatmapStorage);
services.AddTransient<BeatmapPackagePatcher>();
services.AddTransient<ILegacyIO>(_ => mockLegacyIO.Object);
services.AddTransient<IMirrorService, NoOpMirrorService>();
});
}).CreateClient();
}
Expand Down
10 changes: 9 additions & 1 deletion osu.Server.BeatmapSubmission/BeatmapSubmissionController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,18 @@ public class BeatmapSubmissionController : Controller
private readonly IBeatmapStorage beatmapStorage;
private readonly BeatmapPackagePatcher patcher;
private readonly ILegacyIO legacyIO;
private readonly IMirrorService mirrorService;

public BeatmapSubmissionController(IBeatmapStorage beatmapStorage, BeatmapPackagePatcher patcher, ILegacyIO legacyIO)
public BeatmapSubmissionController(
IBeatmapStorage beatmapStorage,
BeatmapPackagePatcher patcher,
ILegacyIO legacyIO,
IMirrorService mirrorService)
{
this.beatmapStorage = beatmapStorage;
this.patcher = patcher;
this.legacyIO = legacyIO;
this.mirrorService = mirrorService;
}

/// <summary>
Expand Down Expand Up @@ -403,6 +409,8 @@ private async Task<bool> updateBeatmapSetFromArchiveAsync(uint beatmapSetId, Str
if (await db.IsBeatmapSetNominatedAsync(beatmapSetId))
await legacyIO.DisqualifyBeatmapSetAsync(beatmapSetId, "This beatmap set was updated by the mapper after a nomination. Please ensure to re-check the beatmaps for new issues. If you are the mapper, please comment in this thread on what you changed.");

await mirrorService.PurgeBeatmapSetAsync(db, beatmapSetId);

if (!await db.IsBeatmapSetInProcessingQueueAsync(beatmapSetId))
{
await db.AddBeatmapSetToProcessingQueueAsync(beatmapSetId);
Expand Down
19 changes: 19 additions & 0 deletions osu.Server.BeatmapSubmission/DatabaseOperationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -397,5 +397,24 @@ public static async Task<IEnumerable<uint>> GetBeatmapOwnersAsync(this MySqlConn
},
transaction);
}

public static async Task<IEnumerable<osu_mirror>> GetMirrorsRequiringUpdateAsync(this MySqlConnection db, MySqlTransaction? transaction = null)
{
return await db.QueryAsync<osu_mirror>(
"SELECT * FROM `osu_mirrors` WHERE `version` > 1 AND `perform_updates` > 0",
transaction: transaction);
}

public static async Task MarkPendingPurgeAsync(this MySqlConnection db, osu_mirror mirror, uint beatmapSetId, MySqlTransaction? transaction = null)
{
await db.ExecuteAsync(
"UPDATE `osu_mirrors` SET `pending_purge` = CONCAT(`pending_purge`, ',', @beatmapset_id) WHERE `mirror_id` = @mirror_id",
new
{
beatmapset_id = beatmapSetId,
mirror_id = mirror.mirror_id
},
transaction);
}
}
}
16 changes: 16 additions & 0 deletions osu.Server.BeatmapSubmission/Models/Database/osu_mirror.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

// ReSharper disable InconsistentNaming

namespace osu.Server.BeatmapSubmission.Models.Database
{
public class osu_mirror
{
public ushort mirror_id { get; set; }
public string base_url { get; set; } = string.Empty;
public decimal version { get; set; }
public bool perform_updates { get; set; }
public string secret_key { get; set; } = string.Empty;
}
}
2 changes: 2 additions & 0 deletions osu.Server.BeatmapSubmission/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public static void Main(string[] args)
builder.Services.AddTransient<BeatmapPackagePatcher>();
builder.Services.AddHttpClient();
builder.Services.AddTransient<ILegacyIO, LegacyIO>();
builder.Services.AddTransient<IMirrorService, NoOpMirrorService>();
builder.Services.AddSwaggerGen(c =>
{
c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, Assembly.GetExecutingAssembly().GetName().Name + ".xml"));
Expand All @@ -57,6 +58,7 @@ public static void Main(string[] args)
builder.Services.AddTransient<BeatmapPackagePatcher>();
builder.Services.AddHttpClient();
builder.Services.AddTransient<ILegacyIO, LegacyIO>();
builder.Services.AddTransient<IMirrorService, MirrorService>();
break;
}
}
Expand Down
12 changes: 12 additions & 0 deletions osu.Server.BeatmapSubmission/Services/IMirrorService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using MySqlConnector;

namespace osu.Server.BeatmapSubmission.Services
{
public interface IMirrorService
{
Task PurgeBeatmapSetAsync(MySqlConnection db, uint beatmapSetId);
}
}
57 changes: 57 additions & 0 deletions osu.Server.BeatmapSubmission/Services/MirrorService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using MySqlConnector;
using osu.Framework.Extensions;
using osu.Server.BeatmapSubmission.Models.Database;

namespace osu.Server.BeatmapSubmission.Services
{
public class MirrorService : IMirrorService
{
private readonly HttpClient client;

public MirrorService(HttpClient client)
{
this.client = client;
}

public async Task PurgeBeatmapSetAsync(MySqlConnection db, uint beatmapSetId)
{
osu_mirror[] mirrors = (await db.GetMirrorsRequiringUpdateAsync()).ToArray();

foreach (var mirror in mirrors)
{
if (await performMirrorAction(mirror, "purge", new Dictionary<string, string> { ["s"] = beatmapSetId.ToString() }) != "1")
await db.MarkPendingPurgeAsync(mirror, beatmapSetId);
}
}

private async Task<string?> performMirrorAction(osu_mirror mirror, string action, Dictionary<string, string> data)
{
data["ts"] = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
data["action"] = action;
data["cs"] = ($"{data.GetValueOrDefault("s")}{data.GetValueOrDefault("fd")}{data.GetValueOrDefault("fs")}{data.GetValueOrDefault("ts")}"
+ $"{data.GetValueOrDefault("nv")}{data.GetValueOrDefault("action")}{mirror.secret_key}").ComputeMD5Hash();

var request = new HttpRequestMessage(HttpMethod.Post, mirror.base_url);
request.Content = new FormUrlEncodedContent(data);

try
{
var response = await client.SendAsync(request);
return await response.Content.ReadAsStringAsync();
}
catch (Exception)
{
// TODO: log error
return null;
Comment on lines +47 to +48
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would ask to look away for now, my next item on the list (one of the very few remaining, actually) is an extensive global pass on observability, reliability, and logging.

}
}
}

public class NoOpMirrorService : IMirrorService
{
public Task PurgeBeatmapSetAsync(MySqlConnection db, uint beatmapSetId) => Task.CompletedTask;
}
}
Loading