Add uploader and file storage provider with openapi docs and demo for modules
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
4910bd9122
commit
048c11ee62
10
api/src/main/kotlin/com/cubetiqs/web/config/JpaConfig.kt
Normal file
10
api/src/main/kotlin/com/cubetiqs/web/config/JpaConfig.kt
Normal file
@ -0,0 +1,10 @@
|
||||
package com.cubetiqs.web.config
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing
|
||||
|
||||
@ConditionalOnProperty(prefix = "spring.datasource", name = ["enabled"], havingValue = "true")
|
||||
@Configuration
|
||||
@EnableJpaAuditing
|
||||
class JpaConfig
|
@ -0,0 +1,20 @@
|
||||
package com.cubetiqs.web.modules
|
||||
|
||||
import com.cubetiqs.web.modules.uploader.FileStorageFactory
|
||||
import com.cubetiqs.web.modules.uploader.FileStorageLocalProvider
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
@Lazy(false)
|
||||
class ModuleInitializer constructor(
|
||||
@Value("\${module.uploader.local.path:./uploads}")
|
||||
private val fileBasePath: String,
|
||||
) {
|
||||
init {
|
||||
FileStorageFactory.setProvider(
|
||||
FileStorageLocalProvider(fileBasePath)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package com.cubetiqs.web.modules.uploader
|
||||
|
||||
import java.io.File
|
||||
|
||||
open class FileResponse(
|
||||
open var file: File? = null,
|
||||
open var shortPath: String? = null,
|
||||
)
|
@ -0,0 +1,37 @@
|
||||
package com.cubetiqs.web.modules.uploader
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.io.File
|
||||
|
||||
object FileStorageFactory {
|
||||
private var provider: FileStorageProvider? = null
|
||||
|
||||
fun setProvider(provider: FileStorageProvider) {
|
||||
this.provider = provider
|
||||
}
|
||||
|
||||
fun getProvider(): FileStorageProvider {
|
||||
return provider ?: throw IllegalStateException("FileStorageProvider is not set")
|
||||
}
|
||||
|
||||
fun store(file: File): FileResponse {
|
||||
return getProvider().store(file)
|
||||
}
|
||||
|
||||
fun store(file: MultipartFile): FileResponse {
|
||||
val tempPath = System.getProperty("java.io.tmpdir") ?: if (System.getProperty("os.name").lowercase()
|
||||
.contains("win")
|
||||
) "C:\\Windows\\Temp" else "/tmp"
|
||||
val temp = File("$tempPath/${file.originalFilename}")
|
||||
file.transferTo(temp)
|
||||
return this.store(temp)
|
||||
}
|
||||
|
||||
fun delete(fileName: String) {
|
||||
getProvider().delete(fileName)
|
||||
}
|
||||
|
||||
fun get(fileName: String): FileResponse {
|
||||
return getProvider().get(fileName)
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package com.cubetiqs.web.modules.uploader
|
||||
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
open class FileStorageLocalProvider(
|
||||
private val basePath: String,
|
||||
) : FileStorageProvider {
|
||||
private fun loadBasePath(fileName: String): String {
|
||||
val prefixPath = if (basePath.endsWith("/")) {
|
||||
""
|
||||
} else {
|
||||
"/"
|
||||
}
|
||||
|
||||
return basePath + prefixPath + fileName
|
||||
}
|
||||
|
||||
override fun store(file: File): FileResponse {
|
||||
if (!file.exists()) {
|
||||
throw FileNotFoundException("File not found")
|
||||
}
|
||||
|
||||
val path = loadBasePath(file.name)
|
||||
val savedFile = file.copyTo(File(path), true)
|
||||
return FileResponse(
|
||||
file = savedFile,
|
||||
shortPath = path,
|
||||
)
|
||||
}
|
||||
|
||||
override fun get(fileName: String): FileResponse {
|
||||
val path = loadBasePath(fileName)
|
||||
val file = File(path)
|
||||
if (!file.exists()) {
|
||||
throw FileNotFoundException("File $fileName not found")
|
||||
}
|
||||
|
||||
return FileResponse(
|
||||
file = file,
|
||||
shortPath = path,
|
||||
)
|
||||
}
|
||||
|
||||
override fun delete(fileName: String) {
|
||||
val path = loadBasePath(fileName)
|
||||
val file = File(path)
|
||||
if (file.isFile) {
|
||||
file.delete()
|
||||
} else {
|
||||
throw IllegalArgumentException("File $fileName not found")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.cubetiqs.web.modules.uploader
|
||||
|
||||
import java.io.File
|
||||
|
||||
interface FileStorageProvider {
|
||||
fun store(file: File): FileResponse
|
||||
fun get(fileName: String): FileResponse
|
||||
fun delete(fileName: String)
|
||||
}
|
@ -1,14 +1,19 @@
|
||||
package com.cubetiqs.web.modules.uploader
|
||||
|
||||
import com.cubetiqs.web.util.RouteConstants
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.Parameter
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import org.springdoc.core.converters.models.PageableAsQueryParam
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.util.FileCopyUtils
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.util.*
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
@UploaderModule
|
||||
@Tag(name = "Uploader Controller")
|
||||
@ -19,6 +24,7 @@ class UploaderController @Autowired constructor(
|
||||
) {
|
||||
@GetMapping
|
||||
@PageableAsQueryParam
|
||||
@Operation(summary = "Get all files")
|
||||
fun getAll(
|
||||
@Parameter(hidden = true)
|
||||
pageable: Pageable?,
|
||||
@ -26,35 +32,88 @@ class UploaderController @Autowired constructor(
|
||||
return repository.findAll(pageable ?: Pageable.unpaged())
|
||||
}
|
||||
|
||||
@ResponseStatus(value = org.springframework.http.HttpStatus.CREATED)
|
||||
@PostMapping
|
||||
fun create(
|
||||
@RequestBody body: UploaderEntity
|
||||
@ResponseStatus(value = org.springframework.http.HttpStatus.OK)
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "Get a file by id")
|
||||
fun get(
|
||||
@PathVariable id: String,
|
||||
): UploaderEntity {
|
||||
return repository.save(body)
|
||||
val entity = repository.findById(UUID.fromString(id)).orElseThrow {
|
||||
throw IllegalArgumentException("File not found")
|
||||
}
|
||||
return repository.save(entity)
|
||||
}
|
||||
|
||||
@ResponseStatus(value = org.springframework.http.HttpStatus.OK)
|
||||
@GetMapping("/{id}/stream", produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE])
|
||||
@Operation(summary = "Get file stream by id")
|
||||
fun stream(
|
||||
@PathVariable id: String,
|
||||
@RequestParam(required = false, value = "download") download: Boolean?,
|
||||
response: HttpServletResponse,
|
||||
) {
|
||||
val entity = repository.findById(UUID.fromString(id)).orElseThrow {
|
||||
throw IllegalArgumentException("User not found")
|
||||
}
|
||||
|
||||
if (!entity.isFileExists()) {
|
||||
throw IllegalArgumentException("File not found")
|
||||
}
|
||||
|
||||
val file = entity.getFile() ?: throw IllegalArgumentException("File source not found")
|
||||
val disposition = if (download == true) {
|
||||
"attachment"
|
||||
} else {
|
||||
"inline"
|
||||
}
|
||||
|
||||
response.setHeader("Content-Disposition", "$disposition; filename=\"${entity.filename}\"")
|
||||
response.contentType = entity.contentType
|
||||
response.setContentLengthLong(file.length())
|
||||
|
||||
FileCopyUtils.copy(file.readBytes(), response.outputStream)
|
||||
}
|
||||
|
||||
@ResponseStatus(value = org.springframework.http.HttpStatus.CREATED)
|
||||
@PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
|
||||
@Operation(summary = "Upload a file")
|
||||
fun create(
|
||||
@RequestPart file: MultipartFile,
|
||||
): UploaderEntity {
|
||||
val entity = UploaderEntity.fromFile(file)
|
||||
return repository.save(entity)
|
||||
}
|
||||
|
||||
@ResponseStatus(value = org.springframework.http.HttpStatus.OK)
|
||||
@PutMapping("/{id}")
|
||||
@Operation(summary = "Update a file by id")
|
||||
fun update(
|
||||
@PathVariable id: String,
|
||||
@RequestBody body: UploaderEntity
|
||||
): UploaderEntity {
|
||||
val user = repository.findById(UUID.fromString(id)).orElseThrow {
|
||||
val entity = repository.findById(UUID.fromString(id)).orElseThrow {
|
||||
throw IllegalArgumentException("File not found")
|
||||
}
|
||||
body.id = user.id
|
||||
body.id = entity.id
|
||||
return repository.save(body)
|
||||
}
|
||||
|
||||
@ResponseStatus(value = org.springframework.http.HttpStatus.NO_CONTENT)
|
||||
@DeleteMapping("/{id}")
|
||||
@Operation(summary = "Delete a file by id")
|
||||
fun delete(
|
||||
@PathVariable id: String,
|
||||
) {
|
||||
val user = repository.findById(UUID.fromString(id)).orElseThrow {
|
||||
val entity = repository.findById(UUID.fromString(id)).orElseThrow {
|
||||
throw IllegalArgumentException("File not found")
|
||||
}
|
||||
repository.delete(user)
|
||||
repository.delete(entity)
|
||||
}
|
||||
|
||||
@ResponseStatus(value = org.springframework.http.HttpStatus.NO_CONTENT)
|
||||
@DeleteMapping
|
||||
@Operation(summary = "Delete all files")
|
||||
fun deleteAll() {
|
||||
repository.deleteAll()
|
||||
}
|
||||
}
|
@ -1,6 +1,13 @@
|
||||
package com.cubetiqs.web.modules.uploader
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import org.hibernate.Hibernate
|
||||
import org.springframework.data.annotation.CreatedDate
|
||||
import org.springframework.data.annotation.LastModifiedDate
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.Serializable
|
||||
import java.util.*
|
||||
import javax.persistence.*
|
||||
@ -8,6 +15,7 @@ import javax.persistence.*
|
||||
@UploaderModule
|
||||
@Entity
|
||||
@Table(name = "uploader")
|
||||
@EntityListeners(AuditingEntityListener::class)
|
||||
open class UploaderEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
@ -27,12 +35,40 @@ open class UploaderEntity(
|
||||
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
@Column(name = "created_at")
|
||||
@CreatedDate
|
||||
open var createdAt: Date? = null,
|
||||
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
@Column(name = "updated_at")
|
||||
@LastModifiedDate
|
||||
open var updatedAt: Date? = null,
|
||||
|
||||
@Column(length = 30)
|
||||
open var providerType: String? = null,
|
||||
) : Serializable {
|
||||
@Transient
|
||||
@JsonIgnore
|
||||
private var partFile: MultipartFile? = null
|
||||
|
||||
@Transient
|
||||
@JsonIgnore
|
||||
private var file: File? = null
|
||||
|
||||
@Transient
|
||||
fun isFileExists(): Boolean {
|
||||
val temp = getFile()
|
||||
return temp?.exists() ?: false
|
||||
}
|
||||
|
||||
@Transient
|
||||
@JsonIgnore
|
||||
fun getFile(): File? {
|
||||
if (file == null) {
|
||||
file = File(path ?: return null)
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false
|
||||
@ -42,4 +78,65 @@ open class UploaderEntity(
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = javaClass.hashCode()
|
||||
|
||||
companion object {
|
||||
fun fromFile(file: MultipartFile): UploaderEntity {
|
||||
// transfer to file storage first
|
||||
val store = FileStorageFactory.store(file)
|
||||
val uploader = UploaderEntity()
|
||||
uploader.partFile = file
|
||||
uploader.providerType = "local"
|
||||
uploader.filename = file.originalFilename
|
||||
uploader.contentType = file.contentType
|
||||
uploader.contentLength = file.size
|
||||
uploader.path = store.shortPath
|
||||
return uploader
|
||||
}
|
||||
|
||||
fun fromUploader(uploader: UploaderEntity): MultipartFile? {
|
||||
if (uploader.partFile != null) {
|
||||
return uploader.partFile
|
||||
}
|
||||
|
||||
val file = try {
|
||||
File(uploader.path!!)
|
||||
} catch (ex: Exception) {
|
||||
null
|
||||
} ?: return null
|
||||
|
||||
return object : MultipartFile {
|
||||
override fun getInputStream(): InputStream {
|
||||
return file.inputStream()
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
return file.name
|
||||
}
|
||||
|
||||
override fun getOriginalFilename(): String? {
|
||||
return uploader.filename
|
||||
}
|
||||
|
||||
override fun getContentType(): String? {
|
||||
return uploader.contentType
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean {
|
||||
return file.length() == 0L
|
||||
}
|
||||
|
||||
override fun getSize(): Long {
|
||||
return file.length()
|
||||
}
|
||||
|
||||
override fun getBytes(): ByteArray {
|
||||
return file.readBytes()
|
||||
}
|
||||
|
||||
override fun transferTo(dest: File) {
|
||||
file.copyTo(dest)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package com.cubetiqs.web.modules.user
|
||||
|
||||
import com.cubetiqs.web.util.RouteConstants
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.Parameter
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import org.springdoc.core.converters.models.PageableAsQueryParam
|
||||
@ -19,6 +20,7 @@ class UserController @Autowired constructor(
|
||||
) {
|
||||
@GetMapping
|
||||
@PageableAsQueryParam
|
||||
@Operation(summary = "Get all users")
|
||||
fun getAll(
|
||||
@Parameter(hidden = true)
|
||||
pageable: Pageable?,
|
||||
@ -26,8 +28,21 @@ class UserController @Autowired constructor(
|
||||
return repository.findAll(pageable ?: Pageable.unpaged())
|
||||
}
|
||||
|
||||
@ResponseStatus(value = org.springframework.http.HttpStatus.OK)
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "Get a user by id")
|
||||
fun get(
|
||||
@PathVariable id: String,
|
||||
): UserEntity {
|
||||
val entity = repository.findById(UUID.fromString(id)).orElseThrow {
|
||||
throw IllegalArgumentException("User not found")
|
||||
}
|
||||
return repository.save(entity)
|
||||
}
|
||||
|
||||
@ResponseStatus(value = org.springframework.http.HttpStatus.CREATED)
|
||||
@PostMapping
|
||||
@Operation(summary = "Create a user")
|
||||
fun create(
|
||||
@RequestBody body: UserEntity
|
||||
): UserEntity {
|
||||
@ -36,25 +51,34 @@ class UserController @Autowired constructor(
|
||||
|
||||
@ResponseStatus(value = org.springframework.http.HttpStatus.OK)
|
||||
@PutMapping("/{id}")
|
||||
@Operation(summary = "Update a user by id")
|
||||
fun update(
|
||||
@PathVariable id: String,
|
||||
@RequestBody body: UserEntity
|
||||
): UserEntity {
|
||||
val user = repository.findById(UUID.fromString(id)).orElseThrow {
|
||||
val entity = repository.findById(UUID.fromString(id)).orElseThrow {
|
||||
throw IllegalArgumentException("User not found")
|
||||
}
|
||||
body.id = user.id
|
||||
body.id = entity.id
|
||||
return repository.save(body)
|
||||
}
|
||||
|
||||
@ResponseStatus(value = org.springframework.http.HttpStatus.NO_CONTENT)
|
||||
@DeleteMapping("/{id}")
|
||||
@Operation(summary = "Delete user by id")
|
||||
fun delete(
|
||||
@PathVariable id: String,
|
||||
) {
|
||||
val user = repository.findById(UUID.fromString(id)).orElseThrow {
|
||||
val entity = repository.findById(UUID.fromString(id)).orElseThrow {
|
||||
throw IllegalArgumentException("User not found")
|
||||
}
|
||||
repository.delete(user)
|
||||
repository.delete(entity)
|
||||
}
|
||||
|
||||
@ResponseStatus(value = org.springframework.http.HttpStatus.NO_CONTENT)
|
||||
@DeleteMapping
|
||||
@Operation(summary = "Delete all users")
|
||||
fun deleteAll() {
|
||||
repository.deleteAll()
|
||||
}
|
||||
}
|
@ -34,6 +34,8 @@ module:
|
||||
enabled: ${MODULE_USER_ENABLED:true}
|
||||
uploader:
|
||||
enabled: ${MODULE_UPLOADER_ENABLED:true}
|
||||
local:
|
||||
path: ${MODULE_UPLOADER_FILE_PATH:${cubetiq.app.data-dir}/uploads}
|
||||
|
||||
cubetiq:
|
||||
app:
|
||||
|
Loading…
Reference in New Issue
Block a user