diff --git a/DTOs/Forgejo/ForgejoCreateReleaseDto.cs b/DTOs/Forgejo/ForgejoCreateReleaseDto.cs new file mode 100644 index 0000000..ef2fd7d --- /dev/null +++ b/DTOs/Forgejo/ForgejoCreateReleaseDto.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace ForgejoReleaseSync.DTOs.Forgejo; + +public sealed record ForgejoCreateReleaseDto( + [property: JsonPropertyName("body")] string Body, + [property: JsonPropertyName("draft")] bool Draft, + [property: JsonPropertyName("hide_archive_links")] bool HideArchiveLinks, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("prerelease")] bool Prerelease, + [property: JsonPropertyName("tag_name")] string TagName, + [property: JsonPropertyName("target_commitish")] string TargetCommitish +); \ No newline at end of file diff --git a/DTOs/Forgejo/ForgejoGetReleasesDto.cs b/DTOs/Forgejo/ForgejoGetReleasesDto.cs new file mode 100644 index 0000000..72521f0 --- /dev/null +++ b/DTOs/Forgejo/ForgejoGetReleasesDto.cs @@ -0,0 +1,101 @@ +using System.Text.Json.Serialization; + +namespace ForgejoReleaseSync.DTOs.Forgejo; + +public sealed record ForgejoReleaseResponseDto( + [property: JsonPropertyName("archive_download_count")] + ArchiveDownloadCount ArchiveDownloadCount, + + [property: JsonPropertyName("assets")] + ForgejoReleaseAsset[] Assets, + + [property: JsonPropertyName("author")] + ForgejoUser Author, + + [property: JsonPropertyName("body")] + string Body, + + [property: JsonPropertyName("created_at")] + DateTimeOffset CreatedAt, + + [property: JsonPropertyName("draft")] + bool Draft, + + [property: JsonPropertyName("hide_archive_links")] + bool HideArchiveLinks, + + [property: JsonPropertyName("html_url")] + string HtmlUrl, + + [property: JsonPropertyName("id")] + long Id, + + [property: JsonPropertyName("name")] + string Name, + + [property: JsonPropertyName("prerelease")] + bool Prerelease, + + [property: JsonPropertyName("published_at")] + DateTimeOffset PublishedAt, + + [property: JsonPropertyName("tag_name")] + string TagName, + + [property: JsonPropertyName("tarball_url")] + string TarballUrl, + + [property: JsonPropertyName("target_commitish")] + string TargetCommitish, + + [property: JsonPropertyName("upload_url")] + string UploadUrl, + + [property: JsonPropertyName("url")] + string Url, + + [property: JsonPropertyName("zipball_url")] + string ZipballUrl +); + +public sealed record ArchiveDownloadCount( + [property: JsonPropertyName("tar_gz")] long TarGz, + [property: JsonPropertyName("zip")] long Zip +); + +public sealed record ForgejoReleaseAsset( + [property: JsonPropertyName("browser_download_url")] string BrowserDownloadUrl, + [property: JsonPropertyName("created_at")] DateTimeOffset CreatedAt, + [property: JsonPropertyName("download_count")] long DownloadCount, + [property: JsonPropertyName("id")] long Id, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("size")] long Size, + [property: JsonPropertyName("type")] string Type, + [property: JsonPropertyName("uuid")] string Uuid +); + +public sealed record ForgejoUser( + [property: JsonPropertyName("active")] bool Active, + [property: JsonPropertyName("avatar_url")] string AvatarUrl, + [property: JsonPropertyName("created")] DateTimeOffset Created, + [property: JsonPropertyName("description")] string Description, + [property: JsonPropertyName("email")] string Email, + [property: JsonPropertyName("followers_count")] long FollowersCount, + [property: JsonPropertyName("following_count")] long FollowingCount, + [property: JsonPropertyName("full_name")] string FullName, + [property: JsonPropertyName("html_url")] string HtmlUrl, + [property: JsonPropertyName("id")] long Id, + [property: JsonPropertyName("is_admin")] bool IsAdmin, + [property: JsonPropertyName("language")] string Language, + [property: JsonPropertyName("last_login")] DateTimeOffset LastLogin, + [property: JsonPropertyName("location")] string Location, + [property: JsonPropertyName("login")] string Login, + [property: JsonPropertyName("login_name")] string LoginName, + [property: JsonPropertyName("prohibit_login")] bool ProhibitLogin, + [property: JsonPropertyName("pronouns")] string Pronouns, + [property: JsonPropertyName("restricted")] bool Restricted, + [property: JsonPropertyName("source_id")] long SourceId, + [property: JsonPropertyName("starred_repos_count")] long StarredReposCount, + [property: JsonPropertyName("visibility")] string Visibility, + [property: JsonPropertyName("website")] string Website +); diff --git a/Program.cs b/Program.cs index a1f0555..c493a2e 100644 --- a/Program.cs +++ b/Program.cs @@ -11,7 +11,16 @@ var repositoryConfig = await RepoConfigLoader.LoadConfigAsync( config.RepositoryConfigFilePath); builder.Services.AddSingleton(repositoryConfig); +var client = new HttpClient(); +client.DefaultRequestHeaders.Add("User-Agent", "ForgejoReleaseSync"); +client.DefaultRequestHeaders.Add("Accept", "application/json"); +client.Timeout = TimeSpan.FromSeconds(90); +client.MaxResponseContentBufferSize = 1024 * 1024 * 1; +builder.Services.AddSingleton(client); + +builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); var app = builder.Build(); diff --git a/Services/ForgejoService.cs b/Services/ForgejoService.cs index 00e0723..6325baf 100644 --- a/Services/ForgejoService.cs +++ b/Services/ForgejoService.cs @@ -1,6 +1,59 @@ +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using ForgejoReleaseSync.DTOs.Forgejo; +using Microsoft.Extensions.Logging; + namespace ForgejoReleaseSync.Services; -public class ForgejoService +public class ForgejoService(HttpClient httpClient, AppConfig config, ILogger logger) { - + public async Task CreateReleaseAsync(string owner, string repo, string body, string name, string tagName, CancellationToken token = default) + { + try + { + var dto = new ForgejoCreateReleaseDto(body, false, false, name, false, tagName, ""); + + var json = JsonSerializer.Serialize(dto, AppJsonSerializerContext.Default.ForgejoCreateReleaseDto); + using var content = new StringContent(json, Encoding.UTF8, "application/json"); + + using var request = new HttpRequestMessage(HttpMethod.Post, new Uri($"{config.ForgejoInstacne}/api/v1/repos/{owner}/{repo}/releases")); + request.Content = content; + + request.Headers.Authorization = + new AuthenticationHeaderValue("token", config.ForgejoToken); + + using var response = await httpClient.SendAsync(request, token); + response.EnsureSuccessStatusCode(); + } + catch (Exception e) + { + logger.LogError(e, "Failed to create forgejo release"); + throw; + } + + } + + public async Task> FetchRepoReleasesAsync(string owner, string repo) + { + try + { + var response = await httpClient.GetAsync( + new Uri($"{config.ForgejoInstacne}/api/v1/repos/{owner}/{repo}/releases?per_page=3&page=1")); + + response.EnsureSuccessStatusCode(); + + var responseStream = await response.Content.ReadAsStreamAsync(); + var releases = await JsonSerializer.DeserializeAsync(responseStream, + AppJsonSerializerContext.Default.ListForgejoReleaseResponseDto); + + return releases ?? []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to fetch forgejo releases"); + throw; + } + } } \ No newline at end of file diff --git a/Services/GithubService.cs b/Services/GithubService.cs index 96081bf..f3dd0f9 100644 --- a/Services/GithubService.cs +++ b/Services/GithubService.cs @@ -1,39 +1,23 @@ using System.Text.Json; using System.Text.Json.Serialization; using ForgejoReleaseSync.DTOs; +using ForgejoReleaseSync.DTOs.Forgejo; using Microsoft.Extensions.Logging; namespace ForgejoReleaseSync.Services; -public class GithubService +public class GithubService( + AppConfig config, + RepositoryConfig repoConfig, + HttpClient httpClient, + ILogger logger) { - - private readonly AppConfig _config; - private readonly RepositoryConfig _repoConfig; - private readonly HttpClient _httpClient; - private readonly ILogger _logger; - - public GithubService(AppConfig config, RepositoryConfig repoConfig, ILogger logger) - { - var client = new HttpClient(); - client.BaseAddress = new Uri("https://api.github.com"); - client.DefaultRequestHeaders.Add("User-Agent", "ForgejoReleaseSync"); - client.DefaultRequestHeaders.Add("Accept", "application/json"); - client.Timeout = TimeSpan.FromSeconds(90); - - _config = config; - _repoConfig = repoConfig; - _httpClient = client; - _logger = logger; - } - - public async Task> FetchRepoReleasesAsync(string owner, string repo) { try { - var response = await _httpClient.GetAsync( - $"/repos/{owner}/{repo}/releases?per_page=3&page=1"); + var response = await httpClient.GetAsync( + new Uri($"https://api.github.com/repos/{owner}/{repo}/releases?per_page=3&page=1")); response.EnsureSuccessStatusCode(); @@ -45,13 +29,16 @@ public class GithubService } catch (Exception ex) { - _logger.LogError(ex, "Failed to fetch posts from Mastodon"); - return []; + logger.LogError(ex, "Failed to fetch posts from Mastodon"); + throw; } } } +// TODO: move this [JsonSerializable(typeof(List))] +[JsonSerializable(typeof(ForgejoCreateReleaseDto))] +[JsonSerializable(typeof(List))] internal partial class AppJsonSerializerContext : JsonSerializerContext { } \ No newline at end of file diff --git a/Services/ReleaseAssetDownloadService.cs b/Services/ReleaseAssetDownloadService.cs new file mode 100644 index 0000000..3fdaf2f --- /dev/null +++ b/Services/ReleaseAssetDownloadService.cs @@ -0,0 +1,14 @@ +namespace ForgejoReleaseSync.Services; + +public class ReleaseAssetDownloadService(HttpClient httpClient) +{ + public async Task DownloadReleaseAssetAsync(Uri url) + { + var fileName = Path.GetFileName(url.AbsoluteUri); + var filePath = Path.Combine(Path.GetTempPath(), fileName); + await using Stream resource = await httpClient.GetStreamAsync(url); + await using FileStream fs = new FileStream(filePath, FileMode.OpenOrCreate); + await resource.CopyToAsync(fs, 8192); + return filePath; + } +} \ No newline at end of file diff --git a/Services/SyncService.cs b/Services/SyncService.cs index 197bac0..b7149c0 100644 --- a/Services/SyncService.cs +++ b/Services/SyncService.cs @@ -8,10 +8,10 @@ public class SyncService( AppConfig config, RepositoryConfig repositoryConfig, GithubService githubService, + ForgejoService forgejoService, + ReleaseAssetDownloadService releaseAssetDownloadService, ILogger logger) { - private readonly AppConfig _appConfig = config; - private readonly GithubService _githubService = githubService; public async Task Sync() { @@ -25,12 +25,24 @@ public class SyncService( var includeRegex = new Regex(repo.IncludeAssetsRegex); var excludeRegex = new Regex(repo.ExcludeAssetsRegex); - var releases = await _githubService.FetchRepoReleasesAsync(repoOwner, repoName); + var releases = await githubService.FetchRepoReleasesAsync(repoOwner, repoName); var lastRelease = releases.LastOrDefault(r => r.Prerelease == false); if (lastRelease is null) continue; + var forgejoReleases = await forgejoService.FetchRepoReleasesAsync(repoOwner, repoName); + var lastForgejoRelease = forgejoReleases.LastOrDefault(r => r.Prerelease == false); + + if (lastForgejoRelease is null || lastForgejoRelease.TagName != lastRelease.TagName) + { + logger.LogInformation("Creating release {ReleaseName} for repo {Owner}/{Name}", + lastRelease.Name, + repoOwner, + repoName); + await forgejoService.CreateReleaseAsync(repoOwner, repoName, lastRelease.Body, lastRelease.Name, lastRelease.TagName); + } + var acceptableAssets = lastRelease.Assets .Where(asset => includeRegex.IsMatch(asset.Name) && @@ -46,7 +58,11 @@ public class SyncService( foreach (var acceptableAsset in acceptableAssets) { - logger.LogInformation(acceptableAsset.Name); + logger.LogInformation($"Downloading {acceptableAsset.Name}"); + + var downloadPath = await releaseAssetDownloadService.DownloadReleaseAssetAsync(acceptableAsset.BrowserDownloadUrl); + + logger.LogInformation($"Downloaded {acceptableAsset.Name} to {downloadPath}"); } } }