mirror of
https://github.com/booklore-app/booklore.git
synced 2026-02-18 00:17:53 +01:00
Remove nginx and serve Angular directly from Spring Boot (#2662)
* feat: remove nginx and serve Angular directly from Spring Boot * fix: handle null values in EnabledFields deserialization from persisted JSON * fix(migration): auto-repair failed Flyway migrations on startup * fix(migration): replace DB triggers with app-level orphan cleanup * fix: restore default port to 6060 for backwards compatibility * fix: align all port references to 6060 and add OPDS compression MIME types * fix: resolve NG0101 recursive tick error in book browser selection --------- Co-authored-by: acx10 <acx10@users.noreply.github.com>
This commit is contained in:
17
Dockerfile
17
Dockerfile
@@ -26,6 +26,9 @@ RUN --mount=type=cache,target=/home/gradle/.gradle \
|
||||
|
||||
COPY ./booklore-api/src /springboot-app/src
|
||||
|
||||
# Copy Angular dist into Spring Boot static resources so it's embedded in the JAR
|
||||
COPY --from=angular-build /angular-app/dist/booklore/browser /springboot-app/src/main/resources/static
|
||||
|
||||
# Inject version into application.yaml using yq
|
||||
ARG APP_VERSION
|
||||
RUN apk add --no-cache yq && \
|
||||
@@ -53,14 +56,14 @@ LABEL org.opencontainers.image.title="BookLore" \
|
||||
|
||||
ENV JAVA_TOOL_OPTIONS="-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+UseStringDeduplication -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
|
||||
|
||||
RUN apk update && apk add nginx gettext su-exec
|
||||
RUN apk update && apk add --no-cache su-exec
|
||||
|
||||
COPY ./nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --from=angular-build /angular-app/dist/booklore/browser /usr/share/nginx/html
|
||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
COPY --from=springboot-build /springboot-app/build/libs/booklore-api-0.0.1-SNAPSHOT.jar /app/app.jar
|
||||
COPY start.sh /start.sh
|
||||
RUN chmod +x /start.sh
|
||||
|
||||
EXPOSE 8080 80
|
||||
ARG BOOKLORE_PORT=6060
|
||||
EXPOSE ${BOOKLORE_PORT}
|
||||
|
||||
CMD ["/start.sh"]
|
||||
ENTRYPOINT ["entrypoint.sh"]
|
||||
CMD ["java", "-jar", "/app/app.jar"]
|
||||
|
||||
10
README.md
10
README.md
@@ -214,10 +214,9 @@ Create a `.env` file in your project directory:
|
||||
|
||||
```ini
|
||||
# 🎯 BookLore Application Settings
|
||||
APP_USER_ID=0
|
||||
APP_GROUP_ID=0
|
||||
APP_USER_ID=1000
|
||||
APP_GROUP_ID=1000
|
||||
TZ=Etc/UTC
|
||||
BOOKLORE_PORT=6060
|
||||
|
||||
# 🗄️ Database Connection (BookLore)
|
||||
DATABASE_URL=jdbc:mariadb://mariadb:3306/booklore
|
||||
@@ -252,18 +251,17 @@ services:
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
- DATABASE_USERNAME=${DB_USER}
|
||||
- DATABASE_PASSWORD=${DB_PASSWORD}
|
||||
- BOOKLORE_PORT=${BOOKLORE_PORT}
|
||||
depends_on:
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "${BOOKLORE_PORT}:${BOOKLORE_PORT}"
|
||||
- "6060:6060"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./books:/books
|
||||
- ./bookdrop:/bookdrop
|
||||
healthcheck:
|
||||
test: wget -q -O - http://localhost:${BOOKLORE_PORT}/api/v1/healthcheck
|
||||
test: wget -q -O - http://localhost:6060/api/v1/healthcheck
|
||||
interval: 60s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.booklore.config;
|
||||
|
||||
import org.flywaydb.core.api.exception.FlywayValidateException;
|
||||
import org.springframework.boot.flyway.autoconfigure.FlywayMigrationStrategy;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class FlywayConfig {
|
||||
|
||||
@Bean
|
||||
FlywayMigrationStrategy flywayMigrationStrategy() {
|
||||
return flyway -> {
|
||||
try {
|
||||
flyway.migrate();
|
||||
} catch (FlywayValidateException e) {
|
||||
flyway.repair();
|
||||
flyway.migrate();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,17 @@ import org.booklore.interceptor.KomgaCleanInterceptor;
|
||||
import org.booklore.interceptor.KomgaEnabledInterceptor;
|
||||
import org.booklore.interceptor.OpdsEnabledInterceptor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.task.VirtualThreadTaskExecutor;
|
||||
import org.springframework.data.web.config.EnableSpringDataWebSupport;
|
||||
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.resource.PathResourceResolver;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO;
|
||||
|
||||
@@ -27,6 +33,22 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
configurer.setTaskExecutor(new VirtualThreadTaskExecutor("mvc-async-"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("/**")
|
||||
.addResourceLocations("classpath:/static/")
|
||||
.resourceChain(true)
|
||||
.addResolver(new PathResourceResolver() {
|
||||
@Override
|
||||
protected Resource getResource(String resourcePath, Resource location) throws IOException {
|
||||
Resource resource = location.createRelative(resourcePath);
|
||||
return resource.exists() && resource.isReadable()
|
||||
? resource
|
||||
: new ClassPathResource("/static/index.html");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(opdsEnabledInterceptor)
|
||||
|
||||
@@ -204,6 +204,7 @@ public class SecurityConfig {
|
||||
publicEndpoints.addAll(Arrays.asList(SWAGGER_ENDPOINTS));
|
||||
}
|
||||
http
|
||||
.securityMatcher("/api/**", "/komga/**", "/ws/**")
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
@@ -216,6 +217,17 @@ public class SecurityConfig {
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(9)
|
||||
public SecurityFilterChain staticResourcesSecurityChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
|
||||
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
|
||||
AuthenticationManagerBuilder auth = http.getSharedObject(AuthenticationManagerBuilder.class);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.booklore.model.dto.request;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonSetter;
|
||||
import com.fasterxml.jackson.annotation.Nulls;
|
||||
import org.booklore.model.enums.MetadataProvider;
|
||||
import org.booklore.model.enums.MetadataReplaceMode;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
@@ -88,70 +90,72 @@ public class MetadataRefreshOptions {
|
||||
@Builder
|
||||
public static class EnabledFields {
|
||||
// All fields default to true so metadata fetcher updates fields by default
|
||||
// unless explicitly disabled by the user
|
||||
@Builder.Default
|
||||
// unless explicitly disabled by the user.
|
||||
// @JsonSetter(nulls = Nulls.SKIP) ensures that null values in persisted JSON
|
||||
// (e.g. from older versions missing newer fields) are ignored, preserving defaults.
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean title = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean subtitle = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean description = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean authors = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean publisher = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean publishedDate = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean seriesName = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean seriesNumber = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean seriesTotal = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean isbn13 = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean isbn10 = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean language = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean categories = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean cover = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean pageCount = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean asin = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean goodreadsId = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean comicvineId = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean hardcoverId = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean googleId = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean lubimyczytacId = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean amazonRating = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean amazonReviewCount = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean goodreadsRating = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean goodreadsReviewCount = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean hardcoverRating = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean hardcoverReviewCount = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean lubimyczytacRating = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean ranobedbId = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean ranobedbRating = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean moods = true;
|
||||
@Builder.Default
|
||||
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
|
||||
private boolean tags = true;
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,10 +2,16 @@ package org.booklore.repository;
|
||||
|
||||
import org.booklore.model.entity.ComicCharacterEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ComicCharacterRepository extends JpaRepository<ComicCharacterEntity, Long> {
|
||||
|
||||
Optional<ComicCharacterEntity> findByName(String name);
|
||||
|
||||
@Modifying(flushAutomatically = true)
|
||||
@Query(value = "DELETE FROM comic_character WHERE id NOT IN (SELECT DISTINCT character_id FROM comic_metadata_character_mapping)", nativeQuery = true)
|
||||
void deleteOrphaned();
|
||||
}
|
||||
|
||||
@@ -2,10 +2,16 @@ package org.booklore.repository;
|
||||
|
||||
import org.booklore.model.entity.ComicCreatorEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ComicCreatorRepository extends JpaRepository<ComicCreatorEntity, Long> {
|
||||
|
||||
Optional<ComicCreatorEntity> findByName(String name);
|
||||
|
||||
@Modifying(flushAutomatically = true)
|
||||
@Query(value = "DELETE FROM comic_creator WHERE id NOT IN (SELECT DISTINCT creator_id FROM comic_metadata_creator_mapping)", nativeQuery = true)
|
||||
void deleteOrphaned();
|
||||
}
|
||||
|
||||
@@ -2,10 +2,16 @@ package org.booklore.repository;
|
||||
|
||||
import org.booklore.model.entity.ComicLocationEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ComicLocationRepository extends JpaRepository<ComicLocationEntity, Long> {
|
||||
|
||||
Optional<ComicLocationEntity> findByName(String name);
|
||||
|
||||
@Modifying(flushAutomatically = true)
|
||||
@Query(value = "DELETE FROM comic_location WHERE id NOT IN (SELECT DISTINCT location_id FROM comic_metadata_location_mapping)", nativeQuery = true)
|
||||
void deleteOrphaned();
|
||||
}
|
||||
|
||||
@@ -2,10 +2,16 @@ package org.booklore.repository;
|
||||
|
||||
import org.booklore.model.entity.ComicTeamEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ComicTeamRepository extends JpaRepository<ComicTeamEntity, Long> {
|
||||
|
||||
Optional<ComicTeamEntity> findByName(String name);
|
||||
|
||||
@Modifying(flushAutomatically = true)
|
||||
@Query(value = "DELETE FROM comic_team WHERE id NOT IN (SELECT DISTINCT team_id FROM comic_metadata_team_mapping)", nativeQuery = true)
|
||||
void deleteOrphaned();
|
||||
}
|
||||
|
||||
@@ -146,12 +146,14 @@ public class SettingPersistenceHelper {
|
||||
.comicvineId(nullProvider)
|
||||
.hardcoverId(nullProvider)
|
||||
.googleId(nullProvider)
|
||||
.lubimyczytacId(nullProvider)
|
||||
.amazonRating(nullProvider)
|
||||
.amazonReviewCount(nullProvider)
|
||||
.goodreadsRating(nullProvider)
|
||||
.goodreadsReviewCount(nullProvider)
|
||||
.hardcoverRating(nullProvider)
|
||||
.hardcoverReviewCount(nullProvider)
|
||||
.lubimyczytacRating(nullProvider)
|
||||
.ranobedbId(nullProvider)
|
||||
.ranobedbRating(nullProvider)
|
||||
.moods(nullProvider)
|
||||
@@ -179,12 +181,14 @@ public class SettingPersistenceHelper {
|
||||
.comicvineId(true)
|
||||
.hardcoverId(true)
|
||||
.googleId(true)
|
||||
.lubimyczytacId(true)
|
||||
.amazonRating(true)
|
||||
.amazonReviewCount(true)
|
||||
.goodreadsRating(true)
|
||||
.goodreadsReviewCount(true)
|
||||
.hardcoverRating(true)
|
||||
.hardcoverReviewCount(true)
|
||||
.lubimyczytacRating(true)
|
||||
.ranobedbId(false)
|
||||
.ranobedbRating(false)
|
||||
.moods(true)
|
||||
|
||||
@@ -438,6 +438,11 @@ public class BookMetadataUpdater {
|
||||
if (comicDto.getLocationsLocked() != null) c.setLocationsLocked(comicDto.getLocationsLocked());
|
||||
|
||||
comicMetadataRepository.save(c);
|
||||
|
||||
comicCharacterRepository.deleteOrphaned();
|
||||
comicTeamRepository.deleteOrphaned();
|
||||
comicLocationRepository.deleteOrphaned();
|
||||
comicCreatorRepository.deleteOrphaned();
|
||||
}
|
||||
|
||||
private void updateComicCharacters(ComicMetadataEntity c, Set<String> characters, MetadataReplaceMode mode) {
|
||||
|
||||
@@ -20,7 +20,11 @@ app:
|
||||
|
||||
server:
|
||||
forward-headers-strategy: native
|
||||
port: 8080
|
||||
port: ${BOOKLORE_PORT:6060}
|
||||
compression:
|
||||
enabled: true
|
||||
mime-types: text/html,text/css,application/javascript,application/json,image/svg+xml,application/xml,application/atom+xml
|
||||
min-response-size: 1024
|
||||
tomcat:
|
||||
relaxed-query-chars: '[,],%,{,},|'
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
DROP TRIGGER IF EXISTS trg_cleanup_orphaned_comic_character;
|
||||
CREATE TRIGGER trg_cleanup_orphaned_comic_character
|
||||
AFTER DELETE
|
||||
ON comic_metadata_character_mapping
|
||||
FOR EACH ROW
|
||||
DELETE
|
||||
FROM comic_character
|
||||
WHERE id = OLD.character_id
|
||||
AND NOT EXISTS (SELECT 1 FROM comic_metadata_character_mapping WHERE character_id = OLD.character_id);
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_cleanup_orphaned_comic_team;
|
||||
CREATE TRIGGER trg_cleanup_orphaned_comic_team
|
||||
AFTER DELETE
|
||||
ON comic_metadata_team_mapping
|
||||
FOR EACH ROW
|
||||
DELETE
|
||||
FROM comic_team
|
||||
WHERE id = OLD.team_id
|
||||
AND NOT EXISTS (SELECT 1 FROM comic_metadata_team_mapping WHERE team_id = OLD.team_id);
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_cleanup_orphaned_comic_location;
|
||||
CREATE TRIGGER trg_cleanup_orphaned_comic_location
|
||||
AFTER DELETE
|
||||
ON comic_metadata_location_mapping
|
||||
FOR EACH ROW
|
||||
DELETE
|
||||
FROM comic_location
|
||||
WHERE id = OLD.location_id
|
||||
AND NOT EXISTS (SELECT 1 FROM comic_metadata_location_mapping WHERE location_id = OLD.location_id);
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_cleanup_orphaned_comic_creator;
|
||||
CREATE TRIGGER trg_cleanup_orphaned_comic_creator
|
||||
AFTER DELETE
|
||||
ON comic_metadata_creator_mapping
|
||||
FOR EACH ROW
|
||||
DELETE
|
||||
FROM comic_creator
|
||||
WHERE id = OLD.creator_id
|
||||
AND NOT EXISTS (SELECT 1 FROM comic_metadata_creator_mapping WHERE creator_id = OLD.creator_id);
|
||||
@@ -3,7 +3,8 @@
|
||||
"index": "/index.html",
|
||||
"navigationUrls": [
|
||||
"/**",
|
||||
"!/api/**"
|
||||
"!/api/**",
|
||||
"!/komga/**"
|
||||
],
|
||||
"assetGroups": [
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {AfterViewInit, ApplicationRef, ChangeDetectorRef, Component, HostListener, inject, NgZone, OnDestroy, OnInit, ViewChild} from '@angular/core';
|
||||
import {AfterViewInit, ChangeDetectorRef, Component, HostListener, inject, OnDestroy, OnInit, ViewChild} from '@angular/core';
|
||||
import {ActivatedRoute, NavigationStart, Router} from '@angular/router';
|
||||
import {ConfirmationService, MenuItem, MessageService} from 'primeng/api';
|
||||
import {PageTitleService} from '../../../../shared/service/page-title.service';
|
||||
@@ -105,8 +105,6 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
protected appSettingsService = inject(AppSettingsService);
|
||||
|
||||
private cdr = inject(ChangeDetectorRef);
|
||||
private ngZone = inject(NgZone);
|
||||
private appRef = inject(ApplicationRef);
|
||||
private activatedRoute = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private messageService = inject(MessageService);
|
||||
@@ -477,11 +475,8 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.bookSelectionService.selectedBooks$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(selectedBooks => {
|
||||
this.ngZone.run(() => {
|
||||
this.selectedCount = selectedBooks.size;
|
||||
this.cdr.detectChanges();
|
||||
this.appRef.tick();
|
||||
});
|
||||
this.selectedCount = selectedBooks.size;
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
API_CONFIG: {
|
||||
BASE_URL: 'http://localhost:8080',
|
||||
BROKER_URL: 'ws://localhost:8080/ws',
|
||||
BASE_URL: 'http://localhost:6060',
|
||||
BROKER_URL: 'ws://localhost:6060/ws',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ services:
|
||||
image: gradle:9.3.1-jdk25-alpine
|
||||
command: sh -c "cd /booklore-api && ./gradlew bootRun"
|
||||
ports:
|
||||
- "${BACKEND_PORT:-8080}:8080"
|
||||
- "${BACKEND_PORT:-6060}:6060"
|
||||
- "${REMOTE_DEBUG_PORT:-5005}:5005"
|
||||
environment:
|
||||
- DATABASE_URL=jdbc:mariadb://backend_db:3306/booklore
|
||||
|
||||
15
entrypoint.sh
Normal file
15
entrypoint.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
USER_ID="${USER_ID:-1000}"
|
||||
GROUP_ID="${GROUP_ID:-1000}"
|
||||
|
||||
# Create group and user if they don't exist
|
||||
if ! getent group "$GROUP_ID" >/dev/null 2>&1; then
|
||||
addgroup -g "$GROUP_ID" -S booklore
|
||||
fi
|
||||
if ! getent passwd "$USER_ID" >/dev/null 2>&1; then
|
||||
adduser -u "$USER_ID" -G "$(getent group "$GROUP_ID" | cut -d: -f1)" -S -D booklore
|
||||
fi
|
||||
|
||||
exec su-exec "$USER_ID:$GROUP_ID" "$@"
|
||||
@@ -62,8 +62,6 @@ spec:
|
||||
containerPort: {{ .Values.service.port }}
|
||||
#protocol: TCP
|
||||
env:
|
||||
- name: BOOKLORE_PORT
|
||||
value: "6060"
|
||||
- name: DATABASE_URL
|
||||
value: jdbc:mariadb://{{ .Release.Name }}-mariadb:{{ .Values.mariadb.primary.service.ports.mysql }}/{{ .Values.mariadb.auth.database }}
|
||||
- name: DATABASE_USERNAME
|
||||
|
||||
@@ -6,22 +6,19 @@ services:
|
||||
# image: ghcr.io/booklore-app/booklore:latest
|
||||
container_name: booklore
|
||||
environment:
|
||||
- USER_ID=0 # Modify this if the volume's ownership is not root
|
||||
- GROUP_ID=0 # Modify this if the volume's ownership is not root
|
||||
- USER_ID=1000 # Modify this if the volume's ownership is not root
|
||||
- GROUP_ID=1000 # Modify this if the volume's ownership is not root
|
||||
- TZ=Etc/UTC
|
||||
- DATABASE_URL=jdbc:mariadb://mariadb:3306/booklore # Only modify this if you're familiar with JDBC and your database setup
|
||||
- DATABASE_USERNAME=booklore # Must match MYSQL_USER defined in the mariadb container
|
||||
- DATABASE_PASSWORD=your_secure_password # Use a strong password; must match MYSQL_PASSWORD defined in the mariadb container
|
||||
- BOOKLORE_PORT=6060 # Port BookLore listens on inside the container; must match container port below
|
||||
- DATABASE_PASSWORD=your_secure_password # Use a strong password; must match MYSQL_PASSWORD defined in the mariadb container
|
||||
- SWAGGER_ENABLED=false # Enable or disable Swagger UI (API docs). Set to 'true' to allow access; 'false' to block access (recommended for production).
|
||||
- FORCE_DISABLE_OIDC=false # Set to 'true' to force-disable OIDC and allow internal login, regardless of UI config
|
||||
depends_on:
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "6060:6060" # HostPort:ContainerPort → Keep both numbers the same, and also ensure the container port matches BOOKLORE_PORT, no exceptions.
|
||||
# All three (host port, container port, BOOKLORE_PORT) must be identical for BookLore to function properly.
|
||||
# Example: To expose on host port 7070, set BOOKLORE_PORT=7070 and use "7070:7070".
|
||||
- "6060:6060" # HostPort:ContainerPort — container port is 6060 by default (configurable via BOOKLORE_PORT env var)
|
||||
volumes:
|
||||
- ./data:/app/data # Application data (settings, metadata, cache, etc.). Persist this folder to retain your library state across container restarts.
|
||||
- ./books:/books # Primary book library folder. Mount your collection here so BookLore can access and organize your books.
|
||||
|
||||
@@ -20,8 +20,6 @@ Environment=TZ=Etc/UTC
|
||||
Secret=booklore_db_pass,type=env,target=DATABASE_PASSWORD
|
||||
Environment=DATABASE_URL=jdbc:mariadb://localhost:3306/booklore
|
||||
Environment=DATABASE_USERNAME=booklore
|
||||
Environment=BOOKLORE_PORT=6060
|
||||
|
||||
HealthCmd=wget -q -O - http://localhost:6060/api/v1/healthcheck
|
||||
HealthInterval=1m
|
||||
HealthRetries=5
|
||||
|
||||
89
nginx.conf
89
nginx.conf
@@ -1,89 +0,0 @@
|
||||
# the events block is required
|
||||
events {}
|
||||
|
||||
http {
|
||||
# include the default mime.types to map file extensions to MIME types
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
# Set max request body size to 100MB (adjust as needed)
|
||||
client_max_body_size 1000M;
|
||||
|
||||
proxy_buffer_size 128k;
|
||||
proxy_buffers 4 256k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
large_client_header_buffers 8 32k;
|
||||
|
||||
server {
|
||||
listen ${BOOKLORE_PORT};
|
||||
listen [::]:${BOOKLORE_PORT};
|
||||
|
||||
# Set the root directory for the server (Angular app)
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
# Set the default index file for the server
|
||||
index index.html;
|
||||
|
||||
# Serve Angular UI
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html; # Fallback to index.html for Angular routing
|
||||
|
||||
location ~* \.mjs$ {
|
||||
# target only *.mjs files
|
||||
# now we can safely override types since we are only
|
||||
# targeting a single file extension.
|
||||
types {
|
||||
text/javascript mjs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Proxy API requests that start with /api/ to the backend
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
|
||||
# Disable buffering for streaming responses
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_set_header Connection '';
|
||||
proxy_http_version 1.1;
|
||||
chunked_transfer_encoding on;
|
||||
|
||||
# Set timeouts for long-running connections
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
}
|
||||
|
||||
# Proxy API requests that start with /komga/ to the backend
|
||||
# we can't use /api because Komga clients expect /komga/api/v1
|
||||
location /komga/ {
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
|
||||
# Disable buffering for streaming responses
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_set_header Connection '';
|
||||
proxy_http_version 1.1;
|
||||
chunked_transfer_encoding on;
|
||||
|
||||
# Set timeouts for long-running connections
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
}
|
||||
|
||||
# Proxy WebSocket requests (ws://) to the backend
|
||||
location /ws {
|
||||
proxy_pass http://localhost:8080/ws; # Backend WebSocket endpoint
|
||||
proxy_http_version 1.1; # Ensure HTTP 1.1 is used for WebSocket connection
|
||||
proxy_set_header Upgrade $http_upgrade; # Pass the upgrade header
|
||||
proxy_set_header Connection 'upgrade'; # Pass the connection header
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
start.sh
21
start.sh
@@ -1,21 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Set default and export so envsubst sees it
|
||||
: "${BOOKLORE_PORT:=6060}"
|
||||
export BOOKLORE_PORT
|
||||
|
||||
# Use envsubst safely
|
||||
TMP_CONF="/tmp/nginx.conf.tmp"
|
||||
envsubst '${BOOKLORE_PORT}' < /etc/nginx/nginx.conf > "$TMP_CONF"
|
||||
|
||||
# Move to final location
|
||||
mv "$TMP_CONF" /etc/nginx/nginx.conf
|
||||
|
||||
# Disable nginx IPv6 listener when IPv6 is disabled on host
|
||||
[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" = "0" ] || sed -i '/^[[:space:]]*listen \[\:\:\]:6060;$/d' /etc/nginx/nginx.conf
|
||||
|
||||
# Start nginx in background
|
||||
nginx -g 'daemon off;' &
|
||||
|
||||
# Start Spring Boot in foreground
|
||||
su-exec ${USER_ID:-0}:${GROUP_ID:-0} java -jar /app/app.jar
|
||||
Reference in New Issue
Block a user