diff --git a/README.md b/README.md index f427324..d819e31 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,32 @@ make docker-build TWITCH_CLIENT_ID=your_id TWITCH_CLIENT_SECRET=your_secret docker run -p 8080:8080 imgfloat:latest ``` +### Data storage configuration +- `IMGFLOAT_DB_PATH` – filesystem location for the SQLite database (default: `imgfloat.db`). +- `IMGFLOAT_ASSETS_PATH` – root directory where uploaded assets are persisted (default: `assets`). +- `IMGFLOAT_PREVIEWS_PATH` – root directory for generated preview images (default: `previews`). + +### Docker Compose example with persistent storage +```yaml +services: + imgfloat: + image: imgfloat:latest + environment: + TWITCH_CLIENT_ID: your_id + TWITCH_CLIENT_SECRET: your_secret + TWITCH_REDIRECT_URI: https://yourdomain/login/oauth2/code/twitch + IMGFLOAT_DB_PATH: /var/lib/imgfloat/imgfloat.db + IMGFLOAT_ASSETS_PATH: /var/lib/imgfloat/assets + IMGFLOAT_PREVIEWS_PATH: /var/lib/imgfloat/previews + ports: + - "8080:8080" + volumes: + - ./data/db:/var/lib/imgfloat + - ./data/assets:/var/lib/imgfloat/assets + - ./data/previews:/var/lib/imgfloat/previews +``` +The `volumes` map local folders into the container so database and uploaded files survive container restarts. + ### OAuth configuration Spring Boot reads Twitch credentials from `TWITCH_CLIENT_ID` and `TWITCH_CLIENT_SECRET`. The redirect URI comes from `TWITCH_REDIRECT_URI` (defaulting to `{baseUrl}/login/oauth2/code/twitch`). diff --git a/src/main/java/com/imgfloat/app/service/ChannelDirectoryService.java b/src/main/java/com/imgfloat/app/service/ChannelDirectoryService.java index ba925cc..c106f50 100644 --- a/src/main/java/com/imgfloat/app/service/ChannelDirectoryService.java +++ b/src/main/java/com/imgfloat/app/service/ChannelDirectoryService.java @@ -19,6 +19,7 @@ import org.jcodec.common.model.Picture; import org.jcodec.scale.AWTUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -57,19 +58,23 @@ import org.w3c.dom.NodeList; public class ChannelDirectoryService { private static final int MIN_GIF_DELAY_MS = 20; private static final String PREVIEW_MEDIA_TYPE = "image/png"; - private static final Path ASSET_ROOT = Paths.get("assets"); - private static final Path PREVIEW_ROOT = Paths.get("previews"); private static final Logger logger = LoggerFactory.getLogger(ChannelDirectoryService.class); private final ChannelRepository channelRepository; private final AssetRepository assetRepository; private final SimpMessagingTemplate messagingTemplate; + private final Path assetRoot; + private final Path previewRoot; public ChannelDirectoryService(ChannelRepository channelRepository, AssetRepository assetRepository, - SimpMessagingTemplate messagingTemplate) { + SimpMessagingTemplate messagingTemplate, + @Value("${IMGFLOAT_ASSETS_PATH:assets}") String assetRoot, + @Value("${IMGFLOAT_PREVIEWS_PATH:previews}") String previewRoot) { this.channelRepository = channelRepository; this.assetRepository = assetRepository; this.messagingTemplate = messagingTemplate; + this.assetRoot = Paths.get(assetRoot); + this.previewRoot = Paths.get(previewRoot); } public Channel getOrCreateChannel(String broadcaster) { @@ -408,7 +413,7 @@ public class ChannelDirectoryService { if (assetBytes == null || assetBytes.length == 0) { throw new IOException("Asset content is empty"); } - Path directory = ASSET_ROOT.resolve(normalize(broadcaster)); + Path directory = assetRoot.resolve(normalize(broadcaster)); Files.createDirectories(directory); String extension = extensionForMediaType(mediaType); Path assetFile = directory.resolve(assetId + extension); @@ -460,7 +465,7 @@ public class ChannelDirectoryService { if (previewBytes == null || previewBytes.length == 0) { return null; } - Path directory = PREVIEW_ROOT.resolve(normalize(broadcaster)); + Path directory = previewRoot.resolve(normalize(broadcaster)); Files.createDirectories(directory); Path previewFile = directory.resolve(assetId + ".png"); Files.write(previewFile, previewBytes, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b0313d7..41e7bc1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -23,7 +23,7 @@ spring: thymeleaf: cache: false datasource: - url: jdbc:sqlite:imgfloat.db?busy_timeout=5000&journal_mode=WAL + url: jdbc:sqlite:${IMGFLOAT_DB_PATH:imgfloat.db}?busy_timeout=5000&journal_mode=WAL driver-class-name: org.sqlite.JDBC hikari: connection-init-sql: "PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;" diff --git a/src/test/java/com/imgfloat/app/ChannelDirectoryServiceTest.java b/src/test/java/com/imgfloat/app/ChannelDirectoryServiceTest.java index 3d83755..1857b73 100644 --- a/src/test/java/com/imgfloat/app/ChannelDirectoryServiceTest.java +++ b/src/test/java/com/imgfloat/app/ChannelDirectoryServiceTest.java @@ -17,6 +17,8 @@ import org.springframework.mock.web.MockMultipartFile; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Collection; import java.util.List; import java.util.Map; @@ -40,12 +42,15 @@ class ChannelDirectoryServiceTest { private AssetRepository assetRepository; @BeforeEach - void setup() { + void setup() throws Exception { messagingTemplate = mock(SimpMessagingTemplate.class); channelRepository = mock(ChannelRepository.class); assetRepository = mock(AssetRepository.class); setupInMemoryPersistence(); - service = new ChannelDirectoryService(channelRepository, assetRepository, messagingTemplate); + Path assetRoot = Files.createTempDirectory("imgfloat-assets-test"); + Path previewRoot = Files.createTempDirectory("imgfloat-previews-test"); + service = new ChannelDirectoryService(channelRepository, assetRepository, messagingTemplate, + assetRoot.toString(), previewRoot.toString()); } @Test