mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 03:39:26 +00:00
Improve error reporting
This commit is contained in:
7
Makefile
7
Makefile
@@ -3,12 +3,16 @@
|
|||||||
|
|
||||||
.DEFAULT_GOAL := build
|
.DEFAULT_GOAL := build
|
||||||
|
|
||||||
|
IMGFLOAT_DB_PATH ?= ./imgfloat.db
|
||||||
IMGFLOAT_ASSETS_PATH ?= ./assets
|
IMGFLOAT_ASSETS_PATH ?= ./assets
|
||||||
IMGFLOAT_PREVIEWS_PATH ?= ./previews
|
IMGFLOAT_PREVIEWS_PATH ?= ./previews
|
||||||
|
SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE ?= 10MB
|
||||||
SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE ?= 10MB
|
SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE ?= 10MB
|
||||||
RUNTIME_ENV = IMGFLOAT_ASSETS_PATH=$(IMGFLOAT_ASSETS_PATH) \
|
RUNTIME_ENV = IMGFLOAT_ASSETS_PATH=$(IMGFLOAT_ASSETS_PATH) \
|
||||||
IMGFLOAT_PREVIEWS_PATH=$(IMGFLOAT_PREVIEWS_PATH) \
|
IMGFLOAT_PREVIEWS_PATH=$(IMGFLOAT_PREVIEWS_PATH) \
|
||||||
SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE=$(SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE)
|
IMGFLOAT_DB_PATH=$(IMGFLOAT_DB_PATH) \
|
||||||
|
SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE=$(SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE) \
|
||||||
|
SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE=$(SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE)
|
||||||
WATCHDIR = ./src/main
|
WATCHDIR = ./src/main
|
||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
@@ -37,3 +41,4 @@ ssl:
|
|||||||
mkdir -p local
|
mkdir -p local
|
||||||
keytool -genkeypair -alias imgfloat -keyalg RSA -keystore local/keystore.p12 -storetype PKCS12 -storepass changeit -keypass changeit -dname "CN=localhost" -validity 365
|
keytool -genkeypair -alias imgfloat -keyalg RSA -keystore local/keystore.p12 -storetype PKCS12 -storepass changeit -keypass changeit -dname "CN=localhost" -validity 365
|
||||||
echo "Use SSL_ENABLED=true SSL_KEYSTORE_PATH=file:$$PWD/local/keystore.p12"
|
echo "Use SSL_ENABLED=true SSL_KEYSTORE_PATH=file:$$PWD/local/keystore.p12"
|
||||||
|
|
||||||
|
|||||||
5
pom.xml
5
pom.xml
@@ -14,6 +14,8 @@
|
|||||||
<java.version>17</java.version>
|
<java.version>17</java.version>
|
||||||
<spring.boot.version>3.2.5</spring.boot.version>
|
<spring.boot.version>3.2.5</spring.boot.version>
|
||||||
<hibernate.version>6.4.4.Final</hibernate.version>
|
<hibernate.version>6.4.4.Final</hibernate.version>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
@@ -140,8 +142,7 @@
|
|||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<version>3.11.0</version>
|
<version>3.11.0</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<source>${java.version}</source>
|
<release>17</release>
|
||||||
<target>${java.version}</target>
|
|
||||||
<compilerArgs>
|
<compilerArgs>
|
||||||
<arg>-parameters</arg>
|
<arg>-parameters</arg>
|
||||||
</compilerArgs>
|
</compilerArgs>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.util.unit.DataSize;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
@@ -19,18 +20,28 @@ public class SystemEnvironmentValidator {
|
|||||||
private String twitchClientSecret;
|
private String twitchClientSecret;
|
||||||
@Value("${spring.servlet.multipart.max-file-size:#{null}}")
|
@Value("${spring.servlet.multipart.max-file-size:#{null}}")
|
||||||
private String springMaxFileSize;
|
private String springMaxFileSize;
|
||||||
|
@Value("${spring.servlet.multipart.max-request-size:#{null}}")
|
||||||
|
private String springMaxRequestSize;
|
||||||
@Value("${IMGFLOAT_ASSETS_PATH:#{null}}")
|
@Value("${IMGFLOAT_ASSETS_PATH:#{null}}")
|
||||||
private String assetsPath;
|
private String assetsPath;
|
||||||
@Value("${IMGFLOAT_PREVIEWS_PATH:#{null}}")
|
@Value("${IMGFLOAT_PREVIEWS_PATH:#{null}}")
|
||||||
private String previewsPath;
|
private String previewsPath;
|
||||||
|
@Value("${IMGFLOAT_DB_PATH}")
|
||||||
|
private String dbPath;
|
||||||
|
|
||||||
|
private long maxUploadBytes;
|
||||||
|
private long maxRequestBytes;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void validate() {
|
public void validate() {
|
||||||
StringBuilder missing = new StringBuilder();
|
StringBuilder missing = new StringBuilder();
|
||||||
|
|
||||||
long maxUploadBytes = parseSizeToBytes(springMaxFileSize);
|
maxUploadBytes = DataSize.parse(springMaxFileSize).toBytes();
|
||||||
|
maxRequestBytes = DataSize.parse(springMaxRequestSize).toBytes();
|
||||||
checkLong(maxUploadBytes, "SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE", missing);
|
checkLong(maxUploadBytes, "SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE", missing);
|
||||||
|
checkLong(maxRequestBytes, "SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE", missing);
|
||||||
checkString(twitchClientId, "TWITCH_CLIENT_ID", missing);
|
checkString(twitchClientId, "TWITCH_CLIENT_ID", missing);
|
||||||
|
checkString(dbPath, "IMGFLOAT_DB_PATH", missing);
|
||||||
checkString(twitchClientSecret, "TWITCH_CLIENT_SECRET", missing);
|
checkString(twitchClientSecret, "TWITCH_CLIENT_SECRET", missing);
|
||||||
checkString(assetsPath, "IMGFLOAT_ASSETS_PATH", missing);
|
checkString(assetsPath, "IMGFLOAT_ASSETS_PATH", missing);
|
||||||
checkString(previewsPath, "IMGFLOAT_PREVIEWS_PATH", missing);
|
checkString(previewsPath, "IMGFLOAT_PREVIEWS_PATH", missing);
|
||||||
@@ -49,6 +60,10 @@ public class SystemEnvironmentValidator {
|
|||||||
springMaxFileSize,
|
springMaxFileSize,
|
||||||
maxUploadBytes
|
maxUploadBytes
|
||||||
);
|
);
|
||||||
|
log.info(" - SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE: {} ({} bytes)",
|
||||||
|
springMaxRequestSize,
|
||||||
|
maxRequestBytes
|
||||||
|
);
|
||||||
log.info(" - IMGFLOAT_ASSETS_PATH: {}", assetsPath);
|
log.info(" - IMGFLOAT_ASSETS_PATH: {}", assetsPath);
|
||||||
log.info(" - IMGFLOAT_PREVIEWS_PATH: {}", previewsPath);
|
log.info(" - IMGFLOAT_PREVIEWS_PATH: {}", previewsPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package dev.kruhlmann.imgfloat.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.util.unit.DataSize;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class UploadLimitsConfig {
|
||||||
|
|
||||||
|
private final Environment environment;
|
||||||
|
|
||||||
|
public UploadLimitsConfig(Environment environment) {
|
||||||
|
this.environment = environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public long uploadLimitBytes() {
|
||||||
|
String value = environment.getProperty("spring.servlet.multipart.max-file-size");
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"spring.servlet.multipart.max-file-size is not set"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DataSize.parse(value).toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public long uploadRequestLimitBytes() {
|
||||||
|
String value = environment.getProperty("spring.servlet.multipart.max-request-size");
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"spring.servlet.multipart.max-request-size is not set"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DataSize.parse(value).toBytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -247,7 +247,7 @@ public class ChannelApiController {
|
|||||||
|
|
||||||
if (authorized) {
|
if (authorized) {
|
||||||
LOG.debug("Serving asset {} for broadcaster {} to authenticated user {}", assetId, broadcaster, authentication.getName());
|
LOG.debug("Serving asset {} for broadcaster {} to authenticated user {}", assetId, broadcaster, authentication.getName());
|
||||||
return channelDirectoryService.getAssetContent(broadcaster, assetId)
|
return channelDirectoryService.getAssetContent(assetId)
|
||||||
.map(content -> ResponseEntity.ok()
|
.map(content -> ResponseEntity.ok()
|
||||||
.header("X-Content-Type-Options", "nosniff")
|
.header("X-Content-Type-Options", "nosniff")
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION, contentDispositionFor(content.mediaType()))
|
.header(HttpHeaders.CONTENT_DISPOSITION, contentDispositionFor(content.mediaType()))
|
||||||
@@ -256,7 +256,7 @@ public class ChannelApiController {
|
|||||||
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Asset not found"));
|
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Asset not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return channelDirectoryService.getVisibleAssetContent(broadcaster, assetId)
|
return channelDirectoryService.getVisibleAssetContent(assetId)
|
||||||
.map(content -> ResponseEntity.ok()
|
.map(content -> ResponseEntity.ok()
|
||||||
.header("X-Content-Type-Options", "nosniff")
|
.header("X-Content-Type-Options", "nosniff")
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION, contentDispositionFor(content.mediaType()))
|
.header(HttpHeaders.CONTENT_DISPOSITION, contentDispositionFor(content.mediaType()))
|
||||||
@@ -278,7 +278,7 @@ public class ChannelApiController {
|
|||||||
|
|
||||||
if (authorized) {
|
if (authorized) {
|
||||||
LOG.debug("Serving preview for asset {} for broadcaster {}", assetId, broadcaster);
|
LOG.debug("Serving preview for asset {} for broadcaster {}", assetId, broadcaster);
|
||||||
return channelDirectoryService.getAssetPreview(broadcaster, assetId, true)
|
return channelDirectoryService.getAssetPreview(assetId, true)
|
||||||
.map(content -> ResponseEntity.ok()
|
.map(content -> ResponseEntity.ok()
|
||||||
.header("X-Content-Type-Options", "nosniff")
|
.header("X-Content-Type-Options", "nosniff")
|
||||||
.contentType(MediaType.parseMediaType(content.mediaType()))
|
.contentType(MediaType.parseMediaType(content.mediaType()))
|
||||||
@@ -286,7 +286,7 @@ public class ChannelApiController {
|
|||||||
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Preview not found"));
|
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Preview not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return channelDirectoryService.getAssetPreview(broadcaster, assetId, false)
|
return channelDirectoryService.getAssetPreview(assetId, false)
|
||||||
.map(content -> ResponseEntity.ok()
|
.map(content -> ResponseEntity.ok()
|
||||||
.header("X-Content-Type-Options", "nosniff")
|
.header("X-Content-Type-Options", "nosniff")
|
||||||
.contentType(MediaType.parseMediaType(content.mediaType()))
|
.contentType(MediaType.parseMediaType(content.mediaType()))
|
||||||
@@ -307,7 +307,7 @@ public class ChannelApiController {
|
|||||||
OAuth2AuthenticationToken authentication) {
|
OAuth2AuthenticationToken authentication) {
|
||||||
String login = TwitchUser.from(authentication).login();
|
String login = TwitchUser.from(authentication).login();
|
||||||
ensureAuthorized(broadcaster, login);
|
ensureAuthorized(broadcaster, login);
|
||||||
boolean removed = channelDirectoryService.deleteAsset(broadcaster, assetId);
|
boolean removed = channelDirectoryService.deleteAsset(assetId);
|
||||||
if (!removed) {
|
if (!removed) {
|
||||||
LOG.warn("Attempt to delete missing asset {} on {} by {}", assetId, broadcaster, login);
|
LOG.warn("Attempt to delete missing asset {} on {} by {}", assetId, broadcaster, login);
|
||||||
throw new ResponseStatusException(NOT_FOUND, "Asset not found");
|
throw new ResponseStatusException(NOT_FOUND, "Asset not found");
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import dev.kruhlmann.imgfloat.service.ChannelDirectoryService;
|
|||||||
import dev.kruhlmann.imgfloat.service.VersionService;
|
import dev.kruhlmann.imgfloat.service.VersionService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.util.unit.DataSize;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import static org.springframework.http.HttpStatus.FORBIDDEN;
|
import static org.springframework.http.HttpStatus.FORBIDDEN;
|
||||||
@@ -17,6 +19,9 @@ public class ViewController {
|
|||||||
private final ChannelDirectoryService channelDirectoryService;
|
private final ChannelDirectoryService channelDirectoryService;
|
||||||
private final VersionService versionService;
|
private final VersionService versionService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private long uploadLimitBytes;
|
||||||
|
|
||||||
public ViewController(ChannelDirectoryService channelDirectoryService, VersionService versionService) {
|
public ViewController(ChannelDirectoryService channelDirectoryService, VersionService versionService) {
|
||||||
this.channelDirectoryService = channelDirectoryService;
|
this.channelDirectoryService = channelDirectoryService;
|
||||||
this.versionService = versionService;
|
this.versionService = versionService;
|
||||||
@@ -55,6 +60,8 @@ public class ViewController {
|
|||||||
LOG.info("Rendering admin console for {} (requested by {})", broadcaster, login);
|
LOG.info("Rendering admin console for {} (requested by {})", broadcaster, login);
|
||||||
model.addAttribute("broadcaster", broadcaster.toLowerCase());
|
model.addAttribute("broadcaster", broadcaster.toLowerCase());
|
||||||
model.addAttribute("username", login);
|
model.addAttribute("username", login);
|
||||||
|
model.addAttribute("uploadLimitBytes", uploadLimitBytes);
|
||||||
|
|
||||||
return "admin";
|
return "admin";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,109 +47,93 @@ public class AssetStorageService {
|
|||||||
this.previewRoot = Paths.get(previewRoot).normalize().toAbsolutePath();
|
this.previewRoot = Paths.get(previewRoot).normalize().toAbsolutePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String storeAsset(String broadcaster, String assetId, byte[] assetBytes, String mediaType)
|
public void storeAsset(String broadcaster, String assetId, byte[] assetBytes, String mediaType)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
|
||||||
if (assetBytes == null || assetBytes.length == 0) {
|
if (assetBytes == null || assetBytes.length == 0) {
|
||||||
throw new IOException("Asset content is empty");
|
throw new IOException("Asset content is empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
String safeUser = sanitizeUserSegment(broadcaster);
|
Path file = assetPath(broadcaster, assetId, mediaType);
|
||||||
Path directory = safeJoin(assetRoot, safeUser);
|
Files.createDirectories(file.getParent());
|
||||||
Files.createDirectories(directory);
|
|
||||||
|
|
||||||
String extension = resolveExtension(mediaType);
|
|
||||||
Path file = directory.resolve(assetId + extension);
|
|
||||||
|
|
||||||
Files.write(file, assetBytes,
|
Files.write(file, assetBytes,
|
||||||
StandardOpenOption.CREATE,
|
StandardOpenOption.CREATE,
|
||||||
StandardOpenOption.TRUNCATE_EXISTING,
|
StandardOpenOption.TRUNCATE_EXISTING,
|
||||||
StandardOpenOption.WRITE);
|
StandardOpenOption.WRITE);
|
||||||
|
logger.info("Wrote asset to {}", file.toString());
|
||||||
return assetRoot.relativize(file).toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String storePreview(String broadcaster, String assetId, byte[] previewBytes)
|
public void storePreview(String broadcaster, String assetId, byte[] previewBytes)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
|
||||||
if (previewBytes == null || previewBytes.length == 0) {
|
if (previewBytes == null || previewBytes.length == 0) return;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String safeUser = sanitizeUserSegment(broadcaster);
|
Path file = previewPath(broadcaster, assetId);
|
||||||
Path directory = safeJoin(previewRoot, safeUser);
|
Files.createDirectories(file.getParent());
|
||||||
Files.createDirectories(directory);
|
|
||||||
|
|
||||||
Path file = directory.resolve(assetId + ".png");
|
|
||||||
|
|
||||||
Files.write(file, previewBytes,
|
Files.write(file, previewBytes,
|
||||||
StandardOpenOption.CREATE,
|
StandardOpenOption.CREATE,
|
||||||
StandardOpenOption.TRUNCATE_EXISTING,
|
StandardOpenOption.TRUNCATE_EXISTING,
|
||||||
StandardOpenOption.WRITE);
|
StandardOpenOption.WRITE);
|
||||||
|
logger.info("Wrote asset to {}", file.toString());
|
||||||
return previewRoot.relativize(file).toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<AssetContent> loadAssetFile(String relativePath, String mediaType) {
|
public Optional<AssetContent> loadAssetFile(Asset asset) {
|
||||||
if (relativePath == null || relativePath.isBlank()) return Optional.empty();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Path file = safeJoin(assetRoot, relativePath);
|
Path file = assetPath(
|
||||||
|
asset.getBroadcaster(),
|
||||||
|
asset.getId(),
|
||||||
|
asset.getMediaType()
|
||||||
|
);
|
||||||
|
|
||||||
if (!Files.exists(file)) return Optional.empty();
|
if (!Files.exists(file)) return Optional.empty();
|
||||||
|
|
||||||
String resolved = mediaType;
|
|
||||||
if (resolved == null || resolved.isBlank()) {
|
|
||||||
resolved = Files.probeContentType(file);
|
|
||||||
}
|
|
||||||
if (resolved == null || resolved.isBlank()) {
|
|
||||||
resolved = "application/octet-stream";
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] bytes = Files.readAllBytes(file);
|
byte[] bytes = Files.readAllBytes(file);
|
||||||
return Optional.of(new AssetContent(bytes, resolved));
|
return Optional.of(new AssetContent(bytes, asset.getMediaType()));
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("Failed to load asset {}", relativePath, e);
|
logger.warn("Failed to load asset {}", asset.getId(), e);
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<AssetContent> loadPreview(String relativePath) {
|
public Optional<AssetContent> loadPreview(Asset asset) {
|
||||||
if (relativePath == null || relativePath.isBlank()) return Optional.empty();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Path file = safeJoin(previewRoot, relativePath);
|
Path file = previewPath(asset.getBroadcaster(), asset.getId());
|
||||||
|
|
||||||
if (!Files.exists(file)) return Optional.empty();
|
if (!Files.exists(file)) return Optional.empty();
|
||||||
|
|
||||||
byte[] bytes = Files.readAllBytes(file);
|
byte[] bytes = Files.readAllBytes(file);
|
||||||
return Optional.of(new AssetContent(bytes, "image/png"));
|
return Optional.of(new AssetContent(bytes, "image/png"));
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("Failed to load preview {}", relativePath, e);
|
logger.warn("Failed to load preview {}", asset.getId(), e);
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<AssetContent> loadAssetFileSafely(Asset asset) {
|
public Optional<AssetContent> loadAssetFileSafely(Asset asset) {
|
||||||
if (asset.getUrl() == null) return Optional.empty();
|
if (asset.getUrl() == null) {
|
||||||
return loadAssetFile(asset.getUrl(), asset.getMediaType());
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return loadAssetFile(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<AssetContent> loadPreviewSafely(Asset asset) {
|
public Optional<AssetContent> loadPreviewSafely(Asset asset) {
|
||||||
if (asset.getPreview() == null) return Optional.empty();
|
if (asset.getPreview() == null) {
|
||||||
return loadPreview(asset.getPreview());
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return loadPreview(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteAssetFile(String relativePath) {
|
public void deleteAsset(Asset asset) {
|
||||||
if (relativePath == null || relativePath.isBlank()) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Path file = safeJoin(assetRoot, relativePath);
|
Files.deleteIfExists(
|
||||||
Files.deleteIfExists(file);
|
assetPath(asset.getBroadcaster(), asset.getId(), asset.getMediaType())
|
||||||
|
);
|
||||||
|
Files.deleteIfExists(
|
||||||
|
previewPath(asset.getBroadcaster(), asset.getId())
|
||||||
|
);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("Failed to delete asset {}", relativePath, e);
|
logger.warn("Failed to delete asset {}", asset.getId(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +163,17 @@ public class AssetStorageService {
|
|||||||
return EXTENSIONS.get(mediaType);
|
return EXTENSIONS.get(mediaType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Path assetPath(String broadcaster, String assetId, String mediaType) throws IOException {
|
||||||
|
String safeUser = sanitizeUserSegment(broadcaster);
|
||||||
|
String extension = resolveExtension(mediaType);
|
||||||
|
return safeJoin(assetRoot, safeUser).resolve(assetId + extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path previewPath(String broadcaster, String assetId) throws IOException {
|
||||||
|
String safeUser = sanitizeUserSegment(broadcaster);
|
||||||
|
return safeJoin(previewRoot, safeUser).resolve(assetId + ".png");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safe path-join that prevents path traversal.
|
* Safe path-join that prevents path traversal.
|
||||||
* Accepts both "abc/123.png" (relative multi-level) and single components.
|
* Accepts both "abc/123.png" (relative multi-level) and single components.
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import dev.kruhlmann.imgfloat.service.media.OptimizedAsset;
|
|||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -49,6 +50,9 @@ public class ChannelDirectoryService {
|
|||||||
private final MediaDetectionService mediaDetectionService;
|
private final MediaDetectionService mediaDetectionService;
|
||||||
private final MediaOptimizationService mediaOptimizationService;
|
private final MediaOptimizationService mediaOptimizationService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private long uploadLimitBytes;
|
||||||
|
|
||||||
public ChannelDirectoryService(
|
public ChannelDirectoryService(
|
||||||
ChannelRepository channelRepository,
|
ChannelRepository channelRepository,
|
||||||
AssetRepository assetRepository,
|
AssetRepository assetRepository,
|
||||||
@@ -130,13 +134,20 @@ public class ChannelDirectoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Optional<AssetView> createAsset(String broadcaster, MultipartFile file) throws IOException {
|
public Optional<AssetView> createAsset(String broadcaster, MultipartFile file) throws IOException {
|
||||||
|
long fileSize = file.getSize();
|
||||||
|
long maxSize = uploadLimitBytes;
|
||||||
|
if (fileSize > maxSize) {
|
||||||
|
throw new ResponseStatusException(
|
||||||
|
PAYLOAD_TOO_LARGE,
|
||||||
|
String.format(
|
||||||
|
"Uploaded file is too large (%d bytes). Maximum allowed is %d bytes.",
|
||||||
|
fileSize,
|
||||||
|
maxSize
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
Channel channel = getOrCreateChannel(broadcaster);
|
Channel channel = getOrCreateChannel(broadcaster);
|
||||||
|
|
||||||
long reported = file.getSize();
|
|
||||||
|
|
||||||
byte[] bytes = file.getBytes();
|
byte[] bytes = file.getBytes();
|
||||||
|
|
||||||
String mediaType = mediaDetectionService.detectAllowedMediaType(file, bytes)
|
String mediaType = mediaDetectionService.detectAllowedMediaType(file, bytes)
|
||||||
.orElseThrow(() -> new ResponseStatusException(
|
.orElseThrow(() -> new ResponseStatusException(
|
||||||
BAD_REQUEST, "Unsupported media type"));
|
BAD_REQUEST, "Unsupported media type"));
|
||||||
@@ -161,18 +172,18 @@ public class ChannelDirectoryService {
|
|||||||
asset.setOriginalMediaType(mediaType);
|
asset.setOriginalMediaType(mediaType);
|
||||||
asset.setMediaType(optimized.mediaType());
|
asset.setMediaType(optimized.mediaType());
|
||||||
|
|
||||||
asset.setUrl(assetStorageService.storeAsset(
|
assetStorageService.storeAsset(
|
||||||
channel.getBroadcaster(),
|
channel.getBroadcaster(),
|
||||||
asset.getId(),
|
asset.getId(),
|
||||||
optimized.bytes(),
|
optimized.bytes(),
|
||||||
optimized.mediaType()
|
optimized.mediaType()
|
||||||
));
|
);
|
||||||
|
|
||||||
asset.setPreview(assetStorageService.storePreview(
|
assetStorageService.storePreview(
|
||||||
channel.getBroadcaster(),
|
channel.getBroadcaster(),
|
||||||
asset.getId(),
|
asset.getId(),
|
||||||
optimized.previewBytes()
|
optimized.previewBytes()
|
||||||
));
|
);
|
||||||
|
|
||||||
asset.setSpeed(1.0);
|
asset.setSpeed(1.0);
|
||||||
asset.setMuted(optimized.mediaType().startsWith("video/"));
|
asset.setMuted(optimized.mediaType().startsWith("video/"));
|
||||||
@@ -274,39 +285,30 @@ public class ChannelDirectoryService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean deleteAsset(String broadcaster, String assetId) {
|
public boolean deleteAsset(String assetId) {
|
||||||
String normalized = normalize(broadcaster);
|
|
||||||
return assetRepository.findById(assetId)
|
return assetRepository.findById(assetId)
|
||||||
.filter(a -> normalized.equals(a.getBroadcaster()))
|
|
||||||
.map(asset -> {
|
.map(asset -> {
|
||||||
assetStorageService.deleteAssetFile(asset.getUrl());
|
|
||||||
assetStorageService.deletePreviewFile(asset.getPreview());
|
|
||||||
assetRepository.delete(asset);
|
assetRepository.delete(asset);
|
||||||
messagingTemplate.convertAndSend(topicFor(broadcaster),
|
assetStorageService.deleteAsset(asset);
|
||||||
AssetEvent.deleted(broadcaster, assetId));
|
messagingTemplate.convertAndSend(topicFor(asset.getBroadcaster()),
|
||||||
|
AssetEvent.deleted(asset.getBroadcaster(), assetId));
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.orElse(false);
|
.orElse(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<AssetContent> getAssetContent(String broadcaster, String assetId) {
|
public Optional<AssetContent> getAssetContent(String assetId) {
|
||||||
String normalized = normalize(broadcaster);
|
return assetRepository.findById(assetId).flatMap(assetStorageService::loadAssetFileSafely);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<AssetContent> getVisibleAssetContent(String assetId) {
|
||||||
return assetRepository.findById(assetId)
|
return assetRepository.findById(assetId)
|
||||||
.filter(a -> normalized.equals(a.getBroadcaster()))
|
.filter(a -> !a.isHidden())
|
||||||
.flatMap(assetStorageService::loadAssetFileSafely);
|
.flatMap(assetStorageService::loadAssetFileSafely);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<AssetContent> getVisibleAssetContent(String broadcaster, String assetId) {
|
public Optional<AssetContent> getAssetPreview(String assetId, boolean includeHidden) {
|
||||||
String normalized = normalize(broadcaster);
|
|
||||||
return assetRepository.findById(assetId)
|
return assetRepository.findById(assetId)
|
||||||
.filter(a -> normalized.equals(a.getBroadcaster()) && !a.isHidden())
|
|
||||||
.flatMap(assetStorageService::loadAssetFileSafely);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<AssetContent> getAssetPreview(String broadcaster, String assetId, boolean includeHidden) {
|
|
||||||
String normalized = normalize(broadcaster);
|
|
||||||
return assetRepository.findById(assetId)
|
|
||||||
.filter(a -> normalized.equals(a.getBroadcaster()))
|
|
||||||
.filter(a -> includeHidden || !a.isHidden())
|
.filter(a -> includeHidden || !a.isHidden())
|
||||||
.flatMap(assetStorageService::loadPreviewSafely);
|
.flatMap(assetStorageService::loadPreviewSafely);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,20 @@ public class VersionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String getGitVersionString() {
|
private String getGitVersionString() {
|
||||||
|
try {
|
||||||
|
Process check = new ProcessBuilder("git", "--version")
|
||||||
|
.redirectErrorStream(true)
|
||||||
|
.start();
|
||||||
|
|
||||||
|
if (check.waitFor() != 0) {
|
||||||
|
LOG.info("git not found on PATH, skipping git version detection");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.info("git not found on PATH, skipping git version detection");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Process process = null;
|
Process process = null;
|
||||||
try {
|
try {
|
||||||
process = new ProcessBuilder("git", "describe", "--tags", "--always")
|
process = new ProcessBuilder("git", "describe", "--tags", "--always")
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
server:
|
server:
|
||||||
port: ${SERVER_PORT:8080}
|
port: ${SERVER_PORT:8080}
|
||||||
|
tomcat:
|
||||||
|
max-swallow-size: 0
|
||||||
ssl:
|
ssl:
|
||||||
enabled: ${SSL_ENABLED:false}
|
enabled: ${SSL_ENABLED:false}
|
||||||
key-store: ${SSL_KEYSTORE_PATH:classpath:keystore.p12}
|
key-store: ${SSL_KEYSTORE_PATH:classpath:keystore.p12}
|
||||||
@@ -19,13 +21,14 @@ spring:
|
|||||||
thymeleaf:
|
thymeleaf:
|
||||||
cache: false
|
cache: false
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:sqlite:${IMGFLOAT_DB_PATH:imgfloat.db}?busy_timeout=5000&journal_mode=WAL
|
url: jdbc:sqlite:${IMGFLOAT_DB_PATH}?busy_timeout=5000&journal_mode=WAL
|
||||||
driver-class-name: org.sqlite.JDBC
|
driver-class-name: org.sqlite.JDBC
|
||||||
hikari:
|
hikari:
|
||||||
connection-init-sql: "PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;"
|
connection-init-sql: "PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;"
|
||||||
maximum-pool-size: 1
|
maximum-pool-size: 1
|
||||||
minimum-idle: 1
|
minimum-idle: 1
|
||||||
jpa:
|
jpa:
|
||||||
|
open-in-view: false
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: update
|
ddl-auto: update
|
||||||
database-platform: org.hibernate.community.dialect.SQLiteDialect
|
database-platform: org.hibernate.community.dialect.SQLiteDialect
|
||||||
|
|||||||
@@ -418,9 +418,7 @@ function connect() {
|
|||||||
fetchAssets();
|
fetchAssets();
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
console.warn('WebSocket connection issue', error);
|
console.warn('WebSocket connection issue', error);
|
||||||
if (typeof showToast === 'function') {
|
|
||||||
setTimeout(() => showToast('Live updates connection interrupted. Retrying may be necessary.', 'warning'), 1000);
|
setTimeout(() => showToast('Live updates connection interrupted. Retrying may be necessary.', 'warning'), 1000);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,11 +431,7 @@ function fetchAssets() {
|
|||||||
return r.json();
|
return r.json();
|
||||||
})
|
})
|
||||||
.then(renderAssets)
|
.then(renderAssets)
|
||||||
.catch(() => {
|
.catch(() => showToast('Unable to load assets. Please refresh.', 'error'));
|
||||||
if (typeof showToast === 'function') {
|
|
||||||
showToast('Unable to load assets. Please refresh.', 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchCanvasSettings() {
|
function fetchCanvasSettings() {
|
||||||
@@ -454,9 +448,7 @@ function fetchCanvasSettings() {
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
resizeCanvas();
|
resizeCanvas();
|
||||||
if (typeof showToast === 'function') {
|
|
||||||
showToast('Using default canvas size. Unable to load saved settings.', 'warning');
|
showToast('Using default canvas size. Unable to load saved settings.', 'warning');
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1968,24 +1960,15 @@ function updateVisibility(asset, hidden) {
|
|||||||
if (updated.hidden) {
|
if (updated.hidden) {
|
||||||
loopPlaybackState.set(updated.id, false);
|
loopPlaybackState.set(updated.id, false);
|
||||||
stopAudio(updated.id);
|
stopAudio(updated.id);
|
||||||
if (typeof showToast === 'function') {
|
|
||||||
showToast('Asset hidden from broadcast.', 'info');
|
showToast('Asset hidden from broadcast.', 'info');
|
||||||
}
|
|
||||||
} else if (isAudioAsset(updated)) {
|
} else if (isAudioAsset(updated)) {
|
||||||
playAudioFromCanvas(updated, true);
|
playAudioFromCanvas(updated, true);
|
||||||
if (typeof showToast === 'function') {
|
|
||||||
showToast('Asset is now visible and active.', 'success');
|
showToast('Asset is now visible and active.', 'success');
|
||||||
}
|
}
|
||||||
} else if (typeof showToast === 'function') {
|
|
||||||
showToast('Asset is now visible.', 'success');
|
showToast('Asset is now visible.', 'success');
|
||||||
}
|
|
||||||
updateRenderState(updated);
|
updateRenderState(updated);
|
||||||
drawAndList();
|
drawAndList();
|
||||||
}).catch(() => {
|
}).catch(() => showToast('Unable to change visibility right now.', 'error'));
|
||||||
if (typeof showToast === 'function') {
|
|
||||||
showToast('Unable to change visibility right now.', 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerAudioPlayback(asset, shouldPlay = true) {
|
function triggerAudioPlayback(asset, shouldPlay = true) {
|
||||||
@@ -2016,15 +1999,9 @@ function deleteAsset(asset) {
|
|||||||
selectedAssetId = null;
|
selectedAssetId = null;
|
||||||
}
|
}
|
||||||
drawAndList();
|
drawAndList();
|
||||||
if (typeof showToast === 'function') {
|
|
||||||
showToast('Asset deleted.', 'info');
|
showToast('Asset deleted.', 'info');
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => showToast('Unable to delete asset. Please try again.', 'error'));
|
||||||
if (typeof showToast === 'function') {
|
|
||||||
showToast('Unable to delete asset. Please try again.', 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileSelection(input) {
|
function handleFileSelection(input) {
|
||||||
@@ -2043,9 +2020,11 @@ function uploadAsset(file = null) {
|
|||||||
const fileInput = document.getElementById('asset-file');
|
const fileInput = document.getElementById('asset-file');
|
||||||
const selectedFile = file || (fileInput?.files && fileInput.files.length ? fileInput.files[0] : null);
|
const selectedFile = file || (fileInput?.files && fileInput.files.length ? fileInput.files[0] : null);
|
||||||
if (!selectedFile) {
|
if (!selectedFile) {
|
||||||
if (typeof showToast === 'function') {
|
|
||||||
showToast('Choose an image, GIF, video, or audio file to upload.', 'info');
|
showToast('Choose an image, GIF, video, or audio file to upload.', 'info');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
if (selectedFile.size > upload_limit_bytes) {
|
||||||
|
showToast(`File is too large. Maximum upload size is ${upload_limit_bytes / 1024 / 1024} MB.`, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2066,18 +2045,14 @@ function uploadAsset(file = null) {
|
|||||||
fileInput.value = '';
|
fileInput.value = '';
|
||||||
handleFileSelection(fileInput);
|
handleFileSelection(fileInput);
|
||||||
}
|
}
|
||||||
if (typeof showToast === 'function') {
|
|
||||||
showToast('Upload received. Processing asset...', 'success');
|
showToast('Upload received. Processing asset...', 'success');
|
||||||
}
|
|
||||||
updatePendingUpload(pendingId, { status: 'processing' });
|
updatePendingUpload(pendingId, { status: 'processing' });
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
if (fileNameLabel) {
|
if (fileNameLabel) {
|
||||||
fileNameLabel.textContent = 'Upload failed';
|
fileNameLabel.textContent = 'Upload failed';
|
||||||
}
|
}
|
||||||
removePendingUpload(pendingId);
|
removePendingUpload(pendingId);
|
||||||
if (typeof showToast === 'function') {
|
|
||||||
showToast('Upload failed. Please try again with a supported file.', 'error');
|
showToast('Upload failed. Please try again with a supported file.', 'error');
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2142,7 +2117,7 @@ function persistTransform(asset, silent = false) {
|
|||||||
drawAndList();
|
drawAndList();
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
if (!silent && typeof showToast === 'function') {
|
if (!silent) {
|
||||||
showToast('Unable to save changes. Please retry.', 'error');
|
showToast('Unable to save changes. Please retry.', 'error');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -208,6 +208,7 @@
|
|||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
const broadcaster = /*[[${broadcaster}]]*/ '';
|
const broadcaster = /*[[${broadcaster}]]*/ '';
|
||||||
const username = /*[[${username}]]*/ '';
|
const username = /*[[${username}]]*/ '';
|
||||||
|
const upload_limit_bytes = /*[[${uploadLimitBytes}]]*/ + 0;
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/toast.js"></script>
|
<script src="/js/toast.js"></script>
|
||||||
<script src="/js/admin.js"></script>
|
<script src="/js/admin.js"></script>
|
||||||
|
|||||||
@@ -56,12 +56,12 @@ class ChannelDirectoryServiceTest {
|
|||||||
setupInMemoryPersistence();
|
setupInMemoryPersistence();
|
||||||
Path assetRoot = Files.createTempDirectory("imgfloat-assets-test");
|
Path assetRoot = Files.createTempDirectory("imgfloat-assets-test");
|
||||||
Path previewRoot = Files.createTempDirectory("imgfloat-previews-test");
|
Path previewRoot = Files.createTempDirectory("imgfloat-previews-test");
|
||||||
AssetStorageService assetStorageService = new AssetStorageService(assetRoot.toString(), previewRoot.toString(), 26214400L);
|
AssetStorageService assetStorageService = new AssetStorageService(assetRoot.toString(), previewRoot.toString());
|
||||||
MediaPreviewService mediaPreviewService = new MediaPreviewService();
|
MediaPreviewService mediaPreviewService = new MediaPreviewService();
|
||||||
MediaOptimizationService mediaOptimizationService = new MediaOptimizationService(mediaPreviewService);
|
MediaOptimizationService mediaOptimizationService = new MediaOptimizationService(mediaPreviewService);
|
||||||
MediaDetectionService mediaDetectionService = new MediaDetectionService();
|
MediaDetectionService mediaDetectionService = new MediaDetectionService();
|
||||||
service = new ChannelDirectoryService(channelRepository, assetRepository, messagingTemplate,
|
service = new ChannelDirectoryService(channelRepository, assetRepository, messagingTemplate,
|
||||||
assetStorageService, mediaDetectionService, mediaOptimizationService, 26214400L);
|
assetStorageService, mediaDetectionService, mediaOptimizationService);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
package dev.kruhlmann.imgfloat;
|
|
||||||
|
|
||||||
import dev.kruhlmann.imgfloat.config.TwitchCredentialsValidator;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
|
||||||
import org.springframework.context.ConfigurableApplicationContext;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
||||||
|
|
||||||
class TwitchEnvironmentValidationTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void failsToStartWhenTwitchCredentialsMissing() {
|
|
||||||
assertThatThrownBy(() -> new SpringApplicationBuilder(ImgfloatApplication.class)
|
|
||||||
.properties("server.port=0")
|
|
||||||
.run())
|
|
||||||
.hasRootCauseInstanceOf(IllegalArgumentException.class)
|
|
||||||
.hasRootCauseMessage("Could not resolve placeholder 'TWITCH_CLIENT_ID' in value \"${TWITCH_CLIENT_ID}\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void loadsCredentialsFromDotEnvFile() {
|
|
||||||
ConfigurableApplicationContext context = null;
|
|
||||||
try {
|
|
||||||
context = new SpringApplicationBuilder(ImgfloatApplication.class)
|
|
||||||
.properties(
|
|
||||||
"server.port=0",
|
|
||||||
"spring.config.import=optional:file:src/test/resources/valid.env[.properties]")
|
|
||||||
.run();
|
|
||||||
ConfigurableApplicationContext finalContext = context;
|
|
||||||
assertThatCode(() -> finalContext.getBean(TwitchCredentialsValidator.class))
|
|
||||||
.doesNotThrowAnyException();
|
|
||||||
} finally {
|
|
||||||
if (context != null) {
|
|
||||||
context.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void stripsQuotesAndWhitespaceFromCredentials() {
|
|
||||||
ConfigurableApplicationContext context = null;
|
|
||||||
try {
|
|
||||||
context = new SpringApplicationBuilder(ImgfloatApplication.class)
|
|
||||||
.properties(
|
|
||||||
"server.port=0",
|
|
||||||
"TWITCH_CLIENT_ID=\" quoted-id \"",
|
|
||||||
"TWITCH_CLIENT_SECRET=' quoted-secret '"
|
|
||||||
)
|
|
||||||
.run();
|
|
||||||
|
|
||||||
var clientRegistrationRepository = context.getBean(org.springframework.security.oauth2.client.registration.ClientRegistrationRepository.class);
|
|
||||||
var twitch = clientRegistrationRepository.findByRegistrationId("twitch");
|
|
||||||
|
|
||||||
org.assertj.core.api.Assertions.assertThat(twitch.getClientId()).isEqualTo("quoted-id");
|
|
||||||
org.assertj.core.api.Assertions.assertThat(twitch.getClientSecret()).isEqualTo("quoted-secret");
|
|
||||||
} finally {
|
|
||||||
if (context != null) {
|
|
||||||
context.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,7 @@ class AssetStorageServiceTest {
|
|||||||
void setUp() throws IOException {
|
void setUp() throws IOException {
|
||||||
assets = Files.createTempDirectory("asset-storage-service");
|
assets = Files.createTempDirectory("asset-storage-service");
|
||||||
previews = Files.createTempDirectory("preview-storage-service");
|
previews = Files.createTempDirectory("preview-storage-service");
|
||||||
service = new AssetStorageService(assets.toString(), previews.toString(), 26214400L);
|
service = new AssetStorageService(assets.toString(), previews.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user