Merge branch 'develop' of https://github.com/Kareadita/Kavita into develop

This commit is contained in:
Robbie Davis 2023-02-20 16:55:20 -05:00
commit 8a62d54c0b
19 changed files with 192 additions and 49 deletions

View File

@ -334,7 +334,7 @@ public class LibraryController : BaseApiController
library.IncludeInDashboard = dto.IncludeInDashboard;
library.IncludeInRecommended = dto.IncludeInRecommended;
library.IncludeInSearch = dto.IncludeInSearch;
library.ManageCollections = dto.CreateCollections;
library.ManageCollections = dto.ManageCollections;
_unitOfWork.LibraryRepository.Update(library);

View File

@ -23,6 +23,6 @@ public class UpdateLibraryDto
[Required]
public bool IncludeInSearch { get; init; }
[Required]
public bool CreateCollections { get; init; }
public bool ManageCollections { get; init; }
}

View File

@ -0,0 +1,135 @@
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data;
/// <summary>
/// v0.7 introduced UTC dates and GMT+1 users would sometimes have dates stored as '0000-12-31 23:00:00'.
/// This Migration will update those dates.
/// </summary>
public static class MigrateBrokenGMT1Dates
{
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger)
{
// if current version is > 0.7, then we can exit and not perform
var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync();
if (Version.Parse(settings.InstallVersion) > new Version(0, 7, 0, 2))
{
return;
}
logger.LogCritical("Running MigrateBrokenGMT1Dates migration. Please be patient, this may take some time depending on the size of your library. Do not abort, this can break your Database");
#region Series
logger.LogInformation("Updating Dates on Series...");
await dataContext.Database.ExecuteSqlRawAsync(@"
UPDATE Series SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00';
UPDATE Series SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00';
UPDATE Series SET LastChapterAddedUtc = '0001-01-01 00:00:00' WHERE LastChapterAddedUtc = '0000-12-31 23:00:00';
UPDATE Series SET LastFolderScannedUtc = '0001-01-01 00:00:00' WHERE LastFolderScannedUtc = '0000-12-31 23:00:00';
");
logger.LogInformation("Updating Dates on Series...Done");
#endregion
#region Library
logger.LogInformation("Updating Dates on Libraries...");
await dataContext.Database.ExecuteSqlRawAsync(@"
UPDATE Library SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00';
UPDATE Library SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00';
");
logger.LogInformation("Updating Dates on Libraries...Done");
#endregion
#region Volume
try
{
logger.LogInformation("Updating Dates on Volumes...");
await dataContext.Database.ExecuteSqlRawAsync(@"
UPDATE Volume SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00';
UPDATE Volume SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00';
");
logger.LogInformation("Updating Dates on Volumes...Done");
}
catch (Exception ex)
{
logger.LogCritical(ex, "Updating Dates on Volumes...Failed");
}
#endregion
#region Chapter
try
{
logger.LogInformation("Updating Dates on Chapters...");
await dataContext.Database.ExecuteSqlRawAsync(@"
UPDATE Chapter SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00';
UPDATE Chapter SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00';
");
logger.LogInformation("Updating Dates on Chapters...Done");
}
catch (Exception ex)
{
logger.LogCritical(ex, "Updating Dates on Chapters...Failed");
}
#endregion
#region AppUserBookmark
logger.LogInformation("Updating Dates on Bookmarks...");
await dataContext.Database.ExecuteSqlRawAsync(@"
UPDATE AppUserBookmark SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00';
UPDATE AppUserBookmark SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00';
");
logger.LogInformation("Updating Dates on Bookmarks...Done");
#endregion
#region AppUserProgress
logger.LogInformation("Updating Dates on Progress...");
await dataContext.Database.ExecuteSqlRawAsync(@"
UPDATE AppUserProgresses SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00';
UPDATE AppUserProgresses SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00';
");
logger.LogInformation("Updating Dates on Progress...Done");
#endregion
#region Device
logger.LogInformation("Updating Dates on Device...");
await dataContext.Database.ExecuteSqlRawAsync(@"
UPDATE Device SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00';
UPDATE Device SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00';
UPDATE Device SET LastUsedUtc = '0001-01-01 00:00:00' WHERE LastUsedUtc = '0000-12-31 23:00:00';
");
logger.LogInformation("Updating Dates on Device...Done");
#endregion
#region MangaFile
logger.LogInformation("Updating Dates on MangaFile...");
await dataContext.Database.ExecuteSqlRawAsync(@"
UPDATE MangaFile SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00';
UPDATE MangaFile SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00';
UPDATE MangaFile SET LastFileAnalysisUtc = '0001-01-01 00:00:00' WHERE LastFileAnalysisUtc = '0000-12-31 23:00:00';
");
logger.LogInformation("Updating Dates on MangaFile...Done");
#endregion
#region ReadingList
logger.LogInformation("Updating Dates on ReadingList...");
await dataContext.Database.ExecuteSqlRawAsync(@"
UPDATE ReadingList SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00';
UPDATE ReadingList SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00';
");
logger.LogInformation("Updating Dates on ReadingList...Done");
#endregion
#region SiteTheme
logger.LogInformation("Updating Dates on SiteTheme...");
await dataContext.Database.ExecuteSqlRawAsync(@"
UPDATE SiteTheme SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00';
UPDATE SiteTheme SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00';
");
logger.LogInformation("Updating Dates on SiteTheme...Done");
#endregion
logger.LogInformation("MigrateBrokenGMT1Dates migration finished");
}
}

View File

@ -4,7 +4,8 @@ namespace API.Extensions;
public static class StringExtensions
{
private static readonly Regex SentenceCaseRegex = new Regex(@"(^[a-z])|\.\s+(.)", RegexOptions.ExplicitCapture | RegexOptions.Compiled);
private static readonly Regex SentenceCaseRegex = new Regex(@"(^[a-z])|\.\s+(.)",
RegexOptions.ExplicitCapture | RegexOptions.Compiled, Services.Tasks.Scanner.Parser.Parser.RegexTimeout);
public static string SentenceCase(this string value)
{

View File

@ -44,12 +44,11 @@ public class Program
.CreateBootstrapLogger();
var directoryService = new DirectoryService(null, new FileSystem());
// Before anything, check if JWT has been generated properly or if user still has default
if (!Configuration.CheckIfJwtTokenSet() &&
Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != Environments.Development)
{
Console.WriteLine("Generating JWT TokenKey for encrypting user sessions...");
Log.Logger.Information("Generating JWT TokenKey for encrypting user sessions...");
var rBytes = new byte[128];
RandomNumberGenerator.Create().GetBytes(rBytes);
Configuration.JwtToken = Convert.ToBase64String(rBytes).Replace("/", string.Empty);
@ -174,7 +173,7 @@ public class Program
webBuilder.UseKestrel((opts) =>
{
var ipAddresses = Configuration.IpAddresses;
if (ipAddresses == null || ipAddresses.Length == 0)
if (string.IsNullOrEmpty(ipAddresses))
{
opts.ListenAnyIP(HttpPort, options => { options.Protocols = HttpProtocols.Http1AndHttp2; });
}

View File

@ -216,7 +216,7 @@ public class BookmarkService : IBookmarkService
foreach (var chapter in chapters)
{
var newFile = await SaveAsWebP(coverDirectory, chapter.CoverImage, coverDirectory);
chapter.CoverImage = newFile;
chapter.CoverImage = Path.GetFileName(newFile);
_unitOfWork.ChapterRepository.Update(chapter);
await _unitOfWork.CommitAsync();
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
@ -272,7 +272,6 @@ public class BookmarkService : IBookmarkService
{
// Convert target file to webp then delete original target file and update bookmark
var originalFile = filename;
try
{
var targetFile = await _imageService.ConvertToWebP(fullSourcePath, fullTargetDirectory);
@ -283,7 +282,7 @@ public class BookmarkService : IBookmarkService
catch (Exception ex)
{
_logger.LogError(ex, "Could not convert image {FilePath}", filename);
newFilename = originalFile;
newFilename = filename;
}
}
catch (Exception ex)

View File

@ -90,9 +90,11 @@ public class DirectoryService : IDirectoryService
private static readonly Regex ExcludeDirectories = new Regex(
@"@eaDir|\.DS_Store|\.qpkg|__MACOSX|@Recently-Snapshot|@recycle",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
RegexOptions.Compiled | RegexOptions.IgnoreCase,
Tasks.Scanner.Parser.Parser.RegexTimeout);
private static readonly Regex FileCopyAppend = new Regex(@"\(\d+\)",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
RegexOptions.Compiled | RegexOptions.IgnoreCase,
Tasks.Scanner.Parser.Parser.RegexTimeout);
public static readonly string BackupDirectory = Path.Join(Directory.GetCurrentDirectory(), "config", "backups");
public DirectoryService(ILogger<DirectoryService> logger, IFileSystem fileSystem)
@ -203,7 +205,8 @@ public class DirectoryService : IDirectoryService
if (fileNameRegex != string.Empty)
{
var reSearchPattern = new Regex(fileNameRegex, RegexOptions.IgnoreCase);
var reSearchPattern = new Regex(fileNameRegex, RegexOptions.IgnoreCase,
Tasks.Scanner.Parser.Parser.RegexTimeout);
return FileSystem.Directory.EnumerateFiles(path, "*", searchOption)
.Where(file =>
{

View File

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Linq;
using System.Threading.Tasks;
using API.Data;
using API.SignalR.Presence;
using Microsoft.AspNetCore.SignalR;
@ -36,8 +37,8 @@ public class EventHub : IEventHub
var users = _messageHub.Clients.All;
if (onlyAdmins)
{
var admins = await _presenceTracker.GetOnlineAdmins();
users = _messageHub.Clients.Users(admins);
var admins = await _presenceTracker.GetOnlineAdminIds();
users = _messageHub.Clients.Users(admins.Select(i => i.ToString()).ToArray());
}

View File

@ -26,13 +26,13 @@ public class LogHub : Hub<ILogHub>
public override async Task OnConnectedAsync()
{
await _tracker.UserConnected(Context.User.GetUsername(), Context.ConnectionId);
await _tracker.UserConnected(Context.User.GetUserId(), Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await _tracker.UserDisconnected(Context.User.GetUsername(), Context.ConnectionId);
await _tracker.UserDisconnected(Context.User.GetUserId(), Context.ConnectionId);
await base.OnDisconnectedAsync(exception);
}

View File

@ -22,7 +22,7 @@ public class MessageHub : Hub
public override async Task OnConnectedAsync()
{
await _tracker.UserConnected(Context.User.GetUsername(), Context.ConnectionId);
await _tracker.UserConnected(Context.User.GetUserId(), Context.ConnectionId);
var currentUsers = await PresenceTracker.GetOnlineUsers();
await Clients.All.SendAsync(MessageFactory.OnlineUsers, currentUsers);
@ -33,7 +33,7 @@ public class MessageHub : Hub
public override async Task OnDisconnectedAsync(Exception exception)
{
await _tracker.UserDisconnected(Context.User.GetUsername(), Context.ConnectionId);
await _tracker.UserDisconnected(Context.User.GetUserId(), Context.ConnectionId);
var currentUsers = await PresenceTracker.GetOnlineUsers();
await Clients.All.SendAsync(MessageFactory.OnlineUsers, currentUsers);

View File

@ -8,15 +8,16 @@ namespace API.SignalR.Presence;
public interface IPresenceTracker
{
Task UserConnected(string username, string connectionId);
Task UserDisconnected(string username, string connectionId);
Task<string[]> GetOnlineAdmins();
Task<List<string>> GetConnectionsForUser(string username);
Task UserConnected(int userId, string connectionId);
Task UserDisconnected(int userId, string connectionId);
Task<int[]> GetOnlineAdminIds();
Task<List<string>> GetConnectionsForUser(int userId);
}
internal class ConnectionDetail
{
public string UserName { get; set; }
public List<string> ConnectionIds { get; set; }
public bool IsAdmin { get; set; }
}
@ -28,28 +29,29 @@ internal class ConnectionDetail
public class PresenceTracker : IPresenceTracker
{
private readonly IUnitOfWork _unitOfWork;
private static readonly Dictionary<string, ConnectionDetail> OnlineUsers = new Dictionary<string, ConnectionDetail>();
private static readonly Dictionary<int, ConnectionDetail> OnlineUsers = new Dictionary<int, ConnectionDetail>();
public PresenceTracker(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task UserConnected(string username, string connectionId)
public async Task UserConnected(int userId, string connectionId)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username);
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
if (user == null) return;
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
lock (OnlineUsers)
{
if (OnlineUsers.ContainsKey(username))
if (OnlineUsers.ContainsKey(userId))
{
OnlineUsers[username].ConnectionIds.Add(connectionId);
OnlineUsers[userId].ConnectionIds.Add(connectionId);
}
else
{
OnlineUsers.Add(username, new ConnectionDetail()
OnlineUsers.Add(userId, new ConnectionDetail()
{
UserName = user.UserName,
ConnectionIds = new List<string>() {connectionId},
IsAdmin = isAdmin
});
@ -61,17 +63,17 @@ public class PresenceTracker : IPresenceTracker
await _unitOfWork.CommitAsync();
}
public Task UserDisconnected(string username, string connectionId)
public Task UserDisconnected(int userId, string connectionId)
{
lock (OnlineUsers)
{
if (!OnlineUsers.ContainsKey(username)) return Task.CompletedTask;
if (!OnlineUsers.ContainsKey(userId)) return Task.CompletedTask;
OnlineUsers[username].ConnectionIds.Remove(connectionId);
OnlineUsers[userId].ConnectionIds.Remove(connectionId);
if (OnlineUsers[username].ConnectionIds.Count == 0)
if (OnlineUsers[userId].ConnectionIds.Count == 0)
{
OnlineUsers.Remove(username);
OnlineUsers.Remove(userId);
}
}
return Task.CompletedTask;
@ -82,15 +84,15 @@ public class PresenceTracker : IPresenceTracker
string[] onlineUsers;
lock (OnlineUsers)
{
onlineUsers = OnlineUsers.OrderBy(k => k.Key).Select(k => k.Key).ToArray();
onlineUsers = OnlineUsers.OrderBy(k => k.Value.UserName).Select(k => k.Value.UserName).ToArray();
}
return Task.FromResult(onlineUsers);
}
public Task<string[]> GetOnlineAdmins()
public Task<int[]> GetOnlineAdminIds()
{
string[] onlineUsers;
int[] onlineUsers;
lock (OnlineUsers)
{
onlineUsers = OnlineUsers.Where(pair => pair.Value.IsAdmin).OrderBy(k => k.Key).Select(k => k.Key).ToArray();
@ -100,12 +102,12 @@ public class PresenceTracker : IPresenceTracker
return Task.FromResult(onlineUsers);
}
public Task<List<string>> GetConnectionsForUser(string username)
public Task<List<string>> GetConnectionsForUser(int userId)
{
List<string> connectionIds;
lock (OnlineUsers)
{
connectionIds = OnlineUsers.GetValueOrDefault(username)?.ConnectionIds;
connectionIds = OnlineUsers.GetValueOrDefault(userId)?.ConnectionIds;
}
return Task.FromResult(connectionIds ?? new List<string>());

View File

@ -234,6 +234,9 @@ public class Startup
await MigrateUserProgressLibraryId.Migrate(unitOfWork, logger);
await MigrateToUtcDates.Migrate(unitOfWork, dataContext, logger);
// v0.7
await MigrateBrokenGMT1Dates.Migrate(unitOfWork, dataContext, logger);
// Update the version in the DB after all migrations are run
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
installVersion.Value = BuildInfo.Version.ToString();

View File

@ -4,7 +4,7 @@
<TargetFramework>net6.0</TargetFramework>
<Company>kavitareader.com</Company>
<Product>Kavita</Product>
<AssemblyVersion>0.7.0.2</AssemblyVersion>
<AssemblyVersion>0.7.1.1</AssemblyVersion>
<NeutralLanguage>en</NeutralLanguage>
<TieredPGO>true</TieredPGO>
</PropertyGroup>

View File

@ -15,5 +15,5 @@ export interface Library {
includeInDashboard: boolean;
includeInRecommended: boolean;
includeInSearch: boolean;
createCollections: boolean;
manageCollections: boolean;
}

View File

@ -96,7 +96,7 @@ export class MessageHubService {
private hubConnection!: HubConnection;
private messagesSource = new ReplaySubject<Message<any>>(1);
private onlineUsersSource = new BehaviorSubject<string[]>([]);
private onlineUsersSource = new BehaviorSubject<number[]>([]); // UserIds
/**
* Any events that come from the backend
@ -142,7 +142,7 @@ export class MessageHubService {
.start()
.catch(err => console.error(err));
this.hubConnection.on(EVENTS.OnlineUsers, (usernames: string[]) => {
this.hubConnection.on(EVENTS.OnlineUsers, (usernames: number[]) => {
this.onlineUsersSource.next(usernames);
});

View File

@ -39,7 +39,7 @@
<li *ngFor="let member of members; let idx = index;" class="list-group-item no-hover">
<div>
<h4>
<i class="presence fa fa-circle" title="Active" aria-hidden="true" *ngIf="false && (messageHub.onlineUsers$ | async)?.includes(member.username)"></i>
<i class="presence fa fa-circle" title="Active" aria-hidden="true" *ngIf="false && (messageHub.onlineUsers$ | async)?.includes(member.id)"></i>
<span id="member-name--{{idx}}">{{member.username | titlecase}} </span>
<span *ngIf="member.username === loggedInUsername">
<i class="fas fa-star" aria-hidden="true"></i>

View File

@ -96,7 +96,7 @@
<div class="col-md-12 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="manage-collections" role="switch" formControlName="createCollections" class="form-check-input" aria-labelledby="auto-close-label">
<input type="checkbox" id="manage-collections" role="switch" formControlName="manageCollections" class="form-check-input" aria-labelledby="auto-close-label">
<label class="form-check-label" for="manage-collections">Manage Collections</label>
</div>
</div>

View File

@ -118,7 +118,7 @@ export class LibrarySettingsModalComponent implements OnInit, OnDestroy {
this.libraryForm.get('includeInDashboard')?.setValue(this.library.includeInDashboard);
this.libraryForm.get('includeInRecommended')?.setValue(this.library.includeInRecommended);
this.libraryForm.get('includeInSearch')?.setValue(this.library.includeInSearch);
this.libraryForm.get('createCollections')?.setValue(this.library.createCollections);
this.libraryForm.get('manageCollections')?.setValue(this.library.manageCollections);
this.selectedFolders = this.library.folders;
this.madeChanges = false;
this.cdRef.markForCheck();

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
},
"version": "0.6.1.41"
"version": "0.7.0.2"
},
"servers": [
{
@ -14070,13 +14070,13 @@
},
"UpdateLibraryDto": {
"required": [
"createCollections",
"folders",
"folderWatching",
"id",
"includeInDashboard",
"includeInRecommended",
"includeInSearch",
"manageCollections",
"name",
"type"
],
@ -14111,7 +14111,7 @@
"includeInSearch": {
"type": "boolean"
},
"createCollections": {
"manageCollections": {
"type": "boolean"
}
},