mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-03 05:34:21 -04:00
* Refactored the Font Escaping Regex with new unit tests. * Fonts are now properly escaped, somehow a regression was introduced. * Refactored most of the book page loading for the reader into the service. * Fixed a bug where going into fullscreen in non dark mode will cause the background of the reader to go black. Fixed a rendering issue with margin left/right screwing html up. Fixed an issue where line-height: 100% would break book's css, now we remove the styles if they are non-valuable. * Changed how I fixed the black mode in fullscreen * Fixed an issue where anchors wouldn't be colored blue in white mode * Fixed a bug in the code that checks if a filename is a cover where it would choose "backcover" as a cover, despite it not being a valid case. * Validate if ReleaseYear is a valid year and if not, set it to 0 to disable it. * Fixed an issue where some large images could blow out the screen when reading on mobile. Now images will force to be max of width of browser * Put my hack back in for fullscreen putting background color to black * Change forwarded headers from All to explicit names * Fixed an issue where Scheme was not https when it should have been. Now the browser will handle which scheme to request. * Cleaned up the user preferences to stack multiple controls onto one row * Fixed fullscreen scroll issue with progress, but now sticky top is missing. * Corrected the element on which we fullscreen
316 lines
12 KiB
C#
316 lines
12 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Threading.Tasks;
|
|
using API.Data;
|
|
using API.Entities;
|
|
using API.Extensions;
|
|
using API.Middleware;
|
|
using API.Services;
|
|
using API.Services.HostedServices;
|
|
using API.Services.Tasks;
|
|
using API.SignalR;
|
|
using Hangfire;
|
|
using Hangfire.MemoryStorage;
|
|
using Kavita.Common;
|
|
using Kavita.Common.EnvironmentInfo;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.HttpOverrides;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.AspNetCore.ResponseCompression;
|
|
using Microsoft.AspNetCore.StaticFiles;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.OpenApi.Models;
|
|
using TaskScheduler = API.Services.TaskScheduler;
|
|
|
|
namespace API
|
|
{
|
|
public class Startup
|
|
{
|
|
private readonly IConfiguration _config;
|
|
private readonly IWebHostEnvironment _env;
|
|
|
|
public Startup(IConfiguration config, IWebHostEnvironment env)
|
|
{
|
|
_config = config;
|
|
_env = env;
|
|
}
|
|
|
|
// This method gets called by the runtime. Use this method to add services to the container.
|
|
public void ConfigureServices(IServiceCollection services)
|
|
{
|
|
services.AddApplicationServices(_config, _env);
|
|
services.AddControllers();
|
|
services.Configure<ForwardedHeadersOptions>(options =>
|
|
{
|
|
options.ForwardedHeaders =
|
|
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
|
});
|
|
services.AddCors();
|
|
services.AddIdentityServices(_config);
|
|
services.AddSwaggerGen(c =>
|
|
{
|
|
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Kavita API", Version = "v1" });
|
|
|
|
c.SwaggerDoc("Kavita API", new OpenApiInfo()
|
|
{
|
|
Description = "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage.",
|
|
Title = "Kavita API",
|
|
Version = "v1",
|
|
});
|
|
|
|
var filePath = Path.Combine(AppContext.BaseDirectory, "API.xml");
|
|
c.IncludeXmlComments(filePath);
|
|
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme {
|
|
In = ParameterLocation.Header,
|
|
Description = "Please insert JWT with Bearer into field",
|
|
Name = "Authorization",
|
|
Type = SecuritySchemeType.ApiKey
|
|
});
|
|
c.AddSecurityRequirement(new OpenApiSecurityRequirement {
|
|
{
|
|
new OpenApiSecurityScheme
|
|
{
|
|
Reference = new OpenApiReference
|
|
{
|
|
Type = ReferenceType.SecurityScheme,
|
|
Id = "Bearer"
|
|
}
|
|
},
|
|
Array.Empty<string>()
|
|
}
|
|
});
|
|
|
|
c.AddServer(new OpenApiServer()
|
|
{
|
|
Description = "Local Server",
|
|
Url = "http://localhost:5000/",
|
|
});
|
|
});
|
|
services.AddResponseCompression(options =>
|
|
{
|
|
options.Providers.Add<BrotliCompressionProvider>();
|
|
options.Providers.Add<GzipCompressionProvider>();
|
|
options.MimeTypes =
|
|
ResponseCompressionDefaults.MimeTypes.Concat(
|
|
new[] { "image/jpeg", "image/jpg" });
|
|
options.EnableForHttps = true;
|
|
});
|
|
services.Configure<BrotliCompressionProviderOptions>(options =>
|
|
{
|
|
options.Level = CompressionLevel.Fastest;
|
|
});
|
|
|
|
services.AddResponseCaching();
|
|
|
|
services.Configure<ForwardedHeadersOptions>(options =>
|
|
{
|
|
options.ForwardedHeaders =
|
|
ForwardedHeaders.All;
|
|
});
|
|
|
|
services.AddHangfire(configuration => configuration
|
|
.UseSimpleAssemblyNameTypeSerializer()
|
|
.UseRecommendedSerializerSettings()
|
|
.UseMemoryStorage());
|
|
|
|
// Add the processing server as IHostedService
|
|
services.AddHangfireServer();
|
|
|
|
// Add IHostedService for startup tasks
|
|
// Any services that should be bootstrapped go here
|
|
services.AddHostedService<StartupTasksHostedService>();
|
|
}
|
|
|
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
|
public void Configure(IApplicationBuilder app, IBackgroundJobClient backgroundJobs, IWebHostEnvironment env,
|
|
IHostApplicationLifetime applicationLifetime, IServiceProvider serviceProvider, ICacheService cacheService,
|
|
IDirectoryService directoryService, IUnitOfWork unitOfWork, IBackupService backupService, IImageService imageService)
|
|
{
|
|
|
|
// Apply Migrations
|
|
try
|
|
{
|
|
Task.Run(async () =>
|
|
{
|
|
// Apply all migrations on startup
|
|
// If we have pending migrations, make a backup first
|
|
var isDocker = new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker;
|
|
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
|
|
var context = serviceProvider.GetRequiredService<DataContext>();
|
|
// var pendingMigrations = await context.Database.GetPendingMigrationsAsync();
|
|
// if (pendingMigrations.Any())
|
|
// {
|
|
// logger.LogInformation("Performing backup as migrations are needed");
|
|
// await backupService.BackupDatabase();
|
|
// }
|
|
//
|
|
// await context.Database.MigrateAsync();
|
|
// var roleManager = serviceProvider.GetRequiredService<RoleManager<AppRole>>();
|
|
//
|
|
// await Seed.SeedRoles(roleManager);
|
|
// await Seed.SeedSettings(context, directoryService);
|
|
// await Seed.SeedUserApiKeys(context);
|
|
|
|
await MigrateBookmarks.Migrate(directoryService, unitOfWork,
|
|
logger, cacheService);
|
|
|
|
var requiresCoverImageMigration = !Directory.Exists(directoryService.CoverImageDirectory);
|
|
try
|
|
{
|
|
// If this is a new install, tables wont exist yet
|
|
if (requiresCoverImageMigration)
|
|
{
|
|
MigrateCoverImages.ExtractToImages(context, directoryService, imageService);
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
requiresCoverImageMigration = false;
|
|
}
|
|
|
|
if (requiresCoverImageMigration)
|
|
{
|
|
await MigrateCoverImages.UpdateDatabaseWithImages(context, directoryService);
|
|
}
|
|
}).GetAwaiter()
|
|
.GetResult();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
|
|
logger.LogCritical(ex, "An error occurred during migration");
|
|
}
|
|
|
|
|
|
|
|
app.UseMiddleware<ExceptionMiddleware>();
|
|
|
|
if (env.IsDevelopment())
|
|
{
|
|
app.UseSwagger();
|
|
app.UseSwaggerUI(c =>
|
|
{
|
|
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Kavita API " + BuildInfo.Version);
|
|
});
|
|
app.UseHangfireDashboard();
|
|
}
|
|
|
|
app.UseResponseCompression();
|
|
|
|
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
|
{
|
|
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost
|
|
});
|
|
|
|
app.UseRouting();
|
|
|
|
// Ordering is important. Cors, authentication, authorization
|
|
if (env.IsDevelopment())
|
|
{
|
|
app.UseCors(policy => policy
|
|
.AllowAnyHeader()
|
|
.AllowAnyMethod()
|
|
.AllowCredentials() // For SignalR token query param
|
|
.WithOrigins("http://localhost:4200", $"http://{GetLocalIpAddress()}:4200")
|
|
.WithExposedHeaders("Content-Disposition", "Pagination"));
|
|
}
|
|
|
|
app.UseResponseCaching();
|
|
|
|
app.UseAuthentication();
|
|
|
|
app.UseAuthorization();
|
|
|
|
app.UseDefaultFiles();
|
|
|
|
// This is not implemented completely. Commenting out until implemented
|
|
// var service = serviceProvider.GetRequiredService<IUnitOfWork>();
|
|
// var settings = service.SettingsRepository.GetSettingsDto();
|
|
// if (!string.IsNullOrEmpty(settings.BaseUrl) && !settings.BaseUrl.Equals("/"))
|
|
// {
|
|
// var path = !settings.BaseUrl.StartsWith("/")
|
|
// ? $"/{settings.BaseUrl}"
|
|
// : settings.BaseUrl;
|
|
// path = !path.EndsWith("/")
|
|
// ? $"{path}/"
|
|
// : path;
|
|
// app.UsePathBase(path);
|
|
// Console.WriteLine("Starting with base url as " + path);
|
|
// }
|
|
|
|
app.UseStaticFiles(new StaticFileOptions
|
|
{
|
|
ContentTypeProvider = new FileExtensionContentTypeProvider()
|
|
});
|
|
|
|
|
|
|
|
|
|
app.Use(async (context, next) =>
|
|
{
|
|
context.Response.GetTypedHeaders().CacheControl =
|
|
new Microsoft.Net.Http.Headers.CacheControlHeaderValue()
|
|
{
|
|
Public = false,
|
|
MaxAge = TimeSpan.FromSeconds(10),
|
|
};
|
|
context.Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.Vary] =
|
|
new[] { "Accept-Encoding" };
|
|
|
|
await next();
|
|
});
|
|
|
|
app.UseEndpoints(endpoints =>
|
|
{
|
|
endpoints.MapControllers();
|
|
endpoints.MapHub<MessageHub>("hubs/messages");
|
|
endpoints.MapHangfireDashboard();
|
|
endpoints.MapFallbackToController("Index", "Fallback");
|
|
});
|
|
|
|
applicationLifetime.ApplicationStopping.Register(OnShutdown);
|
|
applicationLifetime.ApplicationStarted.Register(() =>
|
|
{
|
|
try
|
|
{
|
|
var logger = serviceProvider.GetRequiredService<ILogger<Startup>>();
|
|
logger.LogInformation("Kavita - v{Version}", BuildInfo.Version);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
/* Swallow Exception */
|
|
}
|
|
Console.WriteLine($"Kavita - v{BuildInfo.Version}");
|
|
});
|
|
}
|
|
|
|
private static void OnShutdown()
|
|
{
|
|
Console.WriteLine("Server is shutting down. Please allow a few seconds to stop any background jobs...");
|
|
TaskScheduler.Client.Dispose();
|
|
System.Threading.Thread.Sleep(1000);
|
|
Console.WriteLine("You may now close the application window.");
|
|
}
|
|
|
|
private static string GetLocalIpAddress()
|
|
{
|
|
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0);
|
|
socket.Connect("8.8.8.8", 65530);
|
|
if (socket.LocalEndPoint is IPEndPoint endPoint) return endPoint.Address.ToString();
|
|
throw new KavitaException("No network adapters with an IPv4 address in the system!");
|
|
}
|
|
|
|
|
|
}
|
|
}
|