Logging Enhancements (#1521)
* Recreated Kavita Logging with Serilog instead of Default. This needs to be move out of the appsettings now, to allow auto updater to patch. * Refactored the code to be completely configured via Code rather than appsettings.json. This is a required step for Auto Updating. * Added in the ability to send logs directly to the UI only for users on the log route. Stopping implementation as Alerts page will handle the rest of the implementation. * Fixed up the backup service to not rely on Config from appsettings.json * Tweaked the Logging levels available * Moved everything over to File-scoped namespaces * Moved everything over to File-scoped namespaces * Code cleanup, removed an old migration and changed so debug logging doesn't print sensitive db data * Removed dead code
This commit is contained in:
parent
9f715cc35f
commit
d1a14f7e68
212 changed files with 16599 additions and 16834 deletions
|
@ -11,141 +11,140 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
|
||||
namespace API.Data
|
||||
namespace API.Data;
|
||||
|
||||
public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
||||
IdentityUserClaim<int>, AppUserRole, IdentityUserLogin<int>,
|
||||
IdentityRoleClaim<int>, IdentityUserToken<int>>
|
||||
{
|
||||
public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
||||
IdentityUserClaim<int>, AppUserRole, IdentityUserLogin<int>,
|
||||
IdentityRoleClaim<int>, IdentityUserToken<int>>
|
||||
public DataContext(DbContextOptions options) : base(options)
|
||||
{
|
||||
public DataContext(DbContextOptions options) : base(options)
|
||||
{
|
||||
ChangeTracker.Tracked += OnEntityTracked;
|
||||
ChangeTracker.StateChanged += OnEntityStateChanged;
|
||||
}
|
||||
|
||||
public DbSet<Library> Library { get; set; }
|
||||
public DbSet<Series> Series { get; set; }
|
||||
public DbSet<Chapter> Chapter { get; set; }
|
||||
public DbSet<Volume> Volume { get; set; }
|
||||
public DbSet<AppUser> AppUser { get; set; }
|
||||
public DbSet<MangaFile> MangaFile { get; set; }
|
||||
public DbSet<AppUserProgress> AppUserProgresses { get; set; }
|
||||
public DbSet<AppUserRating> AppUserRating { get; set; }
|
||||
public DbSet<ServerSetting> ServerSetting { get; set; }
|
||||
public DbSet<AppUserPreferences> AppUserPreferences { get; set; }
|
||||
public DbSet<SeriesMetadata> SeriesMetadata { get; set; }
|
||||
public DbSet<CollectionTag> CollectionTag { get; set; }
|
||||
public DbSet<AppUserBookmark> AppUserBookmark { get; set; }
|
||||
public DbSet<ReadingList> ReadingList { get; set; }
|
||||
public DbSet<ReadingListItem> ReadingListItem { get; set; }
|
||||
public DbSet<Person> Person { get; set; }
|
||||
public DbSet<Genre> Genre { get; set; }
|
||||
public DbSet<Tag> Tag { get; set; }
|
||||
public DbSet<SiteTheme> SiteTheme { get; set; }
|
||||
public DbSet<SeriesRelation> SeriesRelation { get; set; }
|
||||
public DbSet<FolderPath> FolderPath { get; set; }
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
|
||||
|
||||
builder.Entity<AppUser>()
|
||||
.HasMany(ur => ur.UserRoles)
|
||||
.WithOne(u => u.User)
|
||||
.HasForeignKey(ur => ur.UserId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Entity<AppRole>()
|
||||
.HasMany(ur => ur.UserRoles)
|
||||
.WithOne(u => u.Role)
|
||||
.HasForeignKey(ur => ur.RoleId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Entity<SeriesRelation>()
|
||||
.HasOne(pt => pt.Series)
|
||||
.WithMany(p => p.Relations)
|
||||
.HasForeignKey(pt => pt.SeriesId)
|
||||
.OnDelete(DeleteBehavior.ClientCascade);
|
||||
|
||||
builder.Entity<SeriesRelation>()
|
||||
.HasOne(pt => pt.TargetSeries)
|
||||
.WithMany(t => t.RelationOf)
|
||||
.HasForeignKey(pt => pt.TargetSeriesId)
|
||||
.OnDelete(DeleteBehavior.ClientCascade);
|
||||
|
||||
|
||||
builder.Entity<AppUserPreferences>()
|
||||
.Property(b => b.BookThemeName)
|
||||
.HasDefaultValue("Dark");
|
||||
builder.Entity<AppUserPreferences>()
|
||||
.Property(b => b.BackgroundColor)
|
||||
.HasDefaultValue("#000000");
|
||||
|
||||
builder.Entity<AppUserPreferences>()
|
||||
.Property(b => b.GlobalPageLayoutMode)
|
||||
.HasDefaultValue(PageLayoutMode.Cards);
|
||||
}
|
||||
|
||||
|
||||
private static void OnEntityTracked(object sender, EntityTrackedEventArgs e)
|
||||
{
|
||||
if (!e.FromQuery && e.Entry.State == EntityState.Added && e.Entry.Entity is IEntityDate entity)
|
||||
{
|
||||
entity.Created = DateTime.Now;
|
||||
entity.LastModified = DateTime.Now;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e)
|
||||
{
|
||||
if (e.NewState == EntityState.Modified && e.Entry.Entity is IEntityDate entity)
|
||||
entity.LastModified = DateTime.Now;
|
||||
}
|
||||
|
||||
private void OnSaveChanges()
|
||||
{
|
||||
foreach (var saveEntity in ChangeTracker.Entries()
|
||||
.Where(e => e.State == EntityState.Modified)
|
||||
.Select(entry => entry.Entity)
|
||||
.OfType<IHasConcurrencyToken>())
|
||||
{
|
||||
saveEntity.OnSavingChanges();
|
||||
}
|
||||
}
|
||||
|
||||
#region SaveChanges overrides
|
||||
|
||||
public override int SaveChanges()
|
||||
{
|
||||
this.OnSaveChanges();
|
||||
|
||||
return base.SaveChanges();
|
||||
}
|
||||
|
||||
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
||||
{
|
||||
this.OnSaveChanges();
|
||||
|
||||
return base.SaveChanges(acceptAllChangesOnSuccess);
|
||||
}
|
||||
|
||||
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
this.OnSaveChanges();
|
||||
|
||||
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
|
||||
}
|
||||
|
||||
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
this.OnSaveChanges();
|
||||
|
||||
return base.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
ChangeTracker.Tracked += OnEntityTracked;
|
||||
ChangeTracker.StateChanged += OnEntityStateChanged;
|
||||
}
|
||||
|
||||
public DbSet<Library> Library { get; set; }
|
||||
public DbSet<Series> Series { get; set; }
|
||||
public DbSet<Chapter> Chapter { get; set; }
|
||||
public DbSet<Volume> Volume { get; set; }
|
||||
public DbSet<AppUser> AppUser { get; set; }
|
||||
public DbSet<MangaFile> MangaFile { get; set; }
|
||||
public DbSet<AppUserProgress> AppUserProgresses { get; set; }
|
||||
public DbSet<AppUserRating> AppUserRating { get; set; }
|
||||
public DbSet<ServerSetting> ServerSetting { get; set; }
|
||||
public DbSet<AppUserPreferences> AppUserPreferences { get; set; }
|
||||
public DbSet<SeriesMetadata> SeriesMetadata { get; set; }
|
||||
public DbSet<CollectionTag> CollectionTag { get; set; }
|
||||
public DbSet<AppUserBookmark> AppUserBookmark { get; set; }
|
||||
public DbSet<ReadingList> ReadingList { get; set; }
|
||||
public DbSet<ReadingListItem> ReadingListItem { get; set; }
|
||||
public DbSet<Person> Person { get; set; }
|
||||
public DbSet<Genre> Genre { get; set; }
|
||||
public DbSet<Tag> Tag { get; set; }
|
||||
public DbSet<SiteTheme> SiteTheme { get; set; }
|
||||
public DbSet<SeriesRelation> SeriesRelation { get; set; }
|
||||
public DbSet<FolderPath> FolderPath { get; set; }
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
|
||||
|
||||
builder.Entity<AppUser>()
|
||||
.HasMany(ur => ur.UserRoles)
|
||||
.WithOne(u => u.User)
|
||||
.HasForeignKey(ur => ur.UserId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Entity<AppRole>()
|
||||
.HasMany(ur => ur.UserRoles)
|
||||
.WithOne(u => u.Role)
|
||||
.HasForeignKey(ur => ur.RoleId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Entity<SeriesRelation>()
|
||||
.HasOne(pt => pt.Series)
|
||||
.WithMany(p => p.Relations)
|
||||
.HasForeignKey(pt => pt.SeriesId)
|
||||
.OnDelete(DeleteBehavior.ClientCascade);
|
||||
|
||||
builder.Entity<SeriesRelation>()
|
||||
.HasOne(pt => pt.TargetSeries)
|
||||
.WithMany(t => t.RelationOf)
|
||||
.HasForeignKey(pt => pt.TargetSeriesId)
|
||||
.OnDelete(DeleteBehavior.ClientCascade);
|
||||
|
||||
|
||||
builder.Entity<AppUserPreferences>()
|
||||
.Property(b => b.BookThemeName)
|
||||
.HasDefaultValue("Dark");
|
||||
builder.Entity<AppUserPreferences>()
|
||||
.Property(b => b.BackgroundColor)
|
||||
.HasDefaultValue("#000000");
|
||||
|
||||
builder.Entity<AppUserPreferences>()
|
||||
.Property(b => b.GlobalPageLayoutMode)
|
||||
.HasDefaultValue(PageLayoutMode.Cards);
|
||||
}
|
||||
|
||||
|
||||
private static void OnEntityTracked(object sender, EntityTrackedEventArgs e)
|
||||
{
|
||||
if (!e.FromQuery && e.Entry.State == EntityState.Added && e.Entry.Entity is IEntityDate entity)
|
||||
{
|
||||
entity.Created = DateTime.Now;
|
||||
entity.LastModified = DateTime.Now;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e)
|
||||
{
|
||||
if (e.NewState == EntityState.Modified && e.Entry.Entity is IEntityDate entity)
|
||||
entity.LastModified = DateTime.Now;
|
||||
}
|
||||
|
||||
private void OnSaveChanges()
|
||||
{
|
||||
foreach (var saveEntity in ChangeTracker.Entries()
|
||||
.Where(e => e.State == EntityState.Modified)
|
||||
.Select(entry => entry.Entity)
|
||||
.OfType<IHasConcurrencyToken>())
|
||||
{
|
||||
saveEntity.OnSavingChanges();
|
||||
}
|
||||
}
|
||||
|
||||
#region SaveChanges overrides
|
||||
|
||||
public override int SaveChanges()
|
||||
{
|
||||
this.OnSaveChanges();
|
||||
|
||||
return base.SaveChanges();
|
||||
}
|
||||
|
||||
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
||||
{
|
||||
this.OnSaveChanges();
|
||||
|
||||
return base.SaveChanges(acceptAllChangesOnSuccess);
|
||||
}
|
||||
|
||||
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
this.OnSaveChanges();
|
||||
|
||||
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
|
||||
}
|
||||
|
||||
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
this.OnSaveChanges();
|
||||
|
||||
return base.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
|
@ -9,162 +9,161 @@ using API.Extensions;
|
|||
using API.Parser;
|
||||
using API.Services.Tasks;
|
||||
|
||||
namespace API.Data
|
||||
namespace API.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Responsible for creating Series, Volume, Chapter, MangaFiles for use in <see cref="ScannerService"/>
|
||||
/// </summary>
|
||||
public static class DbFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Responsible for creating Series, Volume, Chapter, MangaFiles for use in <see cref="ScannerService"/>
|
||||
/// </summary>
|
||||
public static class DbFactory
|
||||
public static Series Series(string name)
|
||||
{
|
||||
public static Series Series(string name)
|
||||
return new Series
|
||||
{
|
||||
return new Series
|
||||
{
|
||||
Name = name,
|
||||
OriginalName = name,
|
||||
LocalizedName = name,
|
||||
NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
|
||||
NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
|
||||
SortName = name,
|
||||
Volumes = new List<Volume>(),
|
||||
Metadata = SeriesMetadata(Array.Empty<CollectionTag>())
|
||||
};
|
||||
}
|
||||
|
||||
public static Series Series(string name, string localizedName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(localizedName))
|
||||
{
|
||||
localizedName = name;
|
||||
}
|
||||
return new Series
|
||||
{
|
||||
Name = name,
|
||||
OriginalName = name,
|
||||
LocalizedName = localizedName,
|
||||
NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
|
||||
NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(localizedName),
|
||||
SortName = name,
|
||||
Volumes = new List<Volume>(),
|
||||
Metadata = SeriesMetadata(Array.Empty<CollectionTag>())
|
||||
};
|
||||
}
|
||||
|
||||
public static Volume Volume(string volumeNumber)
|
||||
{
|
||||
return new Volume()
|
||||
{
|
||||
Name = volumeNumber,
|
||||
Number = (int) Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(volumeNumber),
|
||||
Chapters = new List<Chapter>()
|
||||
};
|
||||
}
|
||||
|
||||
public static Chapter Chapter(ParserInfo info)
|
||||
{
|
||||
var specialTreatment = info.IsSpecialInfo();
|
||||
var specialTitle = specialTreatment ? info.Filename : info.Chapters;
|
||||
return new Chapter()
|
||||
{
|
||||
Number = specialTreatment ? "0" : Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(info.Chapters) + string.Empty,
|
||||
Range = specialTreatment ? info.Filename : info.Chapters,
|
||||
Title = (specialTreatment && info.Format == MangaFormat.Epub)
|
||||
? info.Title
|
||||
: specialTitle,
|
||||
Files = new List<MangaFile>(),
|
||||
IsSpecial = specialTreatment,
|
||||
};
|
||||
}
|
||||
|
||||
public static SeriesMetadata SeriesMetadata(ComicInfo info)
|
||||
{
|
||||
return SeriesMetadata(Array.Empty<CollectionTag>());
|
||||
}
|
||||
|
||||
public static SeriesMetadata SeriesMetadata(ICollection<CollectionTag> collectionTags)
|
||||
{
|
||||
return new SeriesMetadata()
|
||||
{
|
||||
CollectionTags = collectionTags,
|
||||
Summary = string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
public static CollectionTag CollectionTag(int id, string title, string summary, bool promoted)
|
||||
{
|
||||
return new CollectionTag()
|
||||
{
|
||||
Id = id,
|
||||
NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(title?.Trim()).ToUpper(),
|
||||
Title = title?.Trim(),
|
||||
Summary = summary?.Trim(),
|
||||
Promoted = promoted
|
||||
};
|
||||
}
|
||||
|
||||
public static ReadingList ReadingList(string title, string summary, bool promoted)
|
||||
{
|
||||
return new ReadingList()
|
||||
{
|
||||
NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(title?.Trim()).ToUpper(),
|
||||
Title = title?.Trim(),
|
||||
Summary = summary?.Trim(),
|
||||
Promoted = promoted,
|
||||
Items = new List<ReadingListItem>()
|
||||
};
|
||||
}
|
||||
|
||||
public static ReadingListItem ReadingListItem(int index, int seriesId, int volumeId, int chapterId)
|
||||
{
|
||||
return new ReadingListItem()
|
||||
{
|
||||
Order = index,
|
||||
ChapterId = chapterId,
|
||||
SeriesId = seriesId,
|
||||
VolumeId = volumeId
|
||||
};
|
||||
}
|
||||
|
||||
public static Genre Genre(string name, bool external)
|
||||
{
|
||||
return new Genre()
|
||||
{
|
||||
Title = name.Trim().SentenceCase(),
|
||||
NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
|
||||
ExternalTag = external
|
||||
};
|
||||
}
|
||||
|
||||
public static Tag Tag(string name, bool external)
|
||||
{
|
||||
return new Tag()
|
||||
{
|
||||
Title = name.Trim().SentenceCase(),
|
||||
NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
|
||||
ExternalTag = external
|
||||
};
|
||||
}
|
||||
|
||||
public static Person Person(string name, PersonRole role)
|
||||
{
|
||||
return new Person()
|
||||
{
|
||||
Name = name.Trim(),
|
||||
NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
|
||||
Role = role
|
||||
};
|
||||
}
|
||||
|
||||
public static MangaFile MangaFile(string filePath, MangaFormat format, int pages)
|
||||
{
|
||||
return new MangaFile()
|
||||
{
|
||||
FilePath = filePath,
|
||||
Format = format,
|
||||
Pages = pages,
|
||||
LastModified = File.GetLastWriteTime(filePath) // NOTE: Changed this from DateTime.Now
|
||||
};
|
||||
}
|
||||
|
||||
Name = name,
|
||||
OriginalName = name,
|
||||
LocalizedName = name,
|
||||
NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
|
||||
NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
|
||||
SortName = name,
|
||||
Volumes = new List<Volume>(),
|
||||
Metadata = SeriesMetadata(Array.Empty<CollectionTag>())
|
||||
};
|
||||
}
|
||||
|
||||
public static Series Series(string name, string localizedName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(localizedName))
|
||||
{
|
||||
localizedName = name;
|
||||
}
|
||||
return new Series
|
||||
{
|
||||
Name = name,
|
||||
OriginalName = name,
|
||||
LocalizedName = localizedName,
|
||||
NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
|
||||
NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(localizedName),
|
||||
SortName = name,
|
||||
Volumes = new List<Volume>(),
|
||||
Metadata = SeriesMetadata(Array.Empty<CollectionTag>())
|
||||
};
|
||||
}
|
||||
|
||||
public static Volume Volume(string volumeNumber)
|
||||
{
|
||||
return new Volume()
|
||||
{
|
||||
Name = volumeNumber,
|
||||
Number = (int) Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(volumeNumber),
|
||||
Chapters = new List<Chapter>()
|
||||
};
|
||||
}
|
||||
|
||||
public static Chapter Chapter(ParserInfo info)
|
||||
{
|
||||
var specialTreatment = info.IsSpecialInfo();
|
||||
var specialTitle = specialTreatment ? info.Filename : info.Chapters;
|
||||
return new Chapter()
|
||||
{
|
||||
Number = specialTreatment ? "0" : Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(info.Chapters) + string.Empty,
|
||||
Range = specialTreatment ? info.Filename : info.Chapters,
|
||||
Title = (specialTreatment && info.Format == MangaFormat.Epub)
|
||||
? info.Title
|
||||
: specialTitle,
|
||||
Files = new List<MangaFile>(),
|
||||
IsSpecial = specialTreatment,
|
||||
};
|
||||
}
|
||||
|
||||
public static SeriesMetadata SeriesMetadata(ComicInfo info)
|
||||
{
|
||||
return SeriesMetadata(Array.Empty<CollectionTag>());
|
||||
}
|
||||
|
||||
public static SeriesMetadata SeriesMetadata(ICollection<CollectionTag> collectionTags)
|
||||
{
|
||||
return new SeriesMetadata()
|
||||
{
|
||||
CollectionTags = collectionTags,
|
||||
Summary = string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
public static CollectionTag CollectionTag(int id, string title, string summary, bool promoted)
|
||||
{
|
||||
return new CollectionTag()
|
||||
{
|
||||
Id = id,
|
||||
NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(title?.Trim()).ToUpper(),
|
||||
Title = title?.Trim(),
|
||||
Summary = summary?.Trim(),
|
||||
Promoted = promoted
|
||||
};
|
||||
}
|
||||
|
||||
public static ReadingList ReadingList(string title, string summary, bool promoted)
|
||||
{
|
||||
return new ReadingList()
|
||||
{
|
||||
NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(title?.Trim()).ToUpper(),
|
||||
Title = title?.Trim(),
|
||||
Summary = summary?.Trim(),
|
||||
Promoted = promoted,
|
||||
Items = new List<ReadingListItem>()
|
||||
};
|
||||
}
|
||||
|
||||
public static ReadingListItem ReadingListItem(int index, int seriesId, int volumeId, int chapterId)
|
||||
{
|
||||
return new ReadingListItem()
|
||||
{
|
||||
Order = index,
|
||||
ChapterId = chapterId,
|
||||
SeriesId = seriesId,
|
||||
VolumeId = volumeId
|
||||
};
|
||||
}
|
||||
|
||||
public static Genre Genre(string name, bool external)
|
||||
{
|
||||
return new Genre()
|
||||
{
|
||||
Title = name.Trim().SentenceCase(),
|
||||
NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
|
||||
ExternalTag = external
|
||||
};
|
||||
}
|
||||
|
||||
public static Tag Tag(string name, bool external)
|
||||
{
|
||||
return new Tag()
|
||||
{
|
||||
Title = name.Trim().SentenceCase(),
|
||||
NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
|
||||
ExternalTag = external
|
||||
};
|
||||
}
|
||||
|
||||
public static Person Person(string name, PersonRole role)
|
||||
{
|
||||
return new Person()
|
||||
{
|
||||
Name = name.Trim(),
|
||||
NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
|
||||
Role = role
|
||||
};
|
||||
}
|
||||
|
||||
public static MangaFile MangaFile(string filePath, MangaFormat format, int pages)
|
||||
{
|
||||
return new MangaFile()
|
||||
{
|
||||
FilePath = filePath,
|
||||
Format = format,
|
||||
Pages = pages,
|
||||
LastModified = File.GetLastWriteTime(filePath) // NOTE: Changed this from DateTime.Now
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
namespace API.Data
|
||||
{
|
||||
public class LogLevelOptions
|
||||
{
|
||||
public const string Logging = "LogLevel";
|
||||
|
||||
public string Default { get; set; }
|
||||
}
|
||||
}
|
|
@ -3,122 +3,121 @@ using System.Linq;
|
|||
using API.Entities.Enums;
|
||||
using Kavita.Common.Extensions;
|
||||
|
||||
namespace API.Data.Metadata
|
||||
namespace API.Data.Metadata;
|
||||
|
||||
/// <summary>
|
||||
/// A representation of a ComicInfo.xml file
|
||||
/// </summary>
|
||||
/// <remarks>See reference of the loose spec here: https://anansi-project.github.io/docs/comicinfo/documentation</remarks>
|
||||
public class ComicInfo
|
||||
{
|
||||
public string Summary { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Series { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// A representation of a ComicInfo.xml file
|
||||
/// Localized Series name. Not standard.
|
||||
/// </summary>
|
||||
/// <remarks>See reference of the loose spec here: https://anansi-project.github.io/docs/comicinfo/documentation</remarks>
|
||||
public class ComicInfo
|
||||
public string LocalizedSeries { get; set; } = string.Empty;
|
||||
public string SeriesSort { get; set; } = string.Empty;
|
||||
public string Number { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// The total number of items in the series.
|
||||
/// </summary>
|
||||
public int Count { get; set; } = 0;
|
||||
public string Volume { get; set; } = string.Empty;
|
||||
public string Notes { get; set; } = string.Empty;
|
||||
public string Genre { get; set; } = string.Empty;
|
||||
public int PageCount { get; set; }
|
||||
// ReSharper disable once InconsistentNaming
|
||||
/// <summary>
|
||||
/// IETF BCP 47 Code to represent the language of the content
|
||||
/// </summary>
|
||||
public string LanguageISO { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// This is the link to where the data was scraped from
|
||||
/// </summary>
|
||||
public string Web { get; set; } = string.Empty;
|
||||
public int Day { get; set; } = 0;
|
||||
public int Month { get; set; } = 0;
|
||||
public int Year { get; set; } = 0;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Rating based on the content. Think PG-13, R for movies. See <see cref="AgeRating"/> for valid types
|
||||
/// </summary>
|
||||
public string AgeRating { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// User's rating of the content
|
||||
/// </summary>
|
||||
public float UserRating { get; set; }
|
||||
|
||||
public string StoryArc { get; set; } = string.Empty;
|
||||
public string SeriesGroup { get; set; } = string.Empty;
|
||||
public string AlternateNumber { get; set; } = string.Empty;
|
||||
public int AlternateCount { get; set; } = 0;
|
||||
public string AlternateSeries { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// This is Epub only: calibre:title_sort
|
||||
/// Represents the sort order for the title
|
||||
/// </summary>
|
||||
public string TitleSort { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// This comes from ComicInfo and is free form text. We use this to validate against a set of tags and mark a file as
|
||||
/// special.
|
||||
/// </summary>
|
||||
public string Format { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The translator, can be comma separated. This is part of ComicInfo.xml draft v2.1
|
||||
/// </summary>
|
||||
/// See https://github.com/anansi-project/comicinfo/issues/2 for information about this tag
|
||||
public string Translator { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Misc tags. This is part of ComicInfo.xml draft v2.1
|
||||
/// </summary>
|
||||
/// See https://github.com/anansi-project/comicinfo/issues/1 for information about this tag
|
||||
public string Tags { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// This is the Author. For Books, we map creator tag in OPF to this field. Comma separated if multiple.
|
||||
/// </summary>
|
||||
public string Writer { get; set; } = string.Empty;
|
||||
public string Penciller { get; set; } = string.Empty;
|
||||
public string Inker { get; set; } = string.Empty;
|
||||
public string Colorist { get; set; } = string.Empty;
|
||||
public string Letterer { get; set; } = string.Empty;
|
||||
public string CoverArtist { get; set; } = string.Empty;
|
||||
public string Editor { get; set; } = string.Empty;
|
||||
public string Publisher { get; set; } = string.Empty;
|
||||
public string Characters { get; set; } = string.Empty;
|
||||
|
||||
public static AgeRating ConvertAgeRatingToEnum(string value)
|
||||
{
|
||||
public string Summary { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Series { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Localized Series name. Not standard.
|
||||
/// </summary>
|
||||
public string LocalizedSeries { get; set; } = string.Empty;
|
||||
public string SeriesSort { get; set; } = string.Empty;
|
||||
public string Number { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// The total number of items in the series.
|
||||
/// </summary>
|
||||
public int Count { get; set; } = 0;
|
||||
public string Volume { get; set; } = string.Empty;
|
||||
public string Notes { get; set; } = string.Empty;
|
||||
public string Genre { get; set; } = string.Empty;
|
||||
public int PageCount { get; set; }
|
||||
// ReSharper disable once InconsistentNaming
|
||||
/// <summary>
|
||||
/// IETF BCP 47 Code to represent the language of the content
|
||||
/// </summary>
|
||||
public string LanguageISO { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// This is the link to where the data was scraped from
|
||||
/// </summary>
|
||||
public string Web { get; set; } = string.Empty;
|
||||
public int Day { get; set; } = 0;
|
||||
public int Month { get; set; } = 0;
|
||||
public int Year { get; set; } = 0;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Rating based on the content. Think PG-13, R for movies. See <see cref="AgeRating"/> for valid types
|
||||
/// </summary>
|
||||
public string AgeRating { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// User's rating of the content
|
||||
/// </summary>
|
||||
public float UserRating { get; set; }
|
||||
|
||||
public string StoryArc { get; set; } = string.Empty;
|
||||
public string SeriesGroup { get; set; } = string.Empty;
|
||||
public string AlternateNumber { get; set; } = string.Empty;
|
||||
public int AlternateCount { get; set; } = 0;
|
||||
public string AlternateSeries { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// This is Epub only: calibre:title_sort
|
||||
/// Represents the sort order for the title
|
||||
/// </summary>
|
||||
public string TitleSort { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// This comes from ComicInfo and is free form text. We use this to validate against a set of tags and mark a file as
|
||||
/// special.
|
||||
/// </summary>
|
||||
public string Format { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The translator, can be comma separated. This is part of ComicInfo.xml draft v2.1
|
||||
/// </summary>
|
||||
/// See https://github.com/anansi-project/comicinfo/issues/2 for information about this tag
|
||||
public string Translator { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Misc tags. This is part of ComicInfo.xml draft v2.1
|
||||
/// </summary>
|
||||
/// See https://github.com/anansi-project/comicinfo/issues/1 for information about this tag
|
||||
public string Tags { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// This is the Author. For Books, we map creator tag in OPF to this field. Comma separated if multiple.
|
||||
/// </summary>
|
||||
public string Writer { get; set; } = string.Empty;
|
||||
public string Penciller { get; set; } = string.Empty;
|
||||
public string Inker { get; set; } = string.Empty;
|
||||
public string Colorist { get; set; } = string.Empty;
|
||||
public string Letterer { get; set; } = string.Empty;
|
||||
public string CoverArtist { get; set; } = string.Empty;
|
||||
public string Editor { get; set; } = string.Empty;
|
||||
public string Publisher { get; set; } = string.Empty;
|
||||
public string Characters { get; set; } = string.Empty;
|
||||
|
||||
public static AgeRating ConvertAgeRatingToEnum(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return Entities.Enums.AgeRating.Unknown;
|
||||
return Enum.GetValues<AgeRating>()
|
||||
.SingleOrDefault(t => t.ToDescription().ToUpperInvariant().Equals(value.ToUpperInvariant()), Entities.Enums.AgeRating.Unknown);
|
||||
}
|
||||
|
||||
public static void CleanComicInfo(ComicInfo info)
|
||||
{
|
||||
if (info == null) return;
|
||||
|
||||
info.Series = info.Series.Trim();
|
||||
info.SeriesSort = info.SeriesSort.Trim();
|
||||
info.LocalizedSeries = info.LocalizedSeries.Trim();
|
||||
|
||||
info.Writer = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Writer);
|
||||
info.Colorist = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Colorist);
|
||||
info.Editor = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Editor);
|
||||
info.Inker = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Inker);
|
||||
info.Letterer = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Letterer);
|
||||
info.Penciller = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Penciller);
|
||||
info.Publisher = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Publisher);
|
||||
info.Characters = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Characters);
|
||||
info.Translator = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Translator);
|
||||
info.CoverArtist = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.CoverArtist);
|
||||
}
|
||||
|
||||
|
||||
if (string.IsNullOrEmpty(value)) return Entities.Enums.AgeRating.Unknown;
|
||||
return Enum.GetValues<AgeRating>()
|
||||
.SingleOrDefault(t => t.ToDescription().ToUpperInvariant().Equals(value.ToUpperInvariant()), Entities.Enums.AgeRating.Unknown);
|
||||
}
|
||||
|
||||
public static void CleanComicInfo(ComicInfo info)
|
||||
{
|
||||
if (info == null) return;
|
||||
|
||||
info.Series = info.Series.Trim();
|
||||
info.SeriesSort = info.SeriesSort.Trim();
|
||||
info.LocalizedSeries = info.LocalizedSeries.Trim();
|
||||
|
||||
info.Writer = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Writer);
|
||||
info.Colorist = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Colorist);
|
||||
info.Editor = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Editor);
|
||||
info.Inker = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Inker);
|
||||
info.Letterer = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Letterer);
|
||||
info.Penciller = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Penciller);
|
||||
info.Publisher = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Publisher);
|
||||
info.Characters = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Characters);
|
||||
info.Translator = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Translator);
|
||||
info.CoverArtist = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.CoverArtist);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,168 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using API.Services;
|
||||
using Kavita.Common;
|
||||
|
||||
namespace API.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// A Migration to migrate config related files to the config/ directory for installs prior to v0.4.9.
|
||||
/// </summary>
|
||||
public static class MigrateConfigFiles
|
||||
{
|
||||
private static readonly List<string> LooseLeafFiles = new List<string>()
|
||||
{
|
||||
"appsettings.json",
|
||||
"appsettings.Development.json",
|
||||
"kavita.db",
|
||||
};
|
||||
|
||||
private static readonly List<string> AppFolders = new List<string>()
|
||||
{
|
||||
"covers",
|
||||
"stats",
|
||||
"logs",
|
||||
"backups",
|
||||
"cache",
|
||||
"temp"
|
||||
};
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// In v0.4.8 we moved all config files to config/ to match with how docker was setup. This will move all config files from current directory
|
||||
/// to config/
|
||||
/// </summary>
|
||||
public static void Migrate(bool isDocker, IDirectoryService directoryService)
|
||||
{
|
||||
Console.WriteLine("Checking if migration to config/ is needed");
|
||||
|
||||
if (isDocker)
|
||||
{
|
||||
if (Configuration.LogPath.Contains("config"))
|
||||
{
|
||||
Console.WriteLine("Migration to config/ not needed");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
"Migrating files from pre-v0.4.8. All Kavita config files are now located in config/");
|
||||
|
||||
CopyAppFolders(directoryService);
|
||||
DeleteAppFolders(directoryService);
|
||||
|
||||
UpdateConfiguration();
|
||||
|
||||
Console.WriteLine("Migration complete. All config files are now in config/ directory");
|
||||
return;
|
||||
}
|
||||
|
||||
if (new FileInfo(Configuration.AppSettingsFilename).Exists)
|
||||
{
|
||||
Console.WriteLine("Migration to config/ not needed");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
"Migrating files from pre-v0.4.8. All Kavita config files are now located in config/");
|
||||
|
||||
Console.WriteLine($"Creating {directoryService.ConfigDirectory}");
|
||||
directoryService.ExistOrCreate(directoryService.ConfigDirectory);
|
||||
|
||||
try
|
||||
{
|
||||
CopyLooseLeafFiles(directoryService);
|
||||
|
||||
CopyAppFolders(directoryService);
|
||||
|
||||
// Then we need to update the config file to point to the new DB file
|
||||
UpdateConfiguration();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Console.WriteLine("There was an exception during migration. Please move everything manually.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Finally delete everything in the source directory
|
||||
Console.WriteLine("Removing old files");
|
||||
DeleteLooseFiles(directoryService);
|
||||
DeleteAppFolders(directoryService);
|
||||
Console.WriteLine("Removing old files...DONE");
|
||||
|
||||
Console.WriteLine("Migration complete. All config files are now in config/ directory");
|
||||
}
|
||||
|
||||
private static void DeleteAppFolders(IDirectoryService directoryService)
|
||||
{
|
||||
foreach (var folderToDelete in AppFolders)
|
||||
{
|
||||
if (!new DirectoryInfo(Path.Join(Directory.GetCurrentDirectory(), folderToDelete)).Exists) continue;
|
||||
|
||||
directoryService.ClearAndDeleteDirectory(Path.Join(Directory.GetCurrentDirectory(), folderToDelete));
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteLooseFiles(IDirectoryService directoryService)
|
||||
{
|
||||
var configFiles = LooseLeafFiles.Select(file => new FileInfo(Path.Join(Directory.GetCurrentDirectory(), file)))
|
||||
.Where(f => f.Exists);
|
||||
directoryService.DeleteFiles(configFiles.Select(f => f.FullName));
|
||||
}
|
||||
|
||||
private static void CopyAppFolders(IDirectoryService directoryService)
|
||||
{
|
||||
Console.WriteLine("Moving folders to config");
|
||||
|
||||
foreach (var folderToMove in AppFolders)
|
||||
{
|
||||
if (new DirectoryInfo(Path.Join(directoryService.ConfigDirectory, folderToMove)).Exists) continue;
|
||||
|
||||
try
|
||||
{
|
||||
directoryService.CopyDirectoryToDirectory(
|
||||
Path.Join(directoryService.FileSystem.Directory.GetCurrentDirectory(), folderToMove),
|
||||
Path.Join(directoryService.ConfigDirectory, folderToMove));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
/* Swallow Exception */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Console.WriteLine("Moving folders to config...DONE");
|
||||
}
|
||||
|
||||
private static void CopyLooseLeafFiles(IDirectoryService directoryService)
|
||||
{
|
||||
var configFiles = LooseLeafFiles.Select(file => new FileInfo(Path.Join(directoryService.FileSystem.Directory.GetCurrentDirectory(), file)))
|
||||
.Where(f => f.Exists);
|
||||
// First step is to move all the files
|
||||
Console.WriteLine("Moving files to config/");
|
||||
foreach (var fileInfo in configFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
fileInfo.CopyTo(Path.Join(directoryService.ConfigDirectory, fileInfo.Name));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
/* Swallow exception when already exists */
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Moving files to config...DONE");
|
||||
}
|
||||
|
||||
private static void UpdateConfiguration()
|
||||
{
|
||||
Console.WriteLine("Updating appsettings.json to new paths");
|
||||
Configuration.DatabasePath = "config//kavita.db";
|
||||
Configuration.LogPath = "config//logs/kavita.log";
|
||||
Console.WriteLine("Updating appsettings.json to new paths...DONE");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,176 +7,175 @@ using API.Helpers;
|
|||
using API.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data
|
||||
namespace API.Data;
|
||||
|
||||
/// <summary>
|
||||
/// A data structure to migrate Cover Images from byte[] to files.
|
||||
/// </summary>
|
||||
internal class CoverMigration
|
||||
{
|
||||
/// <summary>
|
||||
/// A data structure to migrate Cover Images from byte[] to files.
|
||||
/// </summary>
|
||||
internal class CoverMigration
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public byte[] CoverImage { get; set; }
|
||||
public string ParentId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In v0.4.6, Cover Images were migrated from byte[] in the DB to external files. This migration handles that work.
|
||||
/// </summary>
|
||||
public static class MigrateCoverImages
|
||||
{
|
||||
private static readonly ChapterSortComparerZeroFirst ChapterSortComparerForInChapterSorting = new ();
|
||||
|
||||
/// <summary>
|
||||
/// Run first. Will extract byte[]s from DB and write them to the cover directory.
|
||||
/// </summary>
|
||||
public static void ExtractToImages(DbContext context, IDirectoryService directoryService, IImageService imageService)
|
||||
{
|
||||
Console.WriteLine("Migrating Cover Images to disk. Expect delay.");
|
||||
directoryService.ExistOrCreate(directoryService.CoverImageDirectory);
|
||||
|
||||
Console.WriteLine("Extracting cover images for Series");
|
||||
var lockedSeries = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage From Series Where CoverImage IS NOT NULL", x =>
|
||||
new CoverMigration()
|
||||
{
|
||||
Id = x[0] + string.Empty,
|
||||
CoverImage = (byte[]) x[1],
|
||||
ParentId = "0"
|
||||
});
|
||||
foreach (var series in lockedSeries)
|
||||
{
|
||||
if (series.CoverImage == null || !series.CoverImage.Any()) continue;
|
||||
if (File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetSeriesFormat(int.Parse(series.Id))}.png"))) continue;
|
||||
|
||||
try
|
||||
{
|
||||
var stream = new MemoryStream(series.CoverImage);
|
||||
stream.Position = 0;
|
||||
imageService.WriteCoverThumbnail(stream, ImageService.GetSeriesFormat(int.Parse(series.Id)), directoryService.CoverImageDirectory);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Extracting cover images for Chapters");
|
||||
var chapters = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage, VolumeId From Chapter Where CoverImage IS NOT NULL;", x =>
|
||||
new CoverMigration()
|
||||
{
|
||||
Id = x[0] + string.Empty,
|
||||
CoverImage = (byte[]) x[1],
|
||||
ParentId = x[2] + string.Empty
|
||||
});
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
if (chapter.CoverImage == null || !chapter.CoverImage.Any()) continue;
|
||||
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetChapterFormat(int.Parse(chapter.Id), int.Parse(chapter.ParentId))}.png"))) continue;
|
||||
|
||||
try
|
||||
{
|
||||
var stream = new MemoryStream(chapter.CoverImage);
|
||||
stream.Position = 0;
|
||||
imageService.WriteCoverThumbnail(stream, $"{ImageService.GetChapterFormat(int.Parse(chapter.Id), int.Parse(chapter.ParentId))}", directoryService.CoverImageDirectory);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Extracting cover images for Collection Tags");
|
||||
var tags = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage From CollectionTag Where CoverImage IS NOT NULL;", x =>
|
||||
new CoverMigration()
|
||||
{
|
||||
Id = x[0] + string.Empty,
|
||||
CoverImage = (byte[]) x[1] ,
|
||||
ParentId = "0"
|
||||
});
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
if (tag.CoverImage == null || !tag.CoverImage.Any()) continue;
|
||||
if (directoryService.FileSystem.File.Exists(Path.Join(directoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetCollectionTagFormat(int.Parse(tag.Id))}.png"))) continue;
|
||||
try
|
||||
{
|
||||
var stream = new MemoryStream(tag.CoverImage);
|
||||
stream.Position = 0;
|
||||
imageService.WriteCoverThumbnail(stream, $"{ImageService.GetCollectionTagFormat(int.Parse(tag.Id))}", directoryService.CoverImageDirectory);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run after <see cref="ExtractToImages"/>. Will update the DB with names of files that were extracted.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
public static async Task UpdateDatabaseWithImages(DataContext context, IDirectoryService directoryService)
|
||||
{
|
||||
Console.WriteLine("Updating Series entities");
|
||||
var seriesCovers = await context.Series.Where(s => !string.IsNullOrEmpty(s.CoverImage)).ToListAsync();
|
||||
foreach (var series in seriesCovers)
|
||||
{
|
||||
if (!directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetSeriesFormat(series.Id)}.png"))) continue;
|
||||
series.CoverImage = $"{ImageService.GetSeriesFormat(series.Id)}.png";
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("Updating Chapter entities");
|
||||
var chapters = await context.Chapter.ToListAsync();
|
||||
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId)}.png")))
|
||||
{
|
||||
chapter.CoverImage = $"{ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId)}.png";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("Updating Volume entities");
|
||||
var volumes = await context.Volume.Include(v => v.Chapters).ToListAsync();
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
var firstChapter = volume.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerForInChapterSorting);
|
||||
if (firstChapter == null) continue;
|
||||
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetChapterFormat(firstChapter.Id, firstChapter.VolumeId)}.png")))
|
||||
{
|
||||
volume.CoverImage = $"{ImageService.GetChapterFormat(firstChapter.Id, firstChapter.VolumeId)}.png";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("Updating Collection Tag entities");
|
||||
var tags = await context.CollectionTag.ToListAsync();
|
||||
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetCollectionTagFormat(tag.Id)}.png")))
|
||||
{
|
||||
tag.CoverImage = $"{ImageService.GetCollectionTagFormat(tag.Id)}.png";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("Cover Image Migration completed");
|
||||
}
|
||||
|
||||
}
|
||||
public string Id { get; set; }
|
||||
public byte[] CoverImage { get; set; }
|
||||
public string ParentId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In v0.4.6, Cover Images were migrated from byte[] in the DB to external files. This migration handles that work.
|
||||
/// </summary>
|
||||
public static class MigrateCoverImages
|
||||
{
|
||||
private static readonly ChapterSortComparerZeroFirst ChapterSortComparerForInChapterSorting = new ();
|
||||
|
||||
/// <summary>
|
||||
/// Run first. Will extract byte[]s from DB and write them to the cover directory.
|
||||
/// </summary>
|
||||
public static void ExtractToImages(DbContext context, IDirectoryService directoryService, IImageService imageService)
|
||||
{
|
||||
Console.WriteLine("Migrating Cover Images to disk. Expect delay.");
|
||||
directoryService.ExistOrCreate(directoryService.CoverImageDirectory);
|
||||
|
||||
Console.WriteLine("Extracting cover images for Series");
|
||||
var lockedSeries = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage From Series Where CoverImage IS NOT NULL", x =>
|
||||
new CoverMigration()
|
||||
{
|
||||
Id = x[0] + string.Empty,
|
||||
CoverImage = (byte[]) x[1],
|
||||
ParentId = "0"
|
||||
});
|
||||
foreach (var series in lockedSeries)
|
||||
{
|
||||
if (series.CoverImage == null || !series.CoverImage.Any()) continue;
|
||||
if (File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetSeriesFormat(int.Parse(series.Id))}.png"))) continue;
|
||||
|
||||
try
|
||||
{
|
||||
var stream = new MemoryStream(series.CoverImage);
|
||||
stream.Position = 0;
|
||||
imageService.WriteCoverThumbnail(stream, ImageService.GetSeriesFormat(int.Parse(series.Id)), directoryService.CoverImageDirectory);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Extracting cover images for Chapters");
|
||||
var chapters = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage, VolumeId From Chapter Where CoverImage IS NOT NULL;", x =>
|
||||
new CoverMigration()
|
||||
{
|
||||
Id = x[0] + string.Empty,
|
||||
CoverImage = (byte[]) x[1],
|
||||
ParentId = x[2] + string.Empty
|
||||
});
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
if (chapter.CoverImage == null || !chapter.CoverImage.Any()) continue;
|
||||
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetChapterFormat(int.Parse(chapter.Id), int.Parse(chapter.ParentId))}.png"))) continue;
|
||||
|
||||
try
|
||||
{
|
||||
var stream = new MemoryStream(chapter.CoverImage);
|
||||
stream.Position = 0;
|
||||
imageService.WriteCoverThumbnail(stream, $"{ImageService.GetChapterFormat(int.Parse(chapter.Id), int.Parse(chapter.ParentId))}", directoryService.CoverImageDirectory);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Extracting cover images for Collection Tags");
|
||||
var tags = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage From CollectionTag Where CoverImage IS NOT NULL;", x =>
|
||||
new CoverMigration()
|
||||
{
|
||||
Id = x[0] + string.Empty,
|
||||
CoverImage = (byte[]) x[1] ,
|
||||
ParentId = "0"
|
||||
});
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
if (tag.CoverImage == null || !tag.CoverImage.Any()) continue;
|
||||
if (directoryService.FileSystem.File.Exists(Path.Join(directoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetCollectionTagFormat(int.Parse(tag.Id))}.png"))) continue;
|
||||
try
|
||||
{
|
||||
var stream = new MemoryStream(tag.CoverImage);
|
||||
stream.Position = 0;
|
||||
imageService.WriteCoverThumbnail(stream, $"{ImageService.GetCollectionTagFormat(int.Parse(tag.Id))}", directoryService.CoverImageDirectory);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run after <see cref="ExtractToImages"/>. Will update the DB with names of files that were extracted.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
public static async Task UpdateDatabaseWithImages(DataContext context, IDirectoryService directoryService)
|
||||
{
|
||||
Console.WriteLine("Updating Series entities");
|
||||
var seriesCovers = await context.Series.Where(s => !string.IsNullOrEmpty(s.CoverImage)).ToListAsync();
|
||||
foreach (var series in seriesCovers)
|
||||
{
|
||||
if (!directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetSeriesFormat(series.Id)}.png"))) continue;
|
||||
series.CoverImage = $"{ImageService.GetSeriesFormat(series.Id)}.png";
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("Updating Chapter entities");
|
||||
var chapters = await context.Chapter.ToListAsync();
|
||||
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId)}.png")))
|
||||
{
|
||||
chapter.CoverImage = $"{ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId)}.png";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("Updating Volume entities");
|
||||
var volumes = await context.Volume.Include(v => v.Chapters).ToListAsync();
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
var firstChapter = volume.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerForInChapterSorting);
|
||||
if (firstChapter == null) continue;
|
||||
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetChapterFormat(firstChapter.Id, firstChapter.VolumeId)}.png")))
|
||||
{
|
||||
volume.CoverImage = $"{ImageService.GetChapterFormat(firstChapter.Id, firstChapter.VolumeId)}.png";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("Updating Collection Tag entities");
|
||||
var tags = await context.CollectionTag.ToListAsync();
|
||||
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetCollectionTagFormat(tag.Id)}.png")))
|
||||
{
|
||||
tag.CoverImage = $"{ImageService.GetCollectionTagFormat(tag.Id)}.png";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("Cover Image Migration completed");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
namespace API.Data.Scanner
|
||||
namespace API.Data.Scanner;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a set of Entities which is broken up and iterated on
|
||||
/// </summary>
|
||||
public class Chunk
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a set of Entities which is broken up and iterated on
|
||||
/// Total number of entities
|
||||
/// </summary>
|
||||
public class Chunk
|
||||
{
|
||||
/// <summary>
|
||||
/// Total number of entities
|
||||
/// </summary>
|
||||
public int TotalSize { get; set; }
|
||||
/// <summary>
|
||||
/// Size of each chunk to iterate over
|
||||
/// </summary>
|
||||
public int ChunkSize { get; set; }
|
||||
/// <summary>
|
||||
/// Total chunks to iterate over
|
||||
/// </summary>
|
||||
public int TotalChunks { get; set; }
|
||||
}
|
||||
public int TotalSize { get; set; }
|
||||
/// <summary>
|
||||
/// Size of each chunk to iterate over
|
||||
/// </summary>
|
||||
public int ChunkSize { get; set; }
|
||||
/// <summary>
|
||||
/// Total chunks to iterate over
|
||||
/// </summary>
|
||||
public int TotalChunks { get; set; }
|
||||
}
|
||||
|
|
239
API/Data/Seed.cs
239
API/Data/Seed.cs
|
@ -14,133 +14,130 @@ using Kavita.Common.EnvironmentInfo;
|
|||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data
|
||||
namespace API.Data;
|
||||
|
||||
public static class Seed
|
||||
{
|
||||
public static class Seed
|
||||
/// <summary>
|
||||
/// Generated on Startup. Seed.SeedSettings must run before
|
||||
/// </summary>
|
||||
public static ImmutableArray<ServerSetting> DefaultSettings;
|
||||
|
||||
public static readonly ImmutableArray<SiteTheme> DefaultThemes = ImmutableArray.Create(
|
||||
new List<SiteTheme>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = "Dark",
|
||||
NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize("Dark"),
|
||||
Provider = ThemeProvider.System,
|
||||
FileName = "dark.scss",
|
||||
IsDefault = true,
|
||||
}
|
||||
}.ToArray());
|
||||
|
||||
public static async Task SeedRoles(RoleManager<AppRole> roleManager)
|
||||
{
|
||||
/// <summary>
|
||||
/// Generated on Startup. Seed.SeedSettings must run before
|
||||
/// </summary>
|
||||
public static ImmutableArray<ServerSetting> DefaultSettings;
|
||||
var roles = typeof(PolicyConstants)
|
||||
.GetFields(BindingFlags.Public | BindingFlags.Static)
|
||||
.Where(f => f.FieldType == typeof(string))
|
||||
.ToDictionary(f => f.Name,
|
||||
f => (string) f.GetValue(null)).Values
|
||||
.Select(policyName => new AppRole() {Name = policyName})
|
||||
.ToList();
|
||||
|
||||
public static readonly ImmutableArray<SiteTheme> DefaultThemes = ImmutableArray.Create(
|
||||
new List<SiteTheme>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = "Dark",
|
||||
NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize("Dark"),
|
||||
Provider = ThemeProvider.System,
|
||||
FileName = "dark.scss",
|
||||
IsDefault = true,
|
||||
}
|
||||
}.ToArray());
|
||||
|
||||
public static async Task SeedRoles(RoleManager<AppRole> roleManager)
|
||||
foreach (var role in roles)
|
||||
{
|
||||
var roles = typeof(PolicyConstants)
|
||||
.GetFields(BindingFlags.Public | BindingFlags.Static)
|
||||
.Where(f => f.FieldType == typeof(string))
|
||||
.ToDictionary(f => f.Name,
|
||||
f => (string) f.GetValue(null)).Values
|
||||
.Select(policyName => new AppRole() {Name = policyName})
|
||||
.ToList();
|
||||
|
||||
foreach (var role in roles)
|
||||
var exists = await roleManager.RoleExistsAsync(role.Name);
|
||||
if (!exists)
|
||||
{
|
||||
var exists = await roleManager.RoleExistsAsync(role.Name);
|
||||
if (!exists)
|
||||
{
|
||||
await roleManager.CreateAsync(role);
|
||||
}
|
||||
await roleManager.CreateAsync(role);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SeedThemes(DataContext context)
|
||||
{
|
||||
await context.Database.EnsureCreatedAsync();
|
||||
|
||||
foreach (var theme in DefaultThemes)
|
||||
{
|
||||
var existing = context.SiteTheme.FirstOrDefault(s => s.Name.Equals(theme.Name));
|
||||
if (existing == null)
|
||||
{
|
||||
await context.SiteTheme.AddAsync(theme);
|
||||
}
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public static async Task SeedSettings(DataContext context, IDirectoryService directoryService)
|
||||
{
|
||||
await context.Database.EnsureCreatedAsync();
|
||||
DefaultSettings = ImmutableArray.Create(new List<ServerSetting>()
|
||||
{
|
||||
new() {Key = ServerSettingKey.CacheDirectory, Value = directoryService.CacheDirectory},
|
||||
new() {Key = ServerSettingKey.TaskScan, Value = "daily"},
|
||||
new()
|
||||
{
|
||||
Key = ServerSettingKey.LoggingLevel, Value = "Information"
|
||||
}, // Not used from DB, but DB is sync with appSettings.json
|
||||
new() {Key = ServerSettingKey.TaskBackup, Value = "daily"},
|
||||
new()
|
||||
{
|
||||
Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(DirectoryService.BackupDirectory)
|
||||
},
|
||||
new()
|
||||
{
|
||||
Key = ServerSettingKey.Port, Value = "5000"
|
||||
}, // Not used from DB, but DB is sync with appSettings.json
|
||||
new() {Key = ServerSettingKey.AllowStatCollection, Value = "true"},
|
||||
new() {Key = ServerSettingKey.EnableOpds, Value = "false"},
|
||||
new() {Key = ServerSettingKey.EnableAuthentication, Value = "true"},
|
||||
new() {Key = ServerSettingKey.BaseUrl, Value = "/"},
|
||||
new() {Key = ServerSettingKey.InstallId, Value = HashUtil.AnonymousToken()},
|
||||
new() {Key = ServerSettingKey.InstallVersion, Value = BuildInfo.Version.ToString()},
|
||||
new() {Key = ServerSettingKey.BookmarkDirectory, Value = directoryService.BookmarkDirectory},
|
||||
new() {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl},
|
||||
new() {Key = ServerSettingKey.ConvertBookmarkToWebP, Value = "false"},
|
||||
new() {Key = ServerSettingKey.EnableSwaggerUi, Value = "false"},
|
||||
new() {Key = ServerSettingKey.TotalBackups, Value = "30"},
|
||||
new() {Key = ServerSettingKey.EnableFolderWatching, Value = "false"},
|
||||
}.ToArray());
|
||||
|
||||
foreach (var defaultSetting in DefaultSettings)
|
||||
{
|
||||
var existing = context.ServerSetting.FirstOrDefault(s => s.Key == defaultSetting.Key);
|
||||
if (existing == null)
|
||||
{
|
||||
await context.ServerSetting.AddAsync(defaultSetting);
|
||||
}
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Port and LoggingLevel are managed in appSettings.json. Update the DB values to match
|
||||
context.ServerSetting.First(s => s.Key == ServerSettingKey.Port).Value =
|
||||
Configuration.Port + string.Empty;
|
||||
context.ServerSetting.First(s => s.Key == ServerSettingKey.LoggingLevel).Value =
|
||||
Configuration.LogLevel + string.Empty;
|
||||
context.ServerSetting.First(s => s.Key == ServerSettingKey.CacheDirectory).Value =
|
||||
directoryService.CacheDirectory + string.Empty;
|
||||
context.ServerSetting.First(s => s.Key == ServerSettingKey.BackupDirectory).Value =
|
||||
DirectoryService.BackupDirectory + string.Empty;
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
}
|
||||
|
||||
public static async Task SeedUserApiKeys(DataContext context)
|
||||
{
|
||||
await context.Database.EnsureCreatedAsync();
|
||||
|
||||
var users = await context.AppUser.ToListAsync();
|
||||
foreach (var user in users.Where(user => string.IsNullOrEmpty(user.ApiKey)))
|
||||
{
|
||||
user.ApiKey = HashUtil.ApiKey();
|
||||
}
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SeedThemes(DataContext context)
|
||||
{
|
||||
await context.Database.EnsureCreatedAsync();
|
||||
|
||||
foreach (var theme in DefaultThemes)
|
||||
{
|
||||
var existing = context.SiteTheme.FirstOrDefault(s => s.Name.Equals(theme.Name));
|
||||
if (existing == null)
|
||||
{
|
||||
await context.SiteTheme.AddAsync(theme);
|
||||
}
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public static async Task SeedSettings(DataContext context, IDirectoryService directoryService)
|
||||
{
|
||||
await context.Database.EnsureCreatedAsync();
|
||||
DefaultSettings = ImmutableArray.Create(new List<ServerSetting>()
|
||||
{
|
||||
new() {Key = ServerSettingKey.CacheDirectory, Value = directoryService.CacheDirectory},
|
||||
new() {Key = ServerSettingKey.TaskScan, Value = "daily"},
|
||||
new()
|
||||
{
|
||||
Key = ServerSettingKey.LoggingLevel, Value = "Information"
|
||||
}, // Not used from DB, but DB is sync with appSettings.json
|
||||
new() {Key = ServerSettingKey.TaskBackup, Value = "daily"},
|
||||
new()
|
||||
{
|
||||
Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(DirectoryService.BackupDirectory)
|
||||
},
|
||||
new()
|
||||
{
|
||||
Key = ServerSettingKey.Port, Value = "5000"
|
||||
}, // Not used from DB, but DB is sync with appSettings.json
|
||||
new() {Key = ServerSettingKey.AllowStatCollection, Value = "true"},
|
||||
new() {Key = ServerSettingKey.EnableOpds, Value = "false"},
|
||||
new() {Key = ServerSettingKey.EnableAuthentication, Value = "true"},
|
||||
new() {Key = ServerSettingKey.BaseUrl, Value = "/"},
|
||||
new() {Key = ServerSettingKey.InstallId, Value = HashUtil.AnonymousToken()},
|
||||
new() {Key = ServerSettingKey.InstallVersion, Value = BuildInfo.Version.ToString()},
|
||||
new() {Key = ServerSettingKey.BookmarkDirectory, Value = directoryService.BookmarkDirectory},
|
||||
new() {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl},
|
||||
new() {Key = ServerSettingKey.ConvertBookmarkToWebP, Value = "false"},
|
||||
new() {Key = ServerSettingKey.EnableSwaggerUi, Value = "false"},
|
||||
new() {Key = ServerSettingKey.TotalBackups, Value = "30"},
|
||||
new() {Key = ServerSettingKey.EnableFolderWatching, Value = "false"},
|
||||
}.ToArray());
|
||||
|
||||
foreach (var defaultSetting in DefaultSettings)
|
||||
{
|
||||
var existing = context.ServerSetting.FirstOrDefault(s => s.Key == defaultSetting.Key);
|
||||
if (existing == null)
|
||||
{
|
||||
await context.ServerSetting.AddAsync(defaultSetting);
|
||||
}
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Port and LoggingLevel are managed in appSettings.json. Update the DB values to match
|
||||
context.ServerSetting.First(s => s.Key == ServerSettingKey.Port).Value =
|
||||
Configuration.Port + string.Empty;
|
||||
context.ServerSetting.First(s => s.Key == ServerSettingKey.CacheDirectory).Value =
|
||||
directoryService.CacheDirectory + string.Empty;
|
||||
context.ServerSetting.First(s => s.Key == ServerSettingKey.BackupDirectory).Value =
|
||||
DirectoryService.BackupDirectory + string.Empty;
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
}
|
||||
|
||||
public static async Task SeedUserApiKeys(DataContext context)
|
||||
{
|
||||
await context.Database.EnsureCreatedAsync();
|
||||
|
||||
var users = await context.AppUser.ToListAsync();
|
||||
foreach (var user in users.Where(user => string.IsNullOrEmpty(user.ApiKey)))
|
||||
{
|
||||
user.ApiKey = HashUtil.ApiKey();
|
||||
}
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue