#13 Add icon and logo endpoints to ConfigurationController

This commit is contained in:
William Nolin 2021-09-01 17:53:19 -04:00
parent 2bd59e72c6
commit a02099387e
13 changed files with 143 additions and 47 deletions

View File

@ -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),

View File

@ -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 {

View File

@ -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<ByteArrayResource> {
): ResponseEntity<Resource> {
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()
}

View File

@ -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<Void> {
return ResponseEntity.ok().build()
}
fun ok(file: Resource, mediaType: String? = null): ResponseEntity<Resource> {
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 <T : Model> created(controllerPath: String, body: T): ResponseEntity<T> =
created(controllerPath, body, body.id!!)
@ -63,3 +73,6 @@ fun httpHeaders(
op()
}
fun getFileNameFromPath(path: String) =
path.split("/").last()

View File

@ -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<ByteArrayResource> {
fun getJobPdf(@RequestParam project: String): ResponseEntity<Resource> {
with(touchUpKitService.generateJobPdfResource(project)) {
return ResponseEntity.ok()
.header("Content-Disposition", "filename=TouchUpKit_$project.pdf")

View File

@ -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<Material, MaterialSaveDto, MaterialUpdateDto, MaterialOutputDto, MaterialRepository>(

View File

@ -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<String> {
val recipeDirectory = recipe.getDirectory()

View File

@ -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<TouchUpKit, TouchUpKitSaveDto, TouchUpKitUpdateDto, TouchUpKitOutputDto, TouchUpKitRepository>(
@ -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)) {

View File

@ -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<ConfigurationDto>)
@ -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<ConfigurationDto>) {
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) {

View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB