Add uploader and file storage provider with openapi docs and demo for modules
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Sambo Chea 2022-04-21 11:57:44 +07:00
parent 4910bd9122
commit 048c11ee62
Signed by: sombochea
GPG Key ID: 3C7CF22A05D95490
10 changed files with 333 additions and 13 deletions

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,19 @@
package com.cubetiqs.web.modules.uploader package com.cubetiqs.web.modules.uploader
import com.cubetiqs.web.util.RouteConstants 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.Parameter
import io.swagger.v3.oas.annotations.tags.Tag import io.swagger.v3.oas.annotations.tags.Tag
import org.springdoc.core.converters.models.PageableAsQueryParam import org.springdoc.core.converters.models.PageableAsQueryParam
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.domain.Page import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable 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.bind.annotation.*
import org.springframework.web.multipart.MultipartFile
import java.util.* import java.util.*
import javax.servlet.http.HttpServletResponse
@UploaderModule @UploaderModule
@Tag(name = "Uploader Controller") @Tag(name = "Uploader Controller")
@ -19,6 +24,7 @@ class UploaderController @Autowired constructor(
) { ) {
@GetMapping @GetMapping
@PageableAsQueryParam @PageableAsQueryParam
@Operation(summary = "Get all files")
fun getAll( fun getAll(
@Parameter(hidden = true) @Parameter(hidden = true)
pageable: Pageable?, pageable: Pageable?,
@ -26,35 +32,88 @@ class UploaderController @Autowired constructor(
return repository.findAll(pageable ?: Pageable.unpaged()) return repository.findAll(pageable ?: Pageable.unpaged())
} }
@ResponseStatus(value = org.springframework.http.HttpStatus.CREATED) @ResponseStatus(value = org.springframework.http.HttpStatus.OK)
@PostMapping @GetMapping("/{id}")
fun create( @Operation(summary = "Get a file by id")
@RequestBody body: UploaderEntity fun get(
@PathVariable id: String,
): UploaderEntity { ): 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) @ResponseStatus(value = org.springframework.http.HttpStatus.OK)
@PutMapping("/{id}") @PutMapping("/{id}")
@Operation(summary = "Update a file by id")
fun update( fun update(
@PathVariable id: String, @PathVariable id: String,
@RequestBody body: UploaderEntity @RequestBody body: UploaderEntity
): UploaderEntity { ): UploaderEntity {
val user = repository.findById(UUID.fromString(id)).orElseThrow { val entity = repository.findById(UUID.fromString(id)).orElseThrow {
throw IllegalArgumentException("File not found") throw IllegalArgumentException("File not found")
} }
body.id = user.id body.id = entity.id
return repository.save(body) return repository.save(body)
} }
@ResponseStatus(value = org.springframework.http.HttpStatus.NO_CONTENT) @ResponseStatus(value = org.springframework.http.HttpStatus.NO_CONTENT)
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@Operation(summary = "Delete a file by id")
fun delete( fun delete(
@PathVariable id: String, @PathVariable id: String,
) { ) {
val user = repository.findById(UUID.fromString(id)).orElseThrow { val entity = repository.findById(UUID.fromString(id)).orElseThrow {
throw IllegalArgumentException("File not found") 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()
} }
} }

View File

@ -1,6 +1,13 @@
package com.cubetiqs.web.modules.uploader package com.cubetiqs.web.modules.uploader
import com.fasterxml.jackson.annotation.JsonIgnore
import org.hibernate.Hibernate 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.io.Serializable
import java.util.* import java.util.*
import javax.persistence.* import javax.persistence.*
@ -8,6 +15,7 @@ import javax.persistence.*
@UploaderModule @UploaderModule
@Entity @Entity
@Table(name = "uploader") @Table(name = "uploader")
@EntityListeners(AuditingEntityListener::class)
open class UploaderEntity( open class UploaderEntity(
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
@ -27,12 +35,40 @@ open class UploaderEntity(
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
@Column(name = "created_at") @Column(name = "created_at")
@CreatedDate
open var createdAt: Date? = null, open var createdAt: Date? = null,
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
@Column(name = "updated_at") @Column(name = "updated_at")
@LastModifiedDate
open var updatedAt: Date? = null, open var updatedAt: Date? = null,
@Column(length = 30)
open var providerType: String? = null,
) : Serializable { ) : 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 { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false
@ -42,4 +78,65 @@ open class UploaderEntity(
} }
override fun hashCode(): Int = javaClass.hashCode() 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)
}
}
}
}
} }

View File

@ -1,6 +1,7 @@
package com.cubetiqs.web.modules.user package com.cubetiqs.web.modules.user
import com.cubetiqs.web.util.RouteConstants 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.Parameter
import io.swagger.v3.oas.annotations.tags.Tag import io.swagger.v3.oas.annotations.tags.Tag
import org.springdoc.core.converters.models.PageableAsQueryParam import org.springdoc.core.converters.models.PageableAsQueryParam
@ -19,6 +20,7 @@ class UserController @Autowired constructor(
) { ) {
@GetMapping @GetMapping
@PageableAsQueryParam @PageableAsQueryParam
@Operation(summary = "Get all users")
fun getAll( fun getAll(
@Parameter(hidden = true) @Parameter(hidden = true)
pageable: Pageable?, pageable: Pageable?,
@ -26,8 +28,21 @@ class UserController @Autowired constructor(
return repository.findAll(pageable ?: Pageable.unpaged()) 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) @ResponseStatus(value = org.springframework.http.HttpStatus.CREATED)
@PostMapping @PostMapping
@Operation(summary = "Create a user")
fun create( fun create(
@RequestBody body: UserEntity @RequestBody body: UserEntity
): UserEntity { ): UserEntity {
@ -36,25 +51,34 @@ class UserController @Autowired constructor(
@ResponseStatus(value = org.springframework.http.HttpStatus.OK) @ResponseStatus(value = org.springframework.http.HttpStatus.OK)
@PutMapping("/{id}") @PutMapping("/{id}")
@Operation(summary = "Update a user by id")
fun update( fun update(
@PathVariable id: String, @PathVariable id: String,
@RequestBody body: UserEntity @RequestBody body: UserEntity
): UserEntity { ): UserEntity {
val user = repository.findById(UUID.fromString(id)).orElseThrow { val entity = repository.findById(UUID.fromString(id)).orElseThrow {
throw IllegalArgumentException("User not found") throw IllegalArgumentException("User not found")
} }
body.id = user.id body.id = entity.id
return repository.save(body) return repository.save(body)
} }
@ResponseStatus(value = org.springframework.http.HttpStatus.NO_CONTENT) @ResponseStatus(value = org.springframework.http.HttpStatus.NO_CONTENT)
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@Operation(summary = "Delete user by id")
fun delete( fun delete(
@PathVariable id: String, @PathVariable id: String,
) { ) {
val user = repository.findById(UUID.fromString(id)).orElseThrow { val entity = repository.findById(UUID.fromString(id)).orElseThrow {
throw IllegalArgumentException("User not found") 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()
} }
} }

View File

@ -34,6 +34,8 @@ module:
enabled: ${MODULE_USER_ENABLED:true} enabled: ${MODULE_USER_ENABLED:true}
uploader: uploader:
enabled: ${MODULE_UPLOADER_ENABLED:true} enabled: ${MODULE_UPLOADER_ENABLED:true}
local:
path: ${MODULE_UPLOADER_FILE_PATH:${cubetiq.app.data-dir}/uploads}
cubetiq: cubetiq:
app: app: