mirror of
				https://github.com/Kareadita/Kavita.git
				synced 2025-11-03 19:17:05 -05:00 
			
		
		
		
	* Moved the data connection for the Database out of appsettings.json and hardcoded it. This will allow for more customization and cleaner update process. * Removed unneeded code * Updated pdf viewer to 15.0.0 (pdf 2.6), which now supports east-asian fonts * Fixed up some regex parsing for volumes that have a float number. * Fixed a bug where the tooltip for Publication Status wouldn't show * Fixed some weird parsing rules where v1.1 would parse as volume 1 chapter 1 * Fixed a bug where bookmarking button was hidden for admins without bookmark role (due to migration) * Unified the star rating component in series detail to match metadata filter. * Fixed a bug in the bulk selection code when using shift selection, where the inverse of what was selected would be toggled. * Fixed some old code where if on all series page, only English as a language would return. We now return all languages of all libraries. * Updated api/metadata/languages documentation * Refactored some bookmark api names: get-bookmarks -> chapter-bookmarks, get-all-bookmarks -> all-bookmarks, get-series-bookmarks -> series-bookmarks, etc. * Refactored all cases of createSeriesFilter to filterUtiltityService. Added ability to search for a series on Bookmarks page. Fixed a bug where people filters wouldn't respect the disable flag froms ettings. * Cleaned up a bit of the circular downloader code. * Implemented Russian Parsing * Fixed an issue where some users that had a missing theme entry wouldn't be able to update their user preferences. * Refactored normalization to exclude !, thus allowing series with ! to be different from each other. * Fixed a migration exit case * Fixed broken unit test
		
			
				
	
	
		
			336 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			336 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.Entities.Enums;
 | 
						|
using API.Extensions;
 | 
						|
using API.Logging;
 | 
						|
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.Features;
 | 
						|
using Microsoft.AspNetCore.HttpOverrides;
 | 
						|
using Microsoft.AspNetCore.Identity;
 | 
						|
using Microsoft.AspNetCore.Mvc;
 | 
						|
using Microsoft.AspNetCore.ResponseCompression;
 | 
						|
using Microsoft.AspNetCore.StaticFiles;
 | 
						|
using Microsoft.Extensions.Configuration;
 | 
						|
using Microsoft.Extensions.DependencyInjection;
 | 
						|
using Microsoft.Extensions.Hosting;
 | 
						|
using Microsoft.Extensions.Logging;
 | 
						|
using Microsoft.Net.Http.Headers;
 | 
						|
using Microsoft.OpenApi.Models;
 | 
						|
using Serilog;
 | 
						|
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(options =>
 | 
						|
        {
 | 
						|
            options.CacheProfiles.Add("Images",
 | 
						|
                new CacheProfile()
 | 
						|
                {
 | 
						|
                    Duration = 60,
 | 
						|
                    Location = ResponseCacheLocation.None,
 | 
						|
                    NoStore = false
 | 
						|
                });
 | 
						|
            options.CacheProfiles.Add("Hour",
 | 
						|
                new CacheProfile()
 | 
						|
                {
 | 
						|
                    Duration = 60 * 10,
 | 
						|
                    Location = ResponseCacheLocation.None,
 | 
						|
                    NoStore = false
 | 
						|
                });
 | 
						|
        });
 | 
						|
        services.Configure<ForwardedHeadersOptions>(options =>
 | 
						|
        {
 | 
						|
            options.ForwardedHeaders = ForwardedHeaders.All;
 | 
						|
            foreach(var proxy in _config.GetSection("KnownProxies").AsEnumerable().Where(c => c.Value != null)) {
 | 
						|
                options.KnownProxies.Add(IPAddress.Parse(proxy.Value));
 | 
						|
            }
 | 
						|
        });
 | 
						|
        services.AddCors();
 | 
						|
        services.AddIdentityServices(_config);
 | 
						|
        services.AddSwaggerGen(c =>
 | 
						|
        {
 | 
						|
            c.SwaggerDoc("v1", 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, true);
 | 
						|
            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 = "Custom Url",
 | 
						|
                Url = "/"
 | 
						|
            });
 | 
						|
 | 
						|
            c.AddServer(new OpenApiServer()
 | 
						|
            {
 | 
						|
                Description = "Local Server",
 | 
						|
                Url = "http://localhost:5000/",
 | 
						|
            });
 | 
						|
 | 
						|
            c.AddServer(new OpenApiServer()
 | 
						|
            {
 | 
						|
                Url = "https://demo.kavitareader.com/",
 | 
						|
                Description = "Kavita Demo"
 | 
						|
            });
 | 
						|
 | 
						|
            c.AddServer(new OpenApiServer()
 | 
						|
            {
 | 
						|
                Url = "http://" + GetLocalIpAddress() + ":5000/",
 | 
						|
                Description = "Local IP"
 | 
						|
            });
 | 
						|
 | 
						|
        });
 | 
						|
        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.AddHangfire(configuration => configuration
 | 
						|
            .UseSimpleAssemblyNameTypeSerializer()
 | 
						|
            .UseRecommendedSerializerSettings()
 | 
						|
            .UseMemoryStorage()); // UseSQLiteStorage - SQLite has some issues around resuming jobs when aborted
 | 
						|
 | 
						|
        // Add the processing server as IHostedService
 | 
						|
        services.AddHangfireServer(options =>
 | 
						|
        {
 | 
						|
            options.Queues = new[] {TaskScheduler.ScanQueue, TaskScheduler.DefaultQueue};
 | 
						|
        });
 | 
						|
        // 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
 | 
						|
                    var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
 | 
						|
                    var userManager = serviceProvider.GetRequiredService<UserManager<AppUser>>();
 | 
						|
                    var themeService = serviceProvider.GetRequiredService<IThemeService>();
 | 
						|
                    var dataContext = serviceProvider.GetRequiredService<DataContext>();
 | 
						|
 | 
						|
 | 
						|
                    // Only run this if we are upgrading
 | 
						|
                    await MigrateChangePasswordRoles.Migrate(unitOfWork, userManager);
 | 
						|
 | 
						|
                    await MigrateRemoveExtraThemes.Migrate(unitOfWork, themeService);
 | 
						|
 | 
						|
                    // only needed for v0.5.4 and v0.6.0
 | 
						|
                    await MigrateNormalizedEverything.Migrate(unitOfWork, dataContext, logger);
 | 
						|
 | 
						|
                    //  Update the version in the DB after all migrations are run
 | 
						|
                    var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
 | 
						|
                    installVersion.Value = BuildInfo.Version.ToString();
 | 
						|
                    unitOfWork.SettingsRepository.Update(installVersion);
 | 
						|
 | 
						|
                    await unitOfWork.CommitAsync();
 | 
						|
                }).GetAwaiter()
 | 
						|
                .GetResult();
 | 
						|
        }
 | 
						|
        catch (Exception ex)
 | 
						|
        {
 | 
						|
            var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
 | 
						|
            logger.LogCritical(ex, "An error occurred during migration");
 | 
						|
        }
 | 
						|
 | 
						|
 | 
						|
 | 
						|
        app.UseMiddleware<ExceptionMiddleware>();
 | 
						|
 | 
						|
        Task.Run(async () =>
 | 
						|
        {
 | 
						|
            var allowSwaggerUi = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync())
 | 
						|
                .EnableSwaggerUi;
 | 
						|
 | 
						|
            if (env.IsDevelopment() || allowSwaggerUi)
 | 
						|
            {
 | 
						|
                app.UseSwagger();
 | 
						|
                app.UseSwaggerUI(c =>
 | 
						|
                {
 | 
						|
                    c.SwaggerEndpoint("/swagger/v1/swagger.json", "Kavita API " + BuildInfo.Version);
 | 
						|
                });
 | 
						|
            }
 | 
						|
        });
 | 
						|
 | 
						|
        if (env.IsDevelopment())
 | 
						|
        {
 | 
						|
            app.UseHangfireDashboard();
 | 
						|
        }
 | 
						|
 | 
						|
        app.UseResponseCompression();
 | 
						|
 | 
						|
        app.UseForwardedHeaders();
 | 
						|
 | 
						|
        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();
 | 
						|
 | 
						|
        app.UseStaticFiles(new StaticFileOptions
 | 
						|
        {
 | 
						|
            ContentTypeProvider = new FileExtensionContentTypeProvider(),
 | 
						|
            HttpsCompression = HttpsCompressionMode.Compress,
 | 
						|
            OnPrepareResponse = ctx =>
 | 
						|
            {
 | 
						|
                ctx.Context.Response.Headers[HeaderNames.CacheControl] = "public,max-age=" + TimeSpan.FromHours(24);
 | 
						|
            }
 | 
						|
        });
 | 
						|
 | 
						|
        app.UseSerilogRequestLogging(opts
 | 
						|
            => opts.EnrichDiagnosticContext = LogEnricher.EnrichFromRequest);
 | 
						|
 | 
						|
        app.Use(async (context, next) =>
 | 
						|
        {
 | 
						|
            // Note: I removed this as I caught Chrome caching api responses when it shouldn't have
 | 
						|
            // context.Response.GetTypedHeaders().CacheControl =
 | 
						|
            //     new CacheControlHeaderValue()
 | 
						|
            //     {
 | 
						|
            //         Public = false,
 | 
						|
            //         MaxAge = TimeSpan.FromSeconds(10),
 | 
						|
            //     };
 | 
						|
            context.Response.Headers[HeaderNames.Vary] =
 | 
						|
                new[] { "Accept-Encoding" };
 | 
						|
 | 
						|
            // Don't let the site be iframed outside the same origin (clickjacking)
 | 
						|
            context.Response.Headers.XFrameOptions = "SAMEORIGIN";
 | 
						|
 | 
						|
            // Setup CSP to ensure we load assets only from these origins
 | 
						|
            context.Response.Headers.Add("Content-Security-Policy", "frame-ancestors 'none';");
 | 
						|
 | 
						|
            await next();
 | 
						|
        });
 | 
						|
 | 
						|
        app.UseEndpoints(endpoints =>
 | 
						|
        {
 | 
						|
            endpoints.MapControllers();
 | 
						|
            endpoints.MapHub<MessageHub>("hubs/messages");
 | 
						|
            endpoints.MapHub<LogHub>("hubs/logs");
 | 
						|
            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!");
 | 
						|
    }
 | 
						|
 | 
						|
}
 |