diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt index b913523..990551c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt @@ -100,19 +100,13 @@ enum class ConfigurationType( val secure: Boolean = false ) { INSTANCE_NAME("instance.name", defaultContent = "Color Recipes Explorer", public = true), - INSTANCE_LOGO_PATH("instance.logo.path", defaultContent = "images/logo", public = true), - INSTANCE_ICON_PATH("instance.icon.path", defaultContent = "images/icon", public = true), + INSTANCE_LOGO_SET("instance.logo.set", defaultContent = false, public = true), + INSTANCE_ICON_SET("instance.icon.set", defaultContent = false, public = true), INSTANCE_URL("instance.url", "http://localhost:9090", public = true), DATABASE_URL("database.url", defaultContent = "mysql://localhost/cre", file = true, requireRestart = true), DATABASE_USER("database.user", defaultContent = "cre", file = true, requireRestart = true), - DATABASE_PASSWORD( - "database.password", - defaultContent = "asecurepassword", - file = true, - requireRestart = true, - secure = true - ), + DATABASE_PASSWORD("database.password", defaultContent = "asecurepassword", file = true, requireRestart = true, secure = true), DATABASE_SUPPORTED_VERSION("database.version.supported", computed = true), RECIPE_APPROBATION_EXPIRATION("recipe.approbation.expiration", defaultContent = 4.months), diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/ConfigurationController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/ConfigurationController.kt index 1e7cff4..5e30ea6 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/ConfigurationController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/ConfigurationController.kt @@ -7,6 +7,7 @@ import dev.fyloz.colorrecipesexplorer.model.account.Permission import dev.fyloz.colorrecipesexplorer.model.account.toAuthority import dev.fyloz.colorrecipesexplorer.restartApplication import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService +import org.springframework.http.MediaType import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.core.Authentication import org.springframework.web.bind.annotation.* @@ -33,17 +34,35 @@ class ConfigurationController(val configurationService: ConfigurationService) { configurationService.set(configurations) } - @PutMapping("image") - @PreAuthorize("hasAuthority('ADMIN')") - fun setImage(@RequestParam @NotBlank key: String, @RequestParam @NotBlank image: MultipartFile) = noContent { - configurationService.set(ConfigurationImageDto(key, image)) - } - @PostMapping("restart") @PreAuthorize("hasAuthority('ADMIN')") fun restart() = noContent { restartApplication() } + + // Icon + + @GetMapping("icon") + fun getIcon() = + ok(configurationService.getConfiguredIcon(), MediaType.IMAGE_PNG_VALUE) + + @PutMapping("icon") + @PreAuthorize("hasAuthority('ADMIN')") + fun setIcon(@RequestParam icon: MultipartFile) = noContent { + configurationService.setConfiguredIcon(icon) + } + + // Logo + + @GetMapping("logo") + fun getLogo() = + ok(configurationService.getConfiguredLogo(), MediaType.IMAGE_PNG_VALUE) + + @PutMapping("logo") + @PreAuthorize("hasAuthority('ADMIN')") + fun setLogo(@RequestParam logo: MultipartFile) = noContent { + configurationService.setConfiguredLogo(logo) + } } private fun Authentication?.hasAuthority(configuration: ConfigurationBase) = when { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt index 5f1e689..c7879d6 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt @@ -2,8 +2,9 @@ package dev.fyloz.colorrecipesexplorer.rest import dev.fyloz.colorrecipesexplorer.model.ConfigurationType import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService -import dev.fyloz.colorrecipesexplorer.service.FileService +import dev.fyloz.colorrecipesexplorer.service.files.WriteableFileService import org.springframework.core.io.ByteArrayResource +import org.springframework.core.io.Resource import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.security.access.prepost.PreAuthorize @@ -12,19 +13,18 @@ import org.springframework.web.multipart.MultipartFile import java.net.URI const val FILE_CONTROLLER_PATH = "/api/file" -private const val DEFAULT_MEDIA_TYPE = MediaType.APPLICATION_OCTET_STREAM_VALUE @RestController @RequestMapping(FILE_CONTROLLER_PATH) class FileController( - private val fileService: FileService, + private val fileService: WriteableFileService, private val configService: ConfigurationService ) { @GetMapping(produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE]) fun upload( @RequestParam path: String, @RequestParam(required = false) mediaType: String? - ): ResponseEntity { + ): ResponseEntity { val file = fileService.read(path) return ResponseEntity.ok() .header("Content-Disposition", "filename=${getFileNameFromPath(path)}") @@ -56,7 +56,4 @@ class FileController( ResponseEntity .created(URI.create("${configService.get(ConfigurationType.INSTANCE_URL)}$FILE_CONTROLLER_PATH?path=$path")) .build() - - private fun getFileNameFromPath(path: String) = - path.split("/").last() } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt index 23d59da..36b892e 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt @@ -2,12 +2,14 @@ package dev.fyloz.colorrecipesexplorer.rest import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties import dev.fyloz.colorrecipesexplorer.model.Model +import org.springframework.core.io.Resource import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import java.net.URI +const val DEFAULT_MEDIA_TYPE = MediaType.APPLICATION_OCTET_STREAM_VALUE lateinit var CRE_PROPERTIES: CreProperties /** Creates a HTTP OK [ResponseEntity] from the given [body]. */ @@ -24,6 +26,14 @@ fun ok(action: () -> Unit): ResponseEntity { return ResponseEntity.ok().build() } +fun ok(file: Resource, mediaType: String? = null): ResponseEntity { + return ResponseEntity.ok() + .header("Content-Disposition", "filename=${file.filename}") + .contentLength(file.contentLength()) + .contentType(MediaType.parseMediaType(mediaType ?: DEFAULT_MEDIA_TYPE)) + .body(file) +} + /** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */ fun created(controllerPath: String, body: T): ResponseEntity = created(controllerPath, body, body.id!!) @@ -63,3 +73,6 @@ fun httpHeaders( op() } + +fun getFileNameFromPath(path: String) = + path.split("/").last() diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/TouchUpKitController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/TouchUpKitController.kt index e9cbe47..027d71d 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/TouchUpKitController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/TouchUpKitController.kt @@ -5,7 +5,7 @@ import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitSaveDto import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitUpdateDto import dev.fyloz.colorrecipesexplorer.service.TouchUpKitService import org.springframework.context.annotation.Profile -import org.springframework.core.io.ByteArrayResource +import org.springframework.core.io.Resource import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.security.access.prepost.PreAuthorize @@ -57,7 +57,7 @@ class TouchUpKitController( } @GetMapping("pdf") - fun getJobPdf(@RequestParam project: String): ResponseEntity { + fun getJobPdf(@RequestParam project: String): ResponseEntity { with(touchUpKitService.generateJobPdfResource(project)) { return ResponseEntity.ok() .header("Content-Disposition", "filename=TouchUpKit_$project.pdf") diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt index 7d752f9..327d6e2 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt @@ -4,6 +4,7 @@ import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository import dev.fyloz.colorrecipesexplorer.rest.FILE_CONTROLLER_PATH import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService +import dev.fyloz.colorrecipesexplorer.service.files.WriteableFileService import io.jsonwebtoken.lang.Assert import org.springframework.context.annotation.Lazy import org.springframework.context.annotation.Profile @@ -39,7 +40,7 @@ class MaterialServiceImpl( val recipeService: RecipeService, val mixService: MixService, @Lazy val materialTypeService: MaterialTypeService, - val fileService: FileService, + val fileService: WriteableFileService, val configService: ConfigurationService ) : AbstractExternalNamedModelService( diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt index 4dcda07..4361728 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt @@ -5,6 +5,7 @@ import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.model.validation.or import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService +import dev.fyloz.colorrecipesexplorer.service.files.WriteableFileService import dev.fyloz.colorrecipesexplorer.utils.setAll import org.springframework.context.annotation.Lazy import org.springframework.context.annotation.Profile @@ -222,7 +223,7 @@ const val RECIPE_IMAGE_EXTENSION = ".jpg" @Service @Profile("!emergency") class RecipeImageServiceImpl( - val fileService: FileService + val fileService: WriteableFileService ) : RecipeImageService { override fun getAllImages(recipe: Recipe): Set { val recipeDirectory = recipe.getDirectory() diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitService.kt index b966525..a6bbc1e 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitService.kt @@ -5,9 +5,12 @@ import dev.fyloz.colorrecipesexplorer.model.touchupkit.* import dev.fyloz.colorrecipesexplorer.repository.TouchUpKitRepository import dev.fyloz.colorrecipesexplorer.rest.TOUCH_UP_KIT_CONTROLLER_PATH import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService +import dev.fyloz.colorrecipesexplorer.service.files.FileService +import dev.fyloz.colorrecipesexplorer.service.files.WriteableFileService import dev.fyloz.colorrecipesexplorer.utils.* import org.springframework.context.annotation.Profile import org.springframework.core.io.ByteArrayResource +import org.springframework.core.io.Resource import org.springframework.stereotype.Service import java.time.LocalDate import java.time.Period @@ -32,7 +35,7 @@ interface TouchUpKitService : * If TOUCH_UP_KIT_CACHE_PDF is enabled and a file exists for the job, its content will be returned. * If caching is enabled but no file exists for the job, the generated ByteArrayResource will be cached on the disk. */ - fun generateJobPdfResource(job: String): ByteArrayResource + fun generateJobPdfResource(job: String): Resource /** Writes the given [document] to the [FileService] if TOUCH_UP_KIT_CACHE_PDF is enabled. */ fun String.cachePdfDocument(document: PdfDocument) @@ -41,7 +44,7 @@ interface TouchUpKitService : @Service @Profile("!emergency") class TouchUpKitServiceImpl( - private val fileService: FileService, + private val fileService: WriteableFileService, private val configService: ConfigurationService, touchUpKitRepository: TouchUpKitRepository ) : AbstractExternalModelService( @@ -120,7 +123,7 @@ class TouchUpKitServiceImpl( } } - override fun generateJobPdfResource(job: String): ByteArrayResource { + override fun generateJobPdfResource(job: String): Resource { if (cacheGeneratedFiles) { with(job.pdfDocumentPath()) { if (fileService.exists(this)) { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/config/ConfigurationService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/config/ConfigurationService.kt index 8e5d0c2..302c3b3 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/config/ConfigurationService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/config/ConfigurationService.kt @@ -2,13 +2,16 @@ package dev.fyloz.colorrecipesexplorer.service.config import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.service.FileService +import dev.fyloz.colorrecipesexplorer.service.files.ResourceFileService +import dev.fyloz.colorrecipesexplorer.service.files.WriteableFileService import dev.fyloz.colorrecipesexplorer.utils.decrypt import dev.fyloz.colorrecipesexplorer.utils.encrypt import org.slf4j.Logger import org.springframework.context.annotation.Lazy +import org.springframework.core.io.Resource import org.springframework.security.crypto.keygen.KeyGenerators import org.springframework.stereotype.Service +import org.springframework.web.multipart.MultipartFile interface ConfigurationService { /** Gets all set configurations. */ @@ -35,6 +38,12 @@ interface ConfigurationService { /** Gets the content of the secure configuration with the given [type]. Should not be accessible to the users. */ fun getSecure(type: ConfigurationType): String + /** Gets the app's icon. */ + fun getConfiguredIcon(): Resource + + /** Gets the app's logo. */ + fun getConfiguredLogo(): Resource + /** Sets the content of each configuration in the given [configurations] list. */ fun set(configurations: List) @@ -47,20 +56,26 @@ interface ConfigurationService { /** Sets the content given [configuration]. */ fun set(configuration: Configuration) - /** Sets the content of the configuration matching the given [configuration] with a given image. */ - fun set(configuration: ConfigurationImageDto) + /** Sets the app's icon. */ + fun setConfiguredIcon(icon: MultipartFile) + + /** Sets the app's logo. */ + fun setConfiguredLogo(logo: MultipartFile) /** Initialize the properties matching the given [predicate]. */ fun initializeProperties(predicate: (ConfigurationType) -> Boolean) } +const val CONFIGURATION_LOGO_RESOURCE_PATH = "images/logo.png" const val CONFIGURATION_LOGO_FILE_PATH = "images/logo" +const val CONFIGURATION_ICON_RESOURCE_PATH = "images/icon.png" const val CONFIGURATION_ICON_FILE_PATH = "images/icon" const val CONFIGURATION_FORMATTED_LIST_DELIMITER = ';' @Service("configurationService") class ConfigurationServiceImpl( - @Lazy private val fileService: FileService, + @Lazy private val fileService: WriteableFileService, + private val resourceFileService: ResourceFileService, private val configurationSource: ConfigurationSource, private val securityProperties: CreSecurityProperties, private val logger: Logger @@ -121,6 +136,29 @@ class ConfigurationServiceImpl( return decryptConfiguration(configuration).content } + override fun getConfiguredIcon() = + getConfiguredImage( + type = ConfigurationType.INSTANCE_ICON_SET, + filePath = CONFIGURATION_ICON_FILE_PATH, + resourcePath = CONFIGURATION_ICON_RESOURCE_PATH + ) + + override fun getConfiguredLogo() = + getConfiguredImage( + type = ConfigurationType.INSTANCE_LOGO_SET, + filePath = CONFIGURATION_LOGO_FILE_PATH, + resourcePath = CONFIGURATION_LOGO_RESOURCE_PATH + ) + + private fun getConfiguredImage(type: ConfigurationType, filePath: String, resourcePath: String) = + with(get(type) as Configuration) { + if (this.content == true.toString()) { + fileService.read(filePath) + } else { + resourceFileService.read(resourcePath) + } + } + override fun set(configurations: List) { configurationSource.set( configurations @@ -136,14 +174,15 @@ class ConfigurationServiceImpl( configurationSource.set(encryptConfigurationIfSecure(configuration)) } - override fun set(configuration: ConfigurationImageDto) { - val filePath = when (val configurationType = configuration.key.toConfigurationType()) { - ConfigurationType.INSTANCE_LOGO_PATH -> CONFIGURATION_LOGO_FILE_PATH - ConfigurationType.INSTANCE_ICON_PATH -> CONFIGURATION_ICON_FILE_PATH - else -> throw InvalidImageConfigurationException(configurationType) - } + override fun setConfiguredIcon(icon: MultipartFile) = + setConfiguredImage(icon, CONFIGURATION_ICON_FILE_PATH, ConfigurationType.INSTANCE_ICON_SET) - fileService.write(configuration.image, filePath, true) + override fun setConfiguredLogo(logo: MultipartFile) = + setConfiguredImage(logo, CONFIGURATION_LOGO_FILE_PATH, ConfigurationType.INSTANCE_LOGO_SET) + + private fun setConfiguredImage(image: MultipartFile, path: String, type: ConfigurationType) { + fileService.write(image, path, true) + set(configuration(type, content = true.toString())) } override fun initializeProperties(predicate: (ConfigurationType) -> Boolean) { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/config/ConfigurationSource.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/config/ConfigurationSource.kt index 0b00a97..6971f9e 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/config/ConfigurationSource.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/config/ConfigurationSource.kt @@ -8,7 +8,7 @@ import dev.fyloz.colorrecipesexplorer.model.Configuration import dev.fyloz.colorrecipesexplorer.model.ConfigurationType import dev.fyloz.colorrecipesexplorer.model.configuration import dev.fyloz.colorrecipesexplorer.repository.ConfigurationRepository -import dev.fyloz.colorrecipesexplorer.service.create +import dev.fyloz.colorrecipesexplorer.service.files.create import dev.fyloz.colorrecipesexplorer.utils.excludeAll import org.slf4j.Logger import org.springframework.boot.info.BuildProperties diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/FileService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileService.kt similarity index 96% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/FileService.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileService.kt index 69cd4aa..4136ebe 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/FileService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileService.kt @@ -1,9 +1,10 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.service.files import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties import dev.fyloz.colorrecipesexplorer.exception.RestException import org.slf4j.Logger import org.springframework.core.io.ByteArrayResource +import org.springframework.core.io.Resource import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.web.multipart.MultipartFile @@ -23,8 +24,13 @@ interface FileService { fun exists(path: String): Boolean /** Reads the file at the given [path]. */ - fun read(path: String): ByteArrayResource + fun read(path: String): Resource + /** Completes the path of the given [String] by adding the working directory. */ + fun String.fullPath(): FilePath +} + +interface WriteableFileService : FileService { /** Creates a file at the given [path]. */ fun create(path: String) @@ -36,16 +42,13 @@ interface FileService { /** Deletes the file at the given [path]. */ fun delete(path: String) - - /** Completes the path of the given [String] by adding the working directory. */ - fun String.fullPath(): FilePath } @Service class FileServiceImpl( private val creProperties: CreProperties, private val logger: Logger -) : FileService { +) : WriteableFileService { override fun exists(path: String) = withFileAt(path.fullPath()) { this.exists() && this.isFile } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/ResourceFileService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/ResourceFileService.kt new file mode 100644 index 0000000..8ac67f5 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/ResourceFileService.kt @@ -0,0 +1,26 @@ +package dev.fyloz.colorrecipesexplorer.service.files + +import org.springframework.core.io.Resource +import org.springframework.core.io.ResourceLoader +import org.springframework.stereotype.Service + +@Service +class ResourceFileService( + private val resourceLoader: ResourceLoader +) : FileService { + override fun exists(path: String) = + read(path).exists() + + override fun read(path: String): Resource = + path.fullPath().resource.also { + if (!it.exists()) { + throw FileNotFoundException(path) + } + } + + override fun String.fullPath() = + FilePath("classpath:${this}") + + private val FilePath.resource: Resource + get() = resourceLoader.getResource(this.path) +} diff --git a/src/main/resources/images/favicon.png b/src/main/resources/images/icon.png similarity index 100% rename from src/main/resources/images/favicon.png rename to src/main/resources/images/icon.png