Ability to update settings. Take effect on next reboot.

This commit is contained in:
Joseph Milazzo 2021-02-04 16:49:48 -06:00
parent e60f795410
commit 1050fa4e54
21 changed files with 953 additions and 146 deletions

View File

@ -0,0 +1,28 @@
using System;
using API.Helpers.Converters;
using AutoMapper;
using Hangfire;
using Xunit;
using Xunit.Abstractions;
namespace API.Tests.Converters
{
public class CronConverterTests
{
private readonly ITestOutputHelper _testOutputHelper;
public CronConverterTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}
[Theory]
[InlineData("daily", "0 0 * * *")]
[InlineData("disabled", "0 0 31 2 *")]
[InlineData("weekly", "0 0 * * 1")]
public void ConvertTest(string input, string expected)
{
Assert.Equal(expected, CronConverter.ConvertToCronNotation(input));
}
}
}

View File

@ -20,9 +20,5 @@ namespace API.Controllers
var users = await _userManager.GetUsersInRoleAsync("Admin");
return users.Count > 0;
}
}
}

View File

@ -1,14 +1,14 @@
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using API.Data;
using API.DTOs;
using API.Entities;
using API.Extensions;
using API.Helpers.Converters;
using API.Interfaces;
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Controllers
@ -18,30 +18,30 @@ namespace API.Controllers
{
private readonly DataContext _dataContext;
private readonly ILogger<SettingsController> _logger;
private readonly IMapper _mapper;
private readonly IUnitOfWork _unitOfWork;
private readonly ITaskScheduler _taskScheduler;
public SettingsController(DataContext dataContext, ILogger<SettingsController> logger, IMapper mapper, ITaskScheduler taskScheduler)
public SettingsController(DataContext dataContext, ILogger<SettingsController> logger, IUnitOfWork unitOfWork,
ITaskScheduler taskScheduler)
{
_dataContext = dataContext;
_logger = logger;
_mapper = mapper;
_unitOfWork = unitOfWork;
_taskScheduler = taskScheduler;
}
[HttpGet("")]
public async Task<ActionResult<ServerSettingDto>> GetSettings()
{
var settings = await _dataContext.ServerSetting.Select(x => x).ToListAsync();
return _mapper.Map<ServerSettingDto>(settings);
return Ok(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync());
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("")]
public async Task<ActionResult> UpdateSettings(ServerSettingDto updateSettingsDto)
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto)
{
_logger.LogInformation($"{User.GetUsername()} is updating Server Settings");
if (updateSettingsDto.CacheDirectory.Equals(string.Empty))
{
return BadRequest("Cache Directory cannot be empty");
@ -51,13 +51,39 @@ namespace API.Controllers
{
return BadRequest("Directory does not exist or is not accessible.");
}
// TODO: Figure out how to handle a change. This means that on clean, we need to clean up old cache
// directory and new one, but what if someone is reading?
// I can just clean both always, /cache/ is an owned folder, so users shouldn't use it.
//_dataContext.ServerSetting.Update
return BadRequest("Not Implemented");
// We do not allow CacheDirectory changes, so we will ignore.
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
foreach (var setting in currentSettings)
{
if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value)
{
setting.Value = updateSettingsDto.TaskBackup;
_unitOfWork.SettingsRepository.Update(setting);
}
if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value)
{
setting.Value = updateSettingsDto.TaskScan;
_unitOfWork.SettingsRepository.Update(setting);
}
}
if (_unitOfWork.HasChanges() && await _unitOfWork.Complete())
{
_logger.LogInformation("Server Settings updated.");
return Ok(updateSettingsDto);
}
return BadRequest("There was a critical issue. Please try again.");
}
[Authorize(Policy = "RequireAdminRole")]
[HttpGet("task-frequencies")]
public ActionResult<IEnumerable<string>> GetTaskFrequencies()
{
return Ok(CronConverter.Options);
}
}
}

View File

@ -3,7 +3,8 @@
public class ServerSettingDto
{
public string CacheDirectory { get; set; }
// public string Kind { get; init; }
// public string Value { get; init; }
public string TaskScan { get; set; }
public string LoggingLevel { get; set; }
public string TaskBackup { get; set; }
}
}

View File

@ -0,0 +1,676 @@
// <auto-generated />
using System;
using API.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace API.Data.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20210203164258_ServerSettingsKey")]
partial class ServerSettingsKey
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.1");
modelBuilder.Entity("API.Entities.AppRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("API.Entities.AppUser", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastActive")
.HasColumnType("TEXT");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AppUserId")
.HasColumnType("INTEGER");
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.Property<int>("PagesRead")
.HasColumnType("INTEGER");
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.Property<int>("VolumeId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("AppUserId");
b.ToTable("AppUserProgresses");
});
modelBuilder.Entity("API.Entities.AppUserRating", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AppUserId")
.HasColumnType("INTEGER");
b.Property<int>("Rating")
.HasColumnType("INTEGER");
b.Property<string>("Review")
.HasColumnType("TEXT");
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("AppUserId");
b.ToTable("AppUserRating");
});
modelBuilder.Entity("API.Entities.AppUserRole", b =>
{
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.Property<int>("RoleId")
.HasColumnType("INTEGER");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("API.Entities.Chapter", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<byte[]>("CoverImage")
.HasColumnType("BLOB");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("Number")
.HasColumnType("TEXT");
b.Property<int>("Pages")
.HasColumnType("INTEGER");
b.Property<string>("Range")
.HasColumnType("TEXT");
b.Property<int>("VolumeId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("VolumeId");
b.ToTable("Chapter");
});
modelBuilder.Entity("API.Entities.FolderPath", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("LastScanned")
.HasColumnType("TEXT");
b.Property<int>("LibraryId")
.HasColumnType("INTEGER");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("LibraryId");
b.ToTable("FolderPath");
});
modelBuilder.Entity("API.Entities.Library", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CoverImage")
.HasColumnType("TEXT");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Library");
});
modelBuilder.Entity("API.Entities.MangaFile", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.Property<string>("FilePath")
.HasColumnType("TEXT");
b.Property<int>("Format")
.HasColumnType("INTEGER");
b.Property<int>("NumberOfPages")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ChapterId");
b.ToTable("MangaFile");
});
modelBuilder.Entity("API.Entities.Series", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<byte[]>("CoverImage")
.HasColumnType("BLOB");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<int>("LibraryId")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("OriginalName")
.HasColumnType("TEXT");
b.Property<int>("Pages")
.HasColumnType("INTEGER");
b.Property<string>("SortName")
.HasColumnType("TEXT");
b.Property<string>("Summary")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("LibraryId");
b.ToTable("Series");
});
modelBuilder.Entity("API.Entities.ServerSetting", b =>
{
b.Property<int>("Key")
.HasColumnType("INTEGER");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("ServerSetting");
});
modelBuilder.Entity("API.Entities.Volume", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<byte[]>("CoverImage")
.HasColumnType("BLOB");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<bool>("IsSpecial")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("Number")
.HasColumnType("INTEGER");
b.Property<int>("Pages")
.HasColumnType("INTEGER");
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("SeriesId");
b.ToTable("Volume");
});
modelBuilder.Entity("AppUserLibrary", b =>
{
b.Property<int>("AppUsersId")
.HasColumnType("INTEGER");
b.Property<int>("LibrariesId")
.HasColumnType("INTEGER");
b.HasKey("AppUsersId", "LibrariesId");
b.HasIndex("LibrariesId");
b.ToTable("AppUserLibrary");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<int>("RoleId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
{
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")
.WithMany("Progresses")
.HasForeignKey("AppUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AppUser");
});
modelBuilder.Entity("API.Entities.AppUserRating", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")
.WithMany("Ratings")
.HasForeignKey("AppUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AppUser");
});
modelBuilder.Entity("API.Entities.AppUserRole", b =>
{
b.HasOne("API.Entities.AppRole", "Role")
.WithMany("UserRoles")
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Entities.AppUser", "User")
.WithMany("UserRoles")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
});
modelBuilder.Entity("API.Entities.Chapter", b =>
{
b.HasOne("API.Entities.Volume", "Volume")
.WithMany("Chapters")
.HasForeignKey("VolumeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Volume");
});
modelBuilder.Entity("API.Entities.FolderPath", b =>
{
b.HasOne("API.Entities.Library", "Library")
.WithMany("Folders")
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Library");
});
modelBuilder.Entity("API.Entities.MangaFile", b =>
{
b.HasOne("API.Entities.Chapter", "Chapter")
.WithMany("Files")
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Entities.Series", b =>
{
b.HasOne("API.Entities.Library", "Library")
.WithMany("Series")
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Library");
});
modelBuilder.Entity("API.Entities.Volume", b =>
{
b.HasOne("API.Entities.Series", "Series")
.WithMany("Volumes")
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Series");
});
modelBuilder.Entity("AppUserLibrary", b =>
{
b.HasOne("API.Entities.AppUser", null)
.WithMany()
.HasForeignKey("AppUsersId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Entities.Library", null)
.WithMany()
.HasForeignKey("LibrariesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
{
b.HasOne("API.Entities.AppRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
{
b.HasOne("API.Entities.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
{
b.HasOne("API.Entities.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
{
b.HasOne("API.Entities.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Entities.AppRole", b =>
{
b.Navigation("UserRoles");
});
modelBuilder.Entity("API.Entities.AppUser", b =>
{
b.Navigation("Progresses");
b.Navigation("Ratings");
b.Navigation("UserRoles");
});
modelBuilder.Entity("API.Entities.Chapter", b =>
{
b.Navigation("Files");
});
modelBuilder.Entity("API.Entities.Library", b =>
{
b.Navigation("Folders");
b.Navigation("Series");
});
modelBuilder.Entity("API.Entities.Series", b =>
{
b.Navigation("Volumes");
});
modelBuilder.Entity("API.Entities.Volume", b =>
{
b.Navigation("Chapters");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace API.Data.Migrations
{
public partial class ServerSettingsKey : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<int>(
name: "Key",
table: "ServerSetting",
type: "INTEGER",
nullable: false,
oldClrType: typeof(string),
oldType: "TEXT");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "ServerSetting",
type: "TEXT",
nullable: false,
oldClrType: typeof(int),
oldType: "INTEGER");
}
}
}

View File

@ -335,8 +335,8 @@ namespace API.Data.Migrations
modelBuilder.Entity("API.Entities.ServerSetting", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<int>("Key")
.HasColumnType("INTEGER");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()

View File

@ -31,20 +31,23 @@ namespace API.Data
public static async Task SeedSettings(DataContext context)
{
context.Database.EnsureCreated();
IList<ServerSetting> defaultSettings = new List<ServerSetting>()
{
new() {Key = "CacheDirectory", Value = CacheService.CacheDirectory}
new() {Key = ServerSettingKey.CacheDirectory, Value = CacheService.CacheDirectory},
new () {Key = ServerSettingKey.TaskScan, Value = "daily"}
};
var settings = await context.ServerSetting.Select(s => s).ToListAsync();
foreach (var defaultSetting in defaultSettings)
{
var existing = settings.SingleOrDefault(s => s.Key == defaultSetting.Key);
var existing = context.ServerSetting.FirstOrDefault(s => s.Key == defaultSetting.Key);
if (existing == null)
{
settings.Add(defaultSetting);
context.ServerSetting.Add(defaultSetting);
}
}
await context.SaveChangesAsync();
}
}

View File

@ -0,0 +1,47 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.DTOs;
using API.Entities;
using API.Interfaces;
using AutoMapper;
using Microsoft.EntityFrameworkCore;
namespace API.Data
{
public class SettingsRepository : ISettingsRepository
{
private readonly DataContext _context;
private readonly IMapper _mapper;
public SettingsRepository(DataContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public void Update(ServerSetting settings)
{
_context.Entry(settings).State = EntityState.Modified;
}
public async Task<ServerSettingDto> GetSettingsDtoAsync()
{
var settings = await _context.ServerSetting
.Select(x => x)
.AsNoTracking()
.ToListAsync();
return _mapper.Map<ServerSettingDto>(settings);
}
public Task<ServerSetting> GetSettingAsync(ServerSettingKey key)
{
return _context.ServerSetting.SingleOrDefaultAsync(x => x.Key == key);
}
public async Task<IEnumerable<ServerSetting>> GetSettingsAsync()
{
return await _context.ServerSetting.ToListAsync();
}
}
}

View File

@ -24,6 +24,8 @@ namespace API.Data
public ILibraryRepository LibraryRepository => new LibraryRepository(_context, _mapper);
public IVolumeRepository VolumeRepository => new VolumeRepository(_context, _mapper);
public ISettingsRepository SettingsRepository => new SettingsRepository(_context, _mapper);
public async Task<bool> Complete()
{

View File

@ -7,7 +7,7 @@ namespace API.Entities
public class ServerSetting : IHasConcurrencyToken
{
[Key]
public string Key { get; set; }
public ServerSettingKey Key { get; set; }
public string Value { get; set; }
[ConcurrencyCheck]

View File

@ -0,0 +1,10 @@
namespace API.Entities
{
public enum ServerSettingKey
{
TaskScan = 0,
CacheDirectory = 1,
TaskBackup = 2,
LoggingLevel = 3
}
}

View File

@ -0,0 +1,41 @@
using System.Collections.Generic;
using Hangfire;
namespace API.Helpers.Converters
{
public static class CronConverter
{
public static readonly IEnumerable<string> Options = new []
{
"disabled",
"daily",
"weekly",
};
public static string ConvertToCronNotation(string source)
{
string destination = "";
destination = source.ToLower() switch
{
"daily" => Cron.Daily(),
"weekly" => Cron.Weekly(),
"disabled" => Cron.Never(),
"" => Cron.Never(),
_ => destination
};
return destination;
}
public static string ConvertFromCronNotation(string cronNotation)
{
string destination = "";
destination = cronNotation.ToLower() switch
{
"0 0 31 2 *" => "disabled",
_ => destination
};
return destination;
}
}
}

View File

@ -9,16 +9,24 @@ namespace API.Helpers.Converters
{
public ServerSettingDto Convert(IEnumerable<ServerSetting> source, ServerSettingDto destination, ResolutionContext context)
{
destination = new ServerSettingDto();
destination ??= new ServerSettingDto();
foreach (var row in source)
{
switch (row.Key)
{
case "CacheDirectory":
case ServerSettingKey.CacheDirectory:
destination.CacheDirectory = row.Value;
break;
default:
case ServerSettingKey.TaskScan:
destination.TaskScan = row.Value;
break;
case ServerSettingKey.LoggingLevel:
destination.LoggingLevel = row.Value;
break;
case ServerSettingKey.TaskBackup:
destination.TaskBackup = row.Value;
break;
}
}

View File

@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using API.DTOs;
using API.Entities;
namespace API.Interfaces
{
public interface ISettingsRepository
{
void Update(ServerSetting settings);
Task<ServerSettingDto> GetSettingsDtoAsync();
Task<ServerSetting> GetSettingAsync(ServerSettingKey key);
Task<IEnumerable<ServerSetting>> GetSettingsAsync();
}
}

View File

@ -8,6 +8,7 @@ namespace API.Interfaces
IUserRepository UserRepository { get; }
ILibraryRepository LibraryRepository { get; }
IVolumeRepository VolumeRepository { get; }
ISettingsRepository SettingsRepository { get; }
Task<bool> Complete();
bool HasChanges();
}

View File

@ -74,19 +74,18 @@ namespace API.Services
private byte[] CreateThumbnail(ZipArchiveEntry entry)
{
var coverImage = Array.Empty<byte>();
try
{
using var stream = entry.Open();
using var thumbnail = Image.ThumbnailStream(stream, ThumbnailWidth);
coverImage = thumbnail.WriteToBuffer(".jpg");
return thumbnail.WriteToBuffer(".jpg");
}
catch (Exception ex)
{
_logger.LogError(ex, "There was a critical error and prevented thumbnail generation. Defaulting to no cover image.");
}
return coverImage;
return Array.Empty<byte>();
}
private static byte[] ConvertEntryToByteArray(ZipArchiveEntry entry)

View File

@ -75,7 +75,6 @@ namespace API.Services
public void CleanupChapters(int[] chapterIds)
{
// TODO: Fix this code to work with chapters
_logger.LogInformation($"Running Cache cleanup on Volumes");
foreach (var chapter in chapterIds)
@ -112,7 +111,7 @@ namespace API.Services
{
var path = GetCachePath(chapter.Id);
// TODO: GetFiles should only get image files.
var files = _directoryService.GetFiles(path);
var files = _directoryService.GetFiles(path);
Array.Sort(files, _numericComparer);
return (files.ElementAt(page - pagesSoFar), mangaFile);

View File

@ -4,15 +4,12 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Extensions;
using API.Interfaces;
using API.Parser;
using Microsoft.Extensions.Logging;
using NetVips;
namespace API.Services
{
@ -61,11 +58,11 @@ namespace API.Services
var totalFiles = 0;
foreach (var folderPath in library.Folders)
{
// if (!forceUpdate && Directory.GetLastWriteTime(folderPath.Path) <= folderPath.LastScanned)
// {
// _logger.LogDebug($"{folderPath.Path} hasn't been updated since last scan. Skipping.");
// continue;
// }
if (!forceUpdate && Directory.GetLastWriteTime(folderPath.Path) <= folderPath.LastScanned)
{
_logger.LogDebug($"{folderPath.Path} hasn't been updated since last scan. Skipping.");
continue;
}
try {
totalFiles += DirectoryService.TraverseTreeParallelForEach(folderPath.Path, (f) =>
@ -163,7 +160,7 @@ namespace API.Services
{
if (info.Series == string.Empty) return;
_scannedSeries.AddOrUpdate(info.Series, new List<ParserInfo>() {info}, (key, oldValue) =>
_scannedSeries.AddOrUpdate(info.Series, new List<ParserInfo>() {info}, (_, oldValue) =>
{
oldValue ??= new List<ParserInfo>();
if (!oldValue.Contains(info))
@ -234,94 +231,6 @@ namespace API.Services
return forceUpdate || coverImage == null || !coverImage.Any();
}
/// <summary>
/// Creates or Updates volumes for a given series
/// </summary>
/// <param name="series">Series wanting to be updated</param>
/// <param name="infos">Parser info</param>
/// <param name="forceUpdate">Forces metadata update (cover image) even if it's already been set.</param>
/// <returns>Updated Volumes for given series</returns>
private ICollection<Volume> UpdateVolumes(Series series, ParserInfo[] infos, bool forceUpdate)
{
ICollection<Volume> volumes = new List<Volume>();
IList<Volume> existingVolumes = _unitOfWork.SeriesRepository.GetVolumes(series.Id).ToList();
//var justVolumes = infos.Select(pi => pi.Chapters == "0");
foreach (var info in infos)
{
var existingVolume = existingVolumes.SingleOrDefault(v => v.Name == info.Volumes);
if (existingVolume != null)
{
//var existingFile = existingVolume.Files.SingleOrDefault(f => f.FilePath == info.FullFilePath);
var existingFile = new MangaFile();
if (existingFile != null)
{
//existingFile.Chapter = Parser.Parser.MinimumNumberFromRange(info.Chapters);
existingFile.Format = info.Format;
existingFile.NumberOfPages = _archiveService.GetNumberOfPagesFromArchive(info.FullFilePath);
}
else
{
if (info.Format == MangaFormat.Archive)
{
// existingVolume.Files.Add(CreateMangaFile(info));
}
else
{
_logger.LogDebug($"Ignoring {info.Filename} as it is not an archive.");
}
}
volumes.Add(existingVolume);
}
else
{
// Create New Volume
existingVolume = volumes.SingleOrDefault(v => v.Name == info.Volumes);
if (existingVolume != null)
{
//existingVolume.Files.Add(CreateMangaFile(info));
}
else
{
var vol = new Volume()
{
Name = info.Volumes,
Number = Parser.Parser.MinimumNumberFromRange(info.Volumes),
// Files = new List<MangaFile>()
// {
// CreateMangaFile(info)
// }
};
volumes.Add(vol);
}
}
_logger.LogInformation($"Adding volume {volumes.Last().Number} with File: {info.Filename}");
}
foreach (var volume in volumes)
{
// if (forceUpdate || volume.CoverImage == null || !volume.Files.Any())
// {
// var firstFile = volume.Files.OrderBy(x => x.Chapter).FirstOrDefault();
// if (firstFile != null) volume.CoverImage = _archiveService.GetCoverImage(firstFile.FilePath, true);
// }
//volume.Pages = volume.Files.Sum(x => x.NumberOfPages);
}
return volumes;
}
/// <summary>
///
/// </summary>
@ -345,7 +254,7 @@ namespace API.Services
};
chapter.Files ??= new List<MangaFile>();
var existingFile = chapter?.Files.SingleOrDefault(f => f.FilePath == info.FullFilePath);
var existingFile = chapter.Files.SingleOrDefault(f => f.FilePath == info.FullFilePath);
if (existingFile != null)
{
existingFile.Format = info.Format;
@ -376,7 +285,8 @@ namespace API.Services
if (ShouldFindCoverImage(forceUpdate, chapter.CoverImage))
{
var firstFile = chapter?.Files.OrderBy(x => x.Chapter).FirstOrDefault();
chapter.Files ??= new List<MangaFile>();
var firstFile = chapter.Files.OrderBy(x => x.Chapter).FirstOrDefault();
if (firstFile != null) chapter.CoverImage = _archiveService.GetCoverImage(firstFile.FilePath, true);
}
}

View File

@ -1,4 +1,8 @@
using API.Interfaces;
using System;
using System.Threading.Tasks;
using API.Entities;
using API.Helpers.Converters;
using API.Interfaces;
using Hangfire;
using Microsoft.Extensions.Logging;
@ -9,17 +13,30 @@ namespace API.Services
private readonly ICacheService _cacheService;
private readonly ILogger<TaskScheduler> _logger;
private readonly IScannerService _scannerService;
private readonly IUnitOfWork _unitOfWork;
public BackgroundJobServer Client => new BackgroundJobServer();
public TaskScheduler(ICacheService cacheService, ILogger<TaskScheduler> logger, IScannerService scannerService)
public TaskScheduler(ICacheService cacheService, ILogger<TaskScheduler> logger, IScannerService scannerService, IUnitOfWork unitOfWork)
{
_cacheService = cacheService;
_logger = logger;
_scannerService = scannerService;
_unitOfWork = unitOfWork;
_logger.LogInformation("Scheduling/Updating cache cleanup on a daily basis.");
RecurringJob.AddOrUpdate(() => _cacheService.Cleanup(), Cron.Daily);
RecurringJob.AddOrUpdate(() => _scannerService.ScanLibraries(), Cron.Daily);
var setting = Task.Run(() => _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskScan)).Result;
if (setting != null)
{
RecurringJob.AddOrUpdate(() => _scannerService.ScanLibraries(), () => CronConverter.ConvertToCronNotation(setting.Value));
}
else
{
RecurringJob.AddOrUpdate(() => _cacheService.Cleanup(), Cron.Daily);
RecurringJob.AddOrUpdate(() => _scannerService.ScanLibraries(), Cron.Daily);
}
//JobStorage.Current.GetMonitoringApi().
}
public void ScanSeries(int libraryId, int seriesId)

View File

@ -46,8 +46,6 @@ namespace API
app.UseHangfireDashboard();
}
//app.UseHttpsRedirection();
app.UseRouting();
// Ordering is important. Cors, authentication, authorization