Skip to content
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
1 change: 1 addition & 0 deletions Daybreak.Core/Configuration/ProjectConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ public override void RegisterStartupActions(IStartupActionProducer startupAction
startupActionProducer.RegisterAction<CredentialsOptionsMigrator>();
startupActionProducer.RegisterAction<UpdateToolboxAction>();
startupActionProducer.RegisterAction<UpdateGuildWarsExecutable>();
startupActionProducer.RegisterAction<UpdateDirectSongAction>();
}

public override void RegisterPostUpdateActions(
Expand Down
1 change: 1 addition & 0 deletions Daybreak.Core/Daybreak.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
<PackageReference Include="DiffPlex" />
<PackageReference Include="HtmlAgilityPack" />
<PackageReference Include="ini-parser-netstandard" />
<PackageReference Include="MegaApiClient" />
<PackageReference Include="Microsoft.CorrelationVector" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
Expand Down
331 changes: 286 additions & 45 deletions Daybreak.Core/Services/DirectSong/DirectSongService.cs

Large diffs are not rendered by default.

146 changes: 146 additions & 0 deletions Daybreak.Core/Services/Github/GithubClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ internal sealed class GithubClient(
private const string RefTagsUrlTemplate = "https://api.github.com/repos/{0}/{1}/git/refs/tags";
private const string ReleasesUrlTemplate = "https://api.github.com/repos/{0}/{1}/releases";
private const string LatestReleaseUrlTemplate = "https://github.com/{0}/{1}/releases/latest";
private const string RawFileUrlTemplate = "https://raw.githubusercontent.com/{0}/{1}/{2}/{3}";
private const string CommitsAtomUrlTemplate = "https://github.com/{0}/{1}/commits/{2}.atom";
private const string ContentsUrlTemplate = "https://api.github.com/repos/{0}/{1}/contents/{2}?ref={3}";

private static readonly TimeSpan RequestTimeout = TimeSpan.FromSeconds(10);
private static readonly TimeSpan DownloadTimeout = TimeSpan.FromMinutes(5);

private readonly IHttpClient<GithubClient> httpClient = httpClient.ThrowIfNull();
private readonly ILogger<GithubClient> logger = logger.ThrowIfNull();
Expand Down Expand Up @@ -120,4 +124,146 @@ public async Task<IEnumerable<GithubRelease>> GetReleases(string owner, string r
return default;
}
}

public async Task<bool> DownloadRawFile(string owner, string repo, string branch, string filePath, string destinationPath, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
var url = string.Format(RawFileUrlTemplate, owner, repo, branch, filePath);
try
{
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(DownloadTimeout);

scopedLogger.LogDebug("Downloading raw file from {Url}", url);
using var response = await this.httpClient.GetAsync(url, timeoutCts.Token);
if (!response.IsSuccessStatusCode)
{
scopedLogger.LogError("Failed to download raw file. Status code: {StatusCode}", response.StatusCode);
return false;
}

// Ensure directory exists
var directory = Path.GetDirectoryName(destinationPath);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}

using var fileStream = File.Create(destinationPath);
await response.Content.CopyToAsync(fileStream, timeoutCts.Token);

scopedLogger.LogDebug("Successfully downloaded {FilePath} to {DestinationPath}", filePath, destinationPath);
return true;
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
scopedLogger.LogWarning("Download from {Url} timed out", url);
return false;
}
catch (Exception e)
{
scopedLogger.LogError(e, "Failed to download raw file from {Url}", url);
return false;
}
}

public async Task<string?> GetLatestCommitSha(string owner, string repo, string branch, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
var url = string.Format(CommitsAtomUrlTemplate, owner, repo, branch);
try
{
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(RequestTimeout);

scopedLogger.LogDebug("Fetching commits atom feed from {Url}", url);
using var response = await this.httpClient.GetAsync(url, timeoutCts.Token);
if (!response.IsSuccessStatusCode)
{
scopedLogger.LogError("Failed to fetch commits atom feed. Status code: {StatusCode}", response.StatusCode);
return null;
}

var content = await response.Content.ReadAsStringAsync(timeoutCts.Token);

// Parse the Atom feed to extract the latest commit SHA from the first <id> tag
// Format: tag:github.com,2008:Grit::Commit/{sha}
var idStartIndex = content.IndexOf("<id>tag:github.com,2008:Grit::Commit/", StringComparison.Ordinal);
if (idStartIndex < 0)
{
scopedLogger.LogWarning("Could not find commit ID in atom feed");
return null;
}

var shaStart = idStartIndex + "<id>tag:github.com,2008:Grit::Commit/".Length;
var shaEnd = content.IndexOf("</id>", shaStart, StringComparison.Ordinal);
if (shaEnd < 0)
{
scopedLogger.LogWarning("Could not parse commit SHA from atom feed");
return null;
}

var sha = content[shaStart..shaEnd];
scopedLogger.LogDebug("Latest commit SHA: {Sha}", sha);
return sha;
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
scopedLogger.LogWarning("Request to {Url} timed out", url);
return null;
}
catch (Exception e)
{
scopedLogger.LogError(e, "Failed to fetch commits from {Url}", url);
return null;
}
}

public async Task<IEnumerable<string>> GetDirectoryContents(string owner, string repo, string path, string branch, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
var url = string.Format(ContentsUrlTemplate, owner, repo, path, branch);
try
{
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(RequestTimeout);

scopedLogger.LogDebug("Fetching directory contents from {Url}", url);
using var response = await this.httpClient.GetAsync(url, timeoutCts.Token);
if (!response.IsSuccessStatusCode)
{
scopedLogger.LogError("Failed to fetch directory contents. Status code: {StatusCode}", response.StatusCode);
return [];
}

var items = await response.Content.ReadFromJsonAsync<List<GithubContentItem>>(timeoutCts.Token);
if (items is null)
{
scopedLogger.LogWarning("Failed to deserialize directory contents");
return [];
}

var files = items
.Where(item => item.IsFile && !string.IsNullOrEmpty(item.Name))
.Select(item => item.Name!)
.ToList();

foreach (var fileName in files)
{
scopedLogger.LogDebug("Found file: {FileName}", fileName);
}

return files;
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
scopedLogger.LogWarning("Request to {Url} timed out", url);
return [];
}
catch (Exception e)
{
scopedLogger.LogError(e, "Failed to fetch directory contents from {Url}", url);
return [];
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Daybreak.Shared.Models;
using Daybreak.Shared.Models.Async;
using Daybreak.Shared.Services.DirectSong;
using Microsoft.Extensions.Logging;
using System.Core.Extensions;
using System.Extensions.Core;

namespace Daybreak.Services.Startup.Actions;

internal sealed class UpdateDirectSongAction(
IDirectSongService directSongService,
ILogger<UpdateUModAction> logger) : StartupActionBase
{
private readonly IDirectSongService directSongService = directSongService.ThrowIfNull();
private readonly ILogger<UpdateUModAction> logger = logger.ThrowIfNull();

public override void ExecuteOnStartup()
{
var scopedLogger = this.logger.CreateScopedLogger();
var progress = new Progress<ProgressUpdate>();
if (!this.directSongService.IsInstalled)
{
scopedLogger.LogInformation("DirectSong is not installed. Skipping update.");
return;
}

Task.Factory.StartNew(async () =>
{
if (!await this.directSongService.IsUpdateAvailable(CancellationToken.None))
{
scopedLogger.LogInformation("DirectSong is up to date");
return;
}

await this.directSongService.PerformUpdate(CancellationToken.None);
}, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Current);
}
}
10 changes: 3 additions & 7 deletions Daybreak.Core/Services/TarGz/TarGzExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ private bool ExtractToDirectoryInternal(

// Use ReaderFactory which properly handles nested archives like .tar.gz
using var stream = File.OpenRead(sourceFile);
using var reader = ReaderFactory.Open(stream);
using var reader = ReaderFactory.OpenReader(stream, new ReaderOptions { ExtractFullPath = true, Overwrite = true });

var processedEntries = 0;

Expand All @@ -68,11 +68,7 @@ private bool ExtractToDirectoryInternal(
progressTracker(0.5, entryKey);

// Extract the entry
reader.WriteEntryToDirectory(destinationDirectory, new ExtractionOptions
{
ExtractFullPath = true,
Overwrite = true
});
reader.WriteEntryToDirectory(destinationDirectory);

processedEntries++;
}
Expand All @@ -97,4 +93,4 @@ private bool ExtractToDirectoryInternal(
return false;
}
}
}
}
2 changes: 1 addition & 1 deletion Daybreak.Core/Views/GuildWarsMarketView.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ namespace Daybreak.Views;

public sealed class GuildWarsMarketViewModel : ViewModelBase<GuildWarsMarketViewModel, GuildWarsMarketView>
{
public string GwMarketUrl { get; } = "https://v2.gwmarket.net/";
public string GwMarketUrl { get; } = "https://gwmarket.net/";
}
Loading
Loading