using System.Text.RegularExpressions; using ForgejoReleaseSync.DTOs; using ForgejoReleaseSync.DTOs.Forgejo; using Microsoft.Extensions.Logging; namespace ForgejoReleaseSync.Services; public class SyncService( RepositoryConfig repositoryConfig, GithubService githubService, ForgejoService forgejoService, ReleaseAssetDownloadService releaseAssetDownloadService, ILogger logger) { public async Task Sync(CancellationToken token = default) { logger.LogInformation("Starting sync for {Count} repositories", repositoryConfig.Repositories.Count); foreach (var repo in repositoryConfig.Repositories) { try { await SyncRepositoryAsync(repo, token); } catch (Exception ex) { logger.LogError(ex, "Failed to sync repository {Name}", repo.Name); } } logger.LogInformation("Sync finished"); } private async Task SyncRepositoryAsync(RepoSyncConfig repo, CancellationToken token) { var (owner, name) = ParseGithubUrl(repo.GithubUrl); var githubReleases = await githubService.FetchRepoReleasesAsync(owner, name, token); var githubRelease = githubReleases.FirstOrDefault(r => !r.Prerelease); if (githubRelease is null) { logger.LogWarning("{Owner}/{Name}: no GitHub release found, skipping", owner, name); return; } var forgejoReleases = await forgejoService.FetchRepoReleasesAsync(owner, name, token); var forgejoRelease = forgejoReleases.FirstOrDefault(r => !r.Prerelease); var releaseId = await ResolveReleaseIdAsync(owner, name, githubRelease, forgejoRelease, token); var includeRegex = new Regex(repo.IncludeAssetsRegex); var excludeRegex = new Regex(repo.ExcludeAssetsRegex); var existingAssets = forgejoRelease?.Assets.Select(a => a.Name).ToHashSet(StringComparer.Ordinal) ?? []; var assetsToSync = githubRelease.Assets .Where(a => includeRegex.IsMatch(a.Name)) .Where(a => !excludeRegex.IsMatch(a.Name)) .Where(a => !existingAssets.Contains(a.Name)) .ToArray(); logger.LogInformation( "{Owner}/{Name}: release {Tag} has {Count} asset(s) to sync", owner, name, githubRelease.TagName, assetsToSync.Length); foreach (var asset in assetsToSync) { await SyncAssetAsync(owner, name, releaseId, asset, token); } } private async Task ResolveReleaseIdAsync( string owner, string name, GithubReleaseResponseDto githubRelease, ForgejoReleaseResponseDto? forgejoRelease, CancellationToken token) { if (forgejoRelease is not null && forgejoRelease.TagName == githubRelease.TagName) return forgejoRelease.Id; logger.LogInformation( "{Owner}/{Name}: creating Forgejo release {Tag}", owner, name, githubRelease.TagName); var created = await forgejoService.CreateReleaseAsync( owner, name, githubRelease.Body, githubRelease.Name, githubRelease.TagName, token); return created.Id; } private async Task SyncAssetAsync( string owner, string name, long releaseId, Asset asset, CancellationToken token) { logger.LogInformation("{Owner}/{Name}: downloading {Asset}", owner, name, asset.Name); var path = await releaseAssetDownloadService.DownloadReleaseAssetAsync(asset.BrowserDownloadUrl, token); try { logger.LogInformation("{Owner}/{Name}: uploading {Asset}", owner, name, asset.Name); await forgejoService.UploadReleaseAssetAsync(owner, name, releaseId, path, token); } finally { await releaseAssetDownloadService.DeleteReleaseAssetAsync(path); } } private static (string Owner, string Name) ParseGithubUrl(string url) { var parts = new Uri(url).AbsolutePath.Trim('/').Split('/'); return (parts[0], parts[1]); } }