Update Notification Refactor (#511)
* Replaced profile links to anchors so we can open in new tab if we like * Refactored how update checking works. We now explicitly check and send back on the same API. We have a weekly job that will push an update to the user. * Implemented a changelog tab * Ported over a GA fix for using ' in PR bodies. * Don't check cert for Github
This commit is contained in:
parent
0e48aeebc5
commit
2a76092566
21 changed files with 246 additions and 56 deletions
|
|
@ -1,7 +1,9 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs.Stats;
|
||||
using API.DTOs.Update;
|
||||
using API.Extensions;
|
||||
using API.Interfaces;
|
||||
using API.Interfaces.Services;
|
||||
|
|
@ -25,9 +27,11 @@ namespace API.Controllers
|
|||
private readonly IArchiveService _archiveService;
|
||||
private readonly ICacheService _cacheService;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly IVersionUpdaterService _versionUpdaterService;
|
||||
|
||||
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger, IConfiguration config,
|
||||
IBackupService backupService, IArchiveService archiveService, ICacheService cacheService, ITaskScheduler taskScheduler)
|
||||
IBackupService backupService, IArchiveService archiveService, ICacheService cacheService, ITaskScheduler taskScheduler,
|
||||
IVersionUpdaterService versionUpdaterService)
|
||||
{
|
||||
_applicationLifetime = applicationLifetime;
|
||||
_logger = logger;
|
||||
|
|
@ -36,6 +40,7 @@ namespace API.Controllers
|
|||
_archiveService = archiveService;
|
||||
_cacheService = cacheService;
|
||||
_taskScheduler = taskScheduler;
|
||||
_versionUpdaterService = versionUpdaterService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -102,11 +107,16 @@ namespace API.Controllers
|
|||
}
|
||||
}
|
||||
|
||||
[HttpPost("check-update")]
|
||||
public ActionResult CheckForUpdates()
|
||||
[HttpGet("check-update")]
|
||||
public async Task<ActionResult<UpdateNotificationDto>> CheckForUpdates()
|
||||
{
|
||||
_taskScheduler.CheckForUpdate();
|
||||
return Ok();
|
||||
return Ok(await _versionUpdaterService.CheckForUpdate());
|
||||
}
|
||||
|
||||
[HttpGet("changelog")]
|
||||
public async Task<ActionResult<IEnumerable<UpdateNotificationDto>>> GetChangelog()
|
||||
{
|
||||
return Ok(await _versionUpdaterService.GetAllReleases());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
38
API/DTOs/Update/UpdateNotificationDto.cs
Normal file
38
API/DTOs/Update/UpdateNotificationDto.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
namespace API.DTOs.Update
|
||||
{
|
||||
/// <summary>
|
||||
/// Update Notification denoting a new release available for user to update to
|
||||
/// </summary>
|
||||
public class UpdateNotificationDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Current installed Version
|
||||
/// </summary>
|
||||
public string CurrentVersion { get; init; }
|
||||
/// <summary>
|
||||
/// Semver of the release version
|
||||
/// <example>0.4.3</example>
|
||||
/// </summary>
|
||||
public string UpdateVersion { get; init; }
|
||||
/// <summary>
|
||||
/// Release body in HTML
|
||||
/// </summary>
|
||||
public string UpdateBody { get; init; }
|
||||
/// <summary>
|
||||
/// Title of the release
|
||||
/// </summary>
|
||||
public string UpdateTitle { get; init; }
|
||||
/// <summary>
|
||||
/// Github Url
|
||||
/// </summary>
|
||||
public string UpdateUrl { get; init; }
|
||||
/// <summary>
|
||||
/// If this install is within Docker
|
||||
/// </summary>
|
||||
public bool IsDocker { get; init; }
|
||||
/// <summary>
|
||||
/// Is this a pre-release
|
||||
/// </summary>
|
||||
public bool IsPrerelease { get; init; }
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,5 @@
|
|||
void RefreshSeriesMetadata(int libraryId, int seriesId);
|
||||
void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false);
|
||||
void CancelStatsTasks();
|
||||
void CheckForUpdate();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs.Update;
|
||||
using API.Services.Tasks;
|
||||
|
||||
namespace API.Interfaces.Services
|
||||
{
|
||||
public interface IVersionUpdaterService
|
||||
{
|
||||
public Task CheckForUpdate();
|
||||
|
||||
Task<UpdateNotificationDto> CheckForUpdate();
|
||||
Task PushUpdate(UpdateNotificationDto update);
|
||||
Task<IEnumerable<UpdateNotificationDto>> GetAllReleases();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ namespace API.Services
|
|||
}
|
||||
|
||||
RecurringJob.AddOrUpdate("cleanup", () => _cleanupService.Cleanup(), Cron.Daily);
|
||||
|
||||
RecurringJob.AddOrUpdate("check-for-updates", () => _scannerService.ScanLibraries(), Cron.Daily);
|
||||
}
|
||||
|
||||
#region StatsTasks
|
||||
|
|
@ -104,7 +106,7 @@ namespace API.Services
|
|||
public void ScheduleUpdaterTasks()
|
||||
{
|
||||
_logger.LogInformation("Scheduling Auto-Update tasks");
|
||||
RecurringJob.AddOrUpdate("check-updates", () => _versionUpdaterService.CheckForUpdate(), Cron.Daily);
|
||||
RecurringJob.AddOrUpdate("check-updates", () => CheckForUpdate(), Cron.Weekly);
|
||||
|
||||
}
|
||||
#endregion
|
||||
|
|
@ -152,9 +154,13 @@ namespace API.Services
|
|||
BackgroundJob.Enqueue(() => _backupService.BackupDatabase());
|
||||
}
|
||||
|
||||
public void CheckForUpdate()
|
||||
/// <summary>
|
||||
/// Not an external call. Only public so that we can call this for a Task
|
||||
/// </summary>
|
||||
public async Task CheckForUpdate()
|
||||
{
|
||||
BackgroundJob.Enqueue(() => _versionUpdaterService.CheckForUpdate());
|
||||
var update = await _versionUpdaterService.CheckForUpdate();
|
||||
await _versionUpdaterService.PushUpdate(update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs.Update;
|
||||
using API.Interfaces.Services;
|
||||
using API.SignalR;
|
||||
using API.SignalR.Presence;
|
||||
using Flurl.Http;
|
||||
using Flurl.Http.Configuration;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using MarkdownDeep;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
|
@ -32,36 +36,81 @@ namespace API.Services.Tasks
|
|||
/// Url of the release on Github
|
||||
/// </summary>
|
||||
public string Html_Url { get; init; }
|
||||
|
||||
}
|
||||
|
||||
public class UntrustedCertClientFactory : DefaultHttpClientFactory
|
||||
{
|
||||
public override HttpMessageHandler CreateMessageHandler() {
|
||||
return new HttpClientHandler {
|
||||
ServerCertificateCustomValidationCallback = (a, b, c, d) => true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class VersionUpdaterService : IVersionUpdaterService
|
||||
{
|
||||
private readonly ILogger<VersionUpdaterService> _logger;
|
||||
private readonly IHubContext<MessageHub> _messageHub;
|
||||
private readonly IPresenceTracker _tracker;
|
||||
private readonly Markdown _markdown = new MarkdownDeep.Markdown();
|
||||
private static readonly string GithubLatestReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases/latest";
|
||||
private static readonly string GithubAllReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases";
|
||||
|
||||
public VersionUpdaterService(ILogger<VersionUpdaterService> logger, IHubContext<MessageHub> messageHub, IPresenceTracker tracker)
|
||||
{
|
||||
_logger = logger;
|
||||
_messageHub = messageHub;
|
||||
_tracker = tracker;
|
||||
|
||||
FlurlHttp.ConfigureClient(GithubLatestReleasesUrl, cli =>
|
||||
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
|
||||
FlurlHttp.ConfigureClient(GithubAllReleasesUrl, cli =>
|
||||
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scheduled Task that checks if a newer version is available. If it is, will check if User is currently connected and push
|
||||
/// a message.
|
||||
/// Fetches the latest release from Github
|
||||
/// </summary>
|
||||
public async Task CheckForUpdate()
|
||||
public async Task<UpdateNotificationDto> CheckForUpdate()
|
||||
{
|
||||
|
||||
var update = await GetGithubRelease();
|
||||
return CreateDto(update);
|
||||
}
|
||||
|
||||
if (update == null || string.IsNullOrEmpty(update.Tag_Name)) return;
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<IEnumerable<UpdateNotificationDto>> GetAllReleases()
|
||||
{
|
||||
var updates = await GetGithubReleases();
|
||||
return updates.Select(CreateDto);
|
||||
}
|
||||
|
||||
var admins = await _tracker.GetOnlineAdmins();
|
||||
private UpdateNotificationDto? CreateDto(GithubReleaseMetadata update)
|
||||
{
|
||||
if (update == null || string.IsNullOrEmpty(update.Tag_Name)) return null;
|
||||
var version = update.Tag_Name.Replace("v", string.Empty);
|
||||
var updateVersion = new Version(version);
|
||||
|
||||
return new UpdateNotificationDto()
|
||||
{
|
||||
CurrentVersion = version,
|
||||
UpdateVersion = updateVersion.ToString(),
|
||||
UpdateBody = _markdown.Transform(update.Body.Trim()),
|
||||
UpdateTitle = update.Name,
|
||||
UpdateUrl = update.Html_Url,
|
||||
IsDocker = new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker
|
||||
};
|
||||
}
|
||||
|
||||
public async Task PushUpdate(UpdateNotificationDto update)
|
||||
{
|
||||
if (update == null) return;
|
||||
|
||||
var admins = await _tracker.GetOnlineAdmins();
|
||||
var updateVersion = new Version(update.CurrentVersion);
|
||||
|
||||
if (BuildInfo.Version < updateVersion)
|
||||
{
|
||||
_logger.LogInformation("Server is out of date. Current: {CurrentVersion}. Available: {AvailableUpdate}", BuildInfo.Version, updateVersion);
|
||||
|
|
@ -74,10 +123,8 @@ namespace API.Services.Tasks
|
|||
}
|
||||
}
|
||||
|
||||
private async Task SendEvent(GithubReleaseMetadata update, IReadOnlyList<string> admins)
|
||||
private async Task SendEvent(UpdateNotificationDto update, IReadOnlyList<string> admins)
|
||||
{
|
||||
var version = update.Tag_Name.Replace("v", string.Empty);
|
||||
var updateVersion = new Version(version);
|
||||
var connections = new List<string>();
|
||||
foreach (var admin in admins)
|
||||
{
|
||||
|
|
@ -87,26 +134,29 @@ namespace API.Services.Tasks
|
|||
await _messageHub.Clients.Users(admins).SendAsync("UpdateAvailable", new SignalRMessage
|
||||
{
|
||||
Name = "UpdateAvailable",
|
||||
Body = new
|
||||
{
|
||||
CurrentVersion = version,
|
||||
UpdateVersion = updateVersion.ToString(),
|
||||
UpdateBody = _markdown.Transform(update.Body.Trim()),
|
||||
UpdateTitle = update.Name,
|
||||
UpdateUrl = update.Html_Url,
|
||||
IsDocker = new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker
|
||||
}
|
||||
Body = update
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private static async Task<GithubReleaseMetadata> GetGithubRelease()
|
||||
{
|
||||
var update = await "https://api.github.com/repos/Kareadita/Kavita/releases/latest"
|
||||
var update = await GithubLatestReleasesUrl
|
||||
.WithHeader("Accept", "application/json")
|
||||
.WithHeader("User-Agent", "Kavita")
|
||||
.GetJsonAsync<GithubReleaseMetadata>();
|
||||
|
||||
return update;
|
||||
}
|
||||
|
||||
private static async Task<IEnumerable<GithubReleaseMetadata>> GetGithubReleases()
|
||||
{
|
||||
var update = await GithubAllReleasesUrl
|
||||
.WithHeader("Accept", "application/json")
|
||||
.WithHeader("User-Agent", "Kavita")
|
||||
.GetJsonAsync<IEnumerable<GithubReleaseMetadata>>();
|
||||
|
||||
return update;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,27 +7,30 @@ using Microsoft.AspNetCore.SignalR;
|
|||
namespace API.SignalR
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Generic hub for sending messages to UI
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class MessageHub : Hub
|
||||
{
|
||||
private static readonly HashSet<string> _connections = new HashSet<string>();
|
||||
private static readonly HashSet<string> Connections = new HashSet<string>();
|
||||
|
||||
public static bool IsConnected
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_connections)
|
||||
lock (Connections)
|
||||
{
|
||||
return _connections.Count != 0;
|
||||
return Connections.Count != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
lock (_connections)
|
||||
lock (Connections)
|
||||
{
|
||||
_connections.Add(Context.ConnectionId);
|
||||
Connections.Add(Context.ConnectionId);
|
||||
}
|
||||
|
||||
await base.OnConnectedAsync();
|
||||
|
|
@ -35,9 +38,9 @@ namespace API.SignalR
|
|||
|
||||
public override async Task OnDisconnectedAsync(Exception exception)
|
||||
{
|
||||
lock (_connections)
|
||||
lock (Connections)
|
||||
{
|
||||
_connections.Remove(Context.ConnectionId);
|
||||
Connections.Remove(Context.ConnectionId);
|
||||
}
|
||||
|
||||
await base.OnDisconnectedAsync(exception);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ using Microsoft.AspNetCore.SignalR;
|
|||
|
||||
namespace API.SignalR
|
||||
{
|
||||
/// <summary>
|
||||
/// Keeps track of who is logged into the app
|
||||
/// </summary>
|
||||
public class PresenceHub : Hub
|
||||
{
|
||||
private readonly IPresenceTracker _tracker;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue