diff --git a/.gitignore b/.gitignore index 5d839f3..bbaa093 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ build/ ### Custom ### /data/ /backend/src/main/resources/static/ -/docker/docker-compose.yml \ No newline at end of file +/docker/docker-compose.yml +/.gameyfin/ diff --git a/backend/pom.xml b/backend/pom.xml index 9734656..6906b6d 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -16,12 +16,16 @@ 18 + 1.6.9 1.7.1 2.11.0 1.21 3.11.4 3.21.3 + + + 3.1.0 @@ -140,6 +144,7 @@ **/*.properties **/*.yml **/*.yaml + **/*.json **/*.txt **/*.js **/*.css @@ -166,6 +171,7 @@ maven-resources-plugin + ${maven-resources-plugin.version} copy-resources diff --git a/backend/src/main/java/de/grimsi/gameyfin/GameyfinApplication.java b/backend/src/main/java/de/grimsi/gameyfin/GameyfinApplication.java index 4c79e1b..f17d349 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/GameyfinApplication.java +++ b/backend/src/main/java/de/grimsi/gameyfin/GameyfinApplication.java @@ -8,7 +8,7 @@ public class GameyfinApplication { public static void main(String[] args) { new SpringApplicationBuilder(GameyfinApplication.class) - .properties("spring.config.name=application,gameyfin,database,secure") + .properties( "file.encoding=UTF-8", "spring.config.name=application,gameyfin,database,secure") .build() .run(args); } diff --git a/backend/src/main/java/de/grimsi/gameyfin/config/FilesystemConfig.java b/backend/src/main/java/de/grimsi/gameyfin/config/FilesystemConfig.java new file mode 100644 index 0000000..5b9cf98 --- /dev/null +++ b/backend/src/main/java/de/grimsi/gameyfin/config/FilesystemConfig.java @@ -0,0 +1,66 @@ +package de.grimsi.gameyfin.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.*; +import org.springframework.util.PropertyPlaceholderHelper; + +import javax.sql.DataSource; +import java.util.Arrays; +import java.util.Properties; +import java.util.stream.StreamSupport; + +@Configuration +public class FilesystemConfig { + + @Value("#{'${gameyfin.sources}'.split(',')[0]}") + private String firstLibraryPath; + + @Autowired + Environment env; + + @Autowired + public void setConfigurableEnvironment(ConfigurableEnvironment env) { + Properties props = new Properties(); + props.setProperty("gameyfin.db", "%s/.gameyfin/db".formatted(firstLibraryPath)); + props.setProperty("gameyfin.cache", "%s/.gameyfin/cache".formatted(firstLibraryPath)); + env.getPropertySources().addFirst(new PropertiesPropertySource("gameyfinFilesystemProperties", props)); + } + + /** + * This bean is needed so Spring initializes the data source after we are done messing with the configuration environment + * @return DataSource + */ + @ConfigurationProperties(prefix = "spring.datasource") + @Bean + @Primary + public DataSource getDataSource() { + + Properties properties = loadAllProperties(); + + return DataSourceBuilder + .create() + .url(properties.getProperty("spring.datasource.url")) + .build(); + } + + private Properties loadAllProperties() { + Properties props = new Properties(); + + MutablePropertySources propSrcs = ((AbstractEnvironment) env).getPropertySources(); + + StreamSupport.stream(propSrcs.spliterator(), false) + .filter(ps -> ps instanceof EnumerablePropertySource) + .map(ps -> ((EnumerablePropertySource) ps).getPropertyNames()) + .flatMap(Arrays::stream) + .forEach(propName -> props.setProperty(propName, env.getProperty(propName))); + + return props; + } + +} diff --git a/backend/src/main/java/de/grimsi/gameyfin/config/SecureProperties.java b/backend/src/main/java/de/grimsi/gameyfin/config/SecureProperties.java index 6d66527..3140b37 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/config/SecureProperties.java +++ b/backend/src/main/java/de/grimsi/gameyfin/config/SecureProperties.java @@ -16,7 +16,7 @@ public class SecureProperties { @Autowired public void setConfigurableEnvironment(ConfigurableEnvironment env) { try { - Resource resource = new ClassPathResource("/config/secure.properties"); + Resource resource = new ClassPathResource("/config/secure.yml"); env.getPropertySources().addFirst(new PropertiesPropertySource(Objects.requireNonNull(resource.getFilename()), PropertiesLoaderUtils.loadProperties(resource))); } catch (Exception ex) { throw new RuntimeException(ex.getMessage(), ex); diff --git a/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java b/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java index 3531176..f5c7308 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java +++ b/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java @@ -10,13 +10,13 @@ import de.grimsi.gameyfin.repositories.DetectedGameRepository; import de.grimsi.gameyfin.repositories.UnmappableFileRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.util.StopWatch; import java.io.IOException; -import java.nio.file.*; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; @@ -29,25 +29,29 @@ import static de.grimsi.gameyfin.util.FilenameUtil.hasGameArchiveExtension; @RequiredArgsConstructor public class LibraryService { - @Value("${gameyfin.root}") - private String rootFolderPath; - + @Value("${gameyfin.sources}") + private List libraryFolders; private final IgdbWrapper igdbWrapper; private final DetectedGameRepository detectedGameRepository; private final UnmappableFileRepository unmappableFileRepository; public List getGameFiles() { + List gamefiles = new ArrayList<>(); - Path rootFolder = Path.of(rootFolderPath); + libraryFolders.stream().map(Path::of).forEach( + folder -> { + try (Stream stream = Files.list(folder)) { + // return all sub-folders (non-recursive) and files that have an extension that indicates that they are a downloadable file + List gameFilesFromThisFolder = stream.filter(p -> Files.isDirectory(p) || hasGameArchiveExtension(p)).toList(); + gamefiles.addAll(gameFilesFromThisFolder); - try (Stream stream = Files.list(rootFolder)) { - // return all sub-folders (non-recursive) and files that have an extension that indicates that they are a downloadable file - return stream - .filter(p -> Files.isDirectory(p) || hasGameArchiveExtension(p)) - .toList(); - } catch (IOException e) { - throw new RuntimeException("Error while opening root folder", e); - } + } catch (IOException e) { + throw new RuntimeException("Error while opening library folder '%s'".formatted(folder), e); + } + } + ); + + return gamefiles; } public void scanGameLibrary() { diff --git a/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..e346e41 --- /dev/null +++ b/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,9 @@ +{ + "properties": [ + { + "name": "gameyfin.sources", + "type": "java.lang.String[]", + "description": "List of directories Gameyfin should scan for games." + } + ] +} \ No newline at end of file diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml index 30efaba..37e7822 100644 --- a/backend/src/main/resources/application-dev.yml +++ b/backend/src/main/resources/application-dev.yml @@ -1,8 +1,6 @@ gameyfin: user: admin password: password - cache: ${gameyfin.root}\.gameyfin\cache - db: ${gameyfin.root}\.gameyfin\db logging: level: diff --git a/backend/src/main/resources/config/database.properties b/backend/src/main/resources/config/database.properties deleted file mode 100644 index 6123b15..0000000 --- a/backend/src/main/resources/config/database.properties +++ /dev/null @@ -1,13 +0,0 @@ -# This file contains properties related to the database configuration -# -# -spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true -spring.datasource.db-name=gameyfin_db -spring.datasource.url=jdbc:h2:file:${gameyfin.db}/${spring.datasource.db-name} -spring.datasource.username=gfadmin -spring.datasource.password=gameyfin -spring.datasource.driverClassName=org.h2.Driver -spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -spring.jpa.hibernate.ddl-auto=update -spring.jpa.open-in-view=true -spring.jpa.properties.hibernate.event.merge.entity_copy_observer=allow \ No newline at end of file diff --git a/backend/src/main/resources/config/database.yml b/backend/src/main/resources/config/database.yml new file mode 100644 index 0000000..0e02ae7 --- /dev/null +++ b/backend/src/main/resources/config/database.yml @@ -0,0 +1,18 @@ +spring: + jpa: + open-in-view: true + properties: + hibernate: + enable_lazy_load_no_trans: true + event: + merge: + entity_copy_observer: allow + database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: update + datasource: + username: gfadmin + password: gameyfin + db-name: gameyfin_db + url: jdbc:h2:file:${gameyfin.db}/${spring.datasource.db-name} + driverClassName: org.h2.Driver \ No newline at end of file diff --git a/backend/src/main/resources/config/gameyfin.properties b/backend/src/main/resources/config/gameyfin.properties deleted file mode 100644 index 776836d..0000000 --- a/backend/src/main/resources/config/gameyfin.properties +++ /dev/null @@ -1,28 +0,0 @@ -# This file contains properties related to the configuration of Gameyfin -# -# -# Username and password for the web interface -gameyfin.user= -gameyfin.password= - -# Root folder of your game library -gameyfin.root= -# Folders where gameyfin will store cached images and the database -gameyfin.cache=${gameyfin.root}/.gameyfin/cache -gameyfin.db=${gameyfin.root}/.gameyfin/db - -# File extensions which gameyfin will recognize as game files -gameyfin.file-extensions=iso, zip, rar, 7z, exe - -# List of IGDB platform enums to limit search results. For possible values see: https://api-docs.igdb.com/#platform -gameyfin.igdb.config.preferred-platforms=6 - -# Twitch Client ID and Client Secret -gameyfin.igdb.api.client-id= -gameyfin.igdb.api.client-secret= - -# The IGDB API has a rate limit of 4 req/s -gameyfin.igdb.api.max-requests-per-second=4 - -# According to the docs, there is a maximum of 8 concurrent requests, but in my tests the actual limit was 4 and even then it sometimes failed, so I set it to 2 to be sure -gameyfin.igdb.api.max-concurrent-requests=2 \ No newline at end of file diff --git a/backend/src/main/resources/config/gameyfin.yml b/backend/src/main/resources/config/gameyfin.yml new file mode 100644 index 0000000..f70079e --- /dev/null +++ b/backend/src/main/resources/config/gameyfin.yml @@ -0,0 +1,10 @@ +gameyfin: + file-extensions: iso, zip, rar, 7z, exe + igdb: + api: + client-id: + client-secret: + max-concurrent-requests: 2 + max-requests-per-second: 4 + config: + preferred-platforms: 6 \ No newline at end of file diff --git a/backend/src/main/resources/config/secure.properties b/backend/src/main/resources/config/secure.properties deleted file mode 100644 index cda7c4e..0000000 --- a/backend/src/main/resources/config/secure.properties +++ /dev/null @@ -1,19 +0,0 @@ -# This file contains properties that are *NOT* safe to override by the user -# In theory a user should not be able to override them since they will be loaded from the classpath at launch, overriding existing user properties with the same key -# -# -# System Info -application.name=Gameyfin -application.version=@project.version@ -# API -server.servlet.context-path=/ -# Spring Actuator -management.endpoints.enabled-by-default=false -management.endpoint.health.enabled=true -# Server -server.error.include-stacktrace=never -spring.mvc.async.request-timeout=-1 -# Jackson JSON Mapping -spring.jackson.default-property-inclusion=non_null -spring.jackson.mapper.accept-case-insensitive-enums=true -spring.jackson.deserialization.fail-on-unknown-properties=false diff --git a/backend/src/main/resources/config/secure.yml b/backend/src/main/resources/config/secure.yml new file mode 100644 index 0000000..cf80710 --- /dev/null +++ b/backend/src/main/resources/config/secure.yml @@ -0,0 +1,27 @@ +application: + version: '@project.version@' + name: Gameyfin + +server: + servlet: + context-path: / + error: + include-stacktrace: never + +spring: + jackson: + deserialization: + fail-on-unknown-properties: false + default-property-inclusion: non_null + mapper: + accept-case-insensitive-enums: true + mvc: + async: + request-timeout: -1 + +management: + endpoint: + health: + enabled: true + endpoints: + enabled-by-default: false \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index c6077b7..743516b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,10 +1,10 @@ FROM openjdk:18 -ENV GAMEYFIN_ROOT=/opt/gameyfin-library +ENV GAMEYFIN_SOURCES=/opt/gameyfin-library RUN groupadd gameyfin && useradd gameyfin -g gameyfin && \ - mkdir -p /opt/gameyfin ${GAMEYFIN_ROOT} && \ - chown -R gameyfin:gameyfin /opt/gameyfin ${GAMEYFIN_ROOT} + mkdir -p /opt/gameyfin ${GAMEYFIN_SOURCES} && \ + chown -R gameyfin:gameyfin /opt/gameyfin ${GAMEYFIN_SOURCES} USER gameyfin:gameyfin