mirror of https://github.com/usbharu/Hideout.git
Merge pull request #71 from usbharu/feature/media-upload
Feature/media upload
This commit is contained in:
commit
889aaf60b2
|
@ -60,6 +60,8 @@ tasks.create<GenerateTask>("openApiGenerateMastodonCompatibleApi", GenerateTask:
|
||||||
configOptions.put("interfaceOnly", "true")
|
configOptions.put("interfaceOnly", "true")
|
||||||
configOptions.put("useSpringBoot3", "true")
|
configOptions.put("useSpringBoot3", "true")
|
||||||
additionalProperties.put("useTags", "true")
|
additionalProperties.put("useTags", "true")
|
||||||
|
importMappings.put("org.springframework.core.io.Resource", "org.springframework.web.multipart.MultipartFile")
|
||||||
|
typeMappings.put("org.springframework.core.io.Resource", "org.springframework.web.multipart.MultipartFile")
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
@ -116,6 +118,9 @@ dependencies {
|
||||||
implementation("org.springframework.security:spring-security-oauth2-jose")
|
implementation("org.springframework.security:spring-security-oauth2-jose")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
|
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
|
||||||
implementation("org.jetbrains.exposed:exposed-spring-boot-starter:0.44.0")
|
implementation("org.jetbrains.exposed:exposed-spring-boot-starter:0.44.0")
|
||||||
|
implementation("io.trbl:blurhash:1.0.0")
|
||||||
|
implementation("software.amazon.awssdk:s3:2.20.157")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")
|
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")
|
||||||
|
|
|
@ -3,6 +3,7 @@ build:
|
||||||
weights:
|
weights:
|
||||||
Indentation: 0
|
Indentation: 0
|
||||||
MagicNumber: 0
|
MagicNumber: 0
|
||||||
|
InjectDispatcher: 0
|
||||||
|
|
||||||
style:
|
style:
|
||||||
ClassOrdering:
|
ClassOrdering:
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package dev.usbharu.hideout.config
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
|
||||||
|
import software.amazon.awssdk.regions.Region
|
||||||
|
import software.amazon.awssdk.services.s3.S3Client
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class AwsConfig {
|
||||||
|
@Bean
|
||||||
|
fun s3Client(awsConfig: StorageConfig): S3Client {
|
||||||
|
return S3Client.builder()
|
||||||
|
.endpointOverride(URI.create(awsConfig.endpoint))
|
||||||
|
.region(Region.of(awsConfig.region))
|
||||||
|
.credentialsProvider { AwsBasicCredentials.create(awsConfig.accessKey, awsConfig.secretKey) }
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,9 +10,23 @@ class SpringConfig {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
lateinit var config: ApplicationConfig
|
lateinit var config: ApplicationConfig
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
lateinit var storageConfig: StorageConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@ConfigurationProperties("hideout")
|
@ConfigurationProperties("hideout")
|
||||||
data class ApplicationConfig(
|
data class ApplicationConfig(
|
||||||
val url: URL
|
val url: URL
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ConfigurationProperties("hideout.storage")
|
||||||
|
data class StorageConfig(
|
||||||
|
val useS3: Boolean,
|
||||||
|
val endpoint: String,
|
||||||
|
val publicUrl: String,
|
||||||
|
val bucket: String,
|
||||||
|
val region: String,
|
||||||
|
val accessKey: String,
|
||||||
|
val secretKey: String
|
||||||
|
)
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package dev.usbharu.hideout.controller.mastodon
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.controller.mastodon.generated.MediaApi
|
||||||
|
import dev.usbharu.hideout.domain.mastodon.model.generated.MediaAttachment
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.form.Media
|
||||||
|
import dev.usbharu.hideout.service.api.mastodon.MediaApiService
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.stereotype.Controller
|
||||||
|
import org.springframework.web.multipart.MultipartFile
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
class MastodonMediaApiController(private val mediaApiService: MediaApiService) : MediaApi {
|
||||||
|
override fun apiV1MediaPost(
|
||||||
|
file: MultipartFile,
|
||||||
|
thumbnail: MultipartFile?,
|
||||||
|
description: String?,
|
||||||
|
focus: String?
|
||||||
|
): ResponseEntity<MediaAttachment> = runBlocking {
|
||||||
|
ResponseEntity.ok(
|
||||||
|
mediaApiService.postMedia(
|
||||||
|
Media(
|
||||||
|
file,
|
||||||
|
thumbnail,
|
||||||
|
description,
|
||||||
|
focus
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package dev.usbharu.hideout.domain.model
|
||||||
|
|
||||||
|
data class MediaSave(
|
||||||
|
val name: String,
|
||||||
|
val prefix: String,
|
||||||
|
val fileInputStream: ByteArray,
|
||||||
|
val thumbnailInputStream: ByteArray?
|
||||||
|
)
|
|
@ -0,0 +1,8 @@
|
||||||
|
package dev.usbharu.hideout.domain.model.hideout.dto
|
||||||
|
|
||||||
|
enum class FileType {
|
||||||
|
Image,
|
||||||
|
Video,
|
||||||
|
Audio,
|
||||||
|
Unknown
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package dev.usbharu.hideout.domain.model.hideout.dto
|
||||||
|
|
||||||
|
data class ProcessedFile(
|
||||||
|
val byteArray: ByteArray,
|
||||||
|
val extension: String
|
||||||
|
)
|
|
@ -0,0 +1,6 @@
|
||||||
|
package dev.usbharu.hideout.domain.model.hideout.dto
|
||||||
|
|
||||||
|
data class ProcessedMedia(
|
||||||
|
val file: ProcessedFile,
|
||||||
|
val thumbnail: ProcessedFile?
|
||||||
|
)
|
|
@ -0,0 +1,7 @@
|
||||||
|
package dev.usbharu.hideout.domain.model.hideout.dto
|
||||||
|
|
||||||
|
data class RemoteMedia(
|
||||||
|
val name: String,
|
||||||
|
val url: String,
|
||||||
|
val mediaType: String
|
||||||
|
)
|
|
@ -0,0 +1,16 @@
|
||||||
|
package dev.usbharu.hideout.domain.model.hideout.dto
|
||||||
|
|
||||||
|
sealed class SavedMedia(val success: Boolean)
|
||||||
|
|
||||||
|
class SuccessSavedMedia(
|
||||||
|
val name: String,
|
||||||
|
val url: String,
|
||||||
|
val thumbnailUrl: String,
|
||||||
|
) :
|
||||||
|
SavedMedia(true)
|
||||||
|
|
||||||
|
class FaildSavedMedia(
|
||||||
|
val reason: String,
|
||||||
|
val description: String,
|
||||||
|
val trace: Throwable? = null
|
||||||
|
) : SavedMedia(false)
|
|
@ -0,0 +1,13 @@
|
||||||
|
package dev.usbharu.hideout.domain.model.hideout.entity
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
|
||||||
|
|
||||||
|
data class Media(
|
||||||
|
val id: Long,
|
||||||
|
val name: String,
|
||||||
|
val url: String,
|
||||||
|
val remoteUrl: String?,
|
||||||
|
val thumbnailUrl: String?,
|
||||||
|
val type: FileType,
|
||||||
|
val blurHash: String?
|
||||||
|
)
|
|
@ -0,0 +1,10 @@
|
||||||
|
package dev.usbharu.hideout.domain.model.hideout.form
|
||||||
|
|
||||||
|
import org.springframework.web.multipart.MultipartFile
|
||||||
|
|
||||||
|
data class Media(
|
||||||
|
val file: MultipartFile,
|
||||||
|
val thumbnail: MultipartFile?,
|
||||||
|
val description: String?,
|
||||||
|
val focus: String?
|
||||||
|
)
|
|
@ -0,0 +1,14 @@
|
||||||
|
package dev.usbharu.hideout.exception.media
|
||||||
|
|
||||||
|
open class MediaConvertException : MediaException {
|
||||||
|
constructor() : super()
|
||||||
|
constructor(message: String?) : super(message)
|
||||||
|
constructor(message: String?, cause: Throwable?) : super(message, cause)
|
||||||
|
constructor(cause: Throwable?) : super(cause)
|
||||||
|
constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(
|
||||||
|
message,
|
||||||
|
cause,
|
||||||
|
enableSuppression,
|
||||||
|
writableStackTrace
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package dev.usbharu.hideout.exception.media
|
||||||
|
|
||||||
|
abstract class MediaException : RuntimeException {
|
||||||
|
constructor() : super()
|
||||||
|
constructor(message: String?) : super(message)
|
||||||
|
constructor(message: String?, cause: Throwable?) : super(message, cause)
|
||||||
|
constructor(cause: Throwable?) : super(cause)
|
||||||
|
constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(
|
||||||
|
message,
|
||||||
|
cause,
|
||||||
|
enableSuppression,
|
||||||
|
writableStackTrace
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package dev.usbharu.hideout.exception.media
|
||||||
|
|
||||||
|
open class MediaFileSizeException : MediaException {
|
||||||
|
constructor() : super()
|
||||||
|
constructor(message: String?) : super(message)
|
||||||
|
constructor(message: String?, cause: Throwable?) : super(message, cause)
|
||||||
|
constructor(cause: Throwable?) : super(cause)
|
||||||
|
constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(
|
||||||
|
message,
|
||||||
|
cause,
|
||||||
|
enableSuppression,
|
||||||
|
writableStackTrace
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package dev.usbharu.hideout.exception.media
|
||||||
|
|
||||||
|
class MediaFileSizeIsZeroException : MediaFileSizeException {
|
||||||
|
constructor() : super()
|
||||||
|
constructor(message: String?) : super(message)
|
||||||
|
constructor(message: String?, cause: Throwable?) : super(message, cause)
|
||||||
|
constructor(cause: Throwable?) : super(cause)
|
||||||
|
constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(
|
||||||
|
message,
|
||||||
|
cause,
|
||||||
|
enableSuppression,
|
||||||
|
writableStackTrace
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package dev.usbharu.hideout.exception.media
|
||||||
|
|
||||||
|
open class MediaSaveException : MediaException {
|
||||||
|
constructor() : super()
|
||||||
|
constructor(message: String?) : super(message)
|
||||||
|
constructor(message: String?, cause: Throwable?) : super(message, cause)
|
||||||
|
constructor(cause: Throwable?) : super(cause)
|
||||||
|
constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(
|
||||||
|
message,
|
||||||
|
cause,
|
||||||
|
enableSuppression,
|
||||||
|
writableStackTrace
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package dev.usbharu.hideout.exception.media
|
||||||
|
|
||||||
|
class UnsupportedMediaException : MediaException {
|
||||||
|
constructor() : super()
|
||||||
|
constructor(message: String?) : super(message)
|
||||||
|
constructor(message: String?, cause: Throwable?) : super(message, cause)
|
||||||
|
constructor(cause: Throwable?) : super(cause)
|
||||||
|
constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(
|
||||||
|
message,
|
||||||
|
cause,
|
||||||
|
enableSuppression,
|
||||||
|
writableStackTrace
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package dev.usbharu.hideout.repository
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.entity.Media
|
||||||
|
|
||||||
|
interface MediaRepository {
|
||||||
|
suspend fun generateId(): Long
|
||||||
|
suspend fun save(media: Media): Media
|
||||||
|
suspend fun findById(id: Long): Media
|
||||||
|
suspend fun delete(id: Long)
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
package dev.usbharu.hideout.repository
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
|
||||||
|
import dev.usbharu.hideout.exception.FailedToGetResourcesException
|
||||||
|
import dev.usbharu.hideout.service.core.IdGenerateService
|
||||||
|
import dev.usbharu.hideout.util.singleOr
|
||||||
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.entity.Media as EntityMedia
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
class MediaRepositoryImpl(private val idGenerateService: IdGenerateService) : MediaRepository {
|
||||||
|
override suspend fun generateId(): Long = idGenerateService.generateId()
|
||||||
|
|
||||||
|
override suspend fun save(media: EntityMedia): EntityMedia {
|
||||||
|
if (Media.select {
|
||||||
|
Media.id eq media.id
|
||||||
|
}.singleOrNull() != null
|
||||||
|
) {
|
||||||
|
Media.update({ Media.id eq media.id }) {
|
||||||
|
it[Media.name] = media.name
|
||||||
|
it[Media.url] = media.url
|
||||||
|
it[Media.remoteUrl] = media.remoteUrl
|
||||||
|
it[Media.thumbnailUrl] = media.thumbnailUrl
|
||||||
|
it[Media.type] = media.type.ordinal
|
||||||
|
it[Media.blurhash] = media.blurHash
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Media.insert {
|
||||||
|
it[Media.id] = media.id
|
||||||
|
it[Media.name] = media.name
|
||||||
|
it[Media.url] = media.url
|
||||||
|
it[Media.remoteUrl] = media.remoteUrl
|
||||||
|
it[Media.thumbnailUrl] = media.thumbnailUrl
|
||||||
|
it[Media.type] = media.type.ordinal
|
||||||
|
it[Media.blurhash] = media.blurHash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return media
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findById(id: Long): EntityMedia {
|
||||||
|
return Media
|
||||||
|
.select {
|
||||||
|
Media.id eq id
|
||||||
|
}
|
||||||
|
.singleOr {
|
||||||
|
FailedToGetResourcesException("id: $id was not found.")
|
||||||
|
}.toMedia()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(id: Long) {
|
||||||
|
Media.deleteWhere {
|
||||||
|
Media.id eq id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ResultRow.toMedia(): EntityMedia {
|
||||||
|
return EntityMedia(
|
||||||
|
id = this[Media.id],
|
||||||
|
name = this[Media.name],
|
||||||
|
url = this[Media.url],
|
||||||
|
remoteUrl = this[Media.remoteUrl],
|
||||||
|
thumbnailUrl = this[Media.thumbnailUrl],
|
||||||
|
type = FileType.values().first { it.ordinal == this[Media.type] },
|
||||||
|
blurHash = this[Media.blurhash],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Media : Table("media") {
|
||||||
|
val id = long("id")
|
||||||
|
val name = varchar("name", 255)
|
||||||
|
val url = varchar("url", 255)
|
||||||
|
val remoteUrl = varchar("remote_url", 255).nullable()
|
||||||
|
val thumbnailUrl = varchar("thumbnail_url", 255).nullable()
|
||||||
|
val type = integer("type")
|
||||||
|
val blurhash = varchar("blurhash", 255).nullable()
|
||||||
|
}
|
|
@ -12,7 +12,6 @@ class ReactionRepositoryImpl(
|
||||||
private val idGenerateService: IdGenerateService
|
private val idGenerateService: IdGenerateService
|
||||||
) : ReactionRepository {
|
) : ReactionRepository {
|
||||||
|
|
||||||
|
|
||||||
override suspend fun generateId(): Long = idGenerateService.generateId()
|
override suspend fun generateId(): Long = idGenerateService.generateId()
|
||||||
|
|
||||||
override suspend fun save(reaction: Reaction): Reaction {
|
override suspend fun save(reaction: Reaction): Reaction {
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package dev.usbharu.hideout.service.api.mastodon
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.mastodon.model.generated.MediaAttachment
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.form.Media
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
interface MediaApiService {
|
||||||
|
suspend fun postMedia(media: Media): MediaAttachment
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package dev.usbharu.hideout.service.api.mastodon
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.mastodon.model.generated.MediaAttachment
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.form.Media
|
||||||
|
import dev.usbharu.hideout.service.core.Transaction
|
||||||
|
import dev.usbharu.hideout.service.media.MediaService
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class MediaApiServiceImpl(private val mediaService: MediaService, private val transaction: Transaction) :
|
||||||
|
MediaApiService {
|
||||||
|
|
||||||
|
override suspend fun postMedia(media: Media): MediaAttachment {
|
||||||
|
return transaction.transaction {
|
||||||
|
val uploadLocalMedia = mediaService.uploadLocalMedia(media)
|
||||||
|
val type = when (uploadLocalMedia.type) {
|
||||||
|
FileType.Image -> MediaAttachment.Type.image
|
||||||
|
FileType.Video -> MediaAttachment.Type.video
|
||||||
|
FileType.Audio -> MediaAttachment.Type.audio
|
||||||
|
FileType.Unknown -> MediaAttachment.Type.unknown
|
||||||
|
}
|
||||||
|
return@transaction MediaAttachment(
|
||||||
|
id = uploadLocalMedia.id.toString(),
|
||||||
|
type = type,
|
||||||
|
url = uploadLocalMedia.url,
|
||||||
|
previewUrl = uploadLocalMedia.thumbnailUrl,
|
||||||
|
remoteUrl = null,
|
||||||
|
description = media.description,
|
||||||
|
blurhash = uploadLocalMedia.blurHash,
|
||||||
|
textUrl = uploadLocalMedia.url
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package dev.usbharu.hideout.service.media
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
|
||||||
|
|
||||||
|
interface FileTypeDeterminationService {
|
||||||
|
fun fileType(byteArray: ByteArray, filename: String, contentType: String?): FileType
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package dev.usbharu.hideout.service.media
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class FileTypeDeterminationServiceImpl : FileTypeDeterminationService {
|
||||||
|
override fun fileType(
|
||||||
|
byteArray: ByteArray,
|
||||||
|
filename: String,
|
||||||
|
contentType: String?
|
||||||
|
): FileType {
|
||||||
|
if (contentType == null) {
|
||||||
|
return FileType.Unknown
|
||||||
|
}
|
||||||
|
if (contentType.startsWith("image")) {
|
||||||
|
return FileType.Image
|
||||||
|
}
|
||||||
|
if (contentType.startsWith("video")) {
|
||||||
|
return FileType.Video
|
||||||
|
}
|
||||||
|
if (contentType.startsWith("audio")) {
|
||||||
|
return FileType.Audio
|
||||||
|
}
|
||||||
|
return FileType.Unknown
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package dev.usbharu.hideout.service.media
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
|
||||||
|
interface MediaBlurhashService {
|
||||||
|
fun generateBlurhash(bufferedImage: BufferedImage): String
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package dev.usbharu.hideout.service.media
|
||||||
|
|
||||||
|
import io.trbl.blurhash.BlurHash
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class MediaBlurhashServiceImpl : MediaBlurhashService {
|
||||||
|
override fun generateBlurhash(bufferedImage: BufferedImage): String = BlurHash.encode(bufferedImage)
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package dev.usbharu.hideout.service.media
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.MediaSave
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.SavedMedia
|
||||||
|
|
||||||
|
interface MediaDataStore {
|
||||||
|
suspend fun save(dataMediaSave: MediaSave): SavedMedia
|
||||||
|
suspend fun delete(id: String)
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package dev.usbharu.hideout.service.media
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.RemoteMedia
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.form.Media
|
||||||
|
|
||||||
|
interface MediaService {
|
||||||
|
suspend fun uploadLocalMedia(media: Media): dev.usbharu.hideout.domain.model.hideout.entity.Media
|
||||||
|
suspend fun uploadRemoteMedia(remoteMedia: RemoteMedia)
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
package dev.usbharu.hideout.service.media
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.MediaSave
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.FaildSavedMedia
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.RemoteMedia
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.SuccessSavedMedia
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.form.Media
|
||||||
|
import dev.usbharu.hideout.exception.media.MediaFileSizeIsZeroException
|
||||||
|
import dev.usbharu.hideout.exception.media.MediaSaveException
|
||||||
|
import dev.usbharu.hideout.exception.media.UnsupportedMediaException
|
||||||
|
import dev.usbharu.hideout.repository.MediaRepository
|
||||||
|
import dev.usbharu.hideout.service.media.converter.MediaProcessService
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import java.util.*
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.entity.Media as EntityMedia
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class MediaServiceImpl(
|
||||||
|
private val mediaDataStore: MediaDataStore,
|
||||||
|
private val fileTypeDeterminationService: FileTypeDeterminationService,
|
||||||
|
private val mediaBlurhashService: MediaBlurhashService,
|
||||||
|
private val mediaRepository: MediaRepository,
|
||||||
|
private val mediaProcessService: MediaProcessService
|
||||||
|
) : MediaService {
|
||||||
|
override suspend fun uploadLocalMedia(media: Media): EntityMedia {
|
||||||
|
logger.info(
|
||||||
|
"Media upload. filename:${media.file.name} size:${media.file.size} contentType:${media.file.contentType}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (media.file.size == 0L) {
|
||||||
|
throw MediaFileSizeIsZeroException("Media file size is zero.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileType = fileTypeDeterminationService.fileType(media.file.bytes, media.file.name, media.file.contentType)
|
||||||
|
if (fileType != FileType.Image) {
|
||||||
|
throw UnsupportedMediaException("FileType: $fileType is not supported.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val process = mediaProcessService.process(
|
||||||
|
fileType,
|
||||||
|
media.file.contentType.orEmpty(),
|
||||||
|
media.file.name,
|
||||||
|
media.file.bytes,
|
||||||
|
media.thumbnail?.bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
val dataMediaSave = MediaSave(
|
||||||
|
"${UUID.randomUUID()}.${process.file.extension}",
|
||||||
|
"",
|
||||||
|
process.file.byteArray,
|
||||||
|
process.thumbnail?.byteArray
|
||||||
|
)
|
||||||
|
val save = try {
|
||||||
|
mediaDataStore.save(dataMediaSave)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.warn("Failed save media", e)
|
||||||
|
throw MediaSaveException("Failed save media.", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (save.success.not()) {
|
||||||
|
save as FaildSavedMedia
|
||||||
|
logger.warn("Failed save media. reason: ${save.reason}")
|
||||||
|
logger.warn(save.description, save.trace)
|
||||||
|
throw MediaSaveException("Failed save media.")
|
||||||
|
}
|
||||||
|
save as SuccessSavedMedia
|
||||||
|
|
||||||
|
val blurHash = withContext(Dispatchers.IO) {
|
||||||
|
mediaBlurhashService.generateBlurhash(ImageIO.read(media.file.bytes.inputStream()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return mediaRepository.save(
|
||||||
|
EntityMedia(
|
||||||
|
id = mediaRepository.generateId(),
|
||||||
|
name = media.file.name,
|
||||||
|
url = save.url,
|
||||||
|
remoteUrl = null,
|
||||||
|
thumbnailUrl = save.thumbnailUrl,
|
||||||
|
type = fileType,
|
||||||
|
blurHash = blurHash
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun uploadRemoteMedia(remoteMedia: RemoteMedia) = Unit
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(MediaServiceImpl::class.java)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package dev.usbharu.hideout.service.media
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.config.StorageConfig
|
||||||
|
import dev.usbharu.hideout.domain.model.MediaSave
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.SavedMedia
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.SuccessSavedMedia
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import software.amazon.awssdk.core.sync.RequestBody
|
||||||
|
import software.amazon.awssdk.services.s3.S3Client
|
||||||
|
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest
|
||||||
|
import software.amazon.awssdk.services.s3.model.GetUrlRequest
|
||||||
|
import software.amazon.awssdk.services.s3.model.PutObjectRequest
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig: StorageConfig) : MediaDataStore {
|
||||||
|
override suspend fun save(dataMediaSave: MediaSave): SavedMedia {
|
||||||
|
val fileUploadRequest = PutObjectRequest.builder()
|
||||||
|
.bucket(storageConfig.bucket)
|
||||||
|
.key(dataMediaSave.name)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val thumbnailKey = "thumbnail-${dataMediaSave.name}"
|
||||||
|
val thumbnailUploadRequest = PutObjectRequest.builder()
|
||||||
|
.bucket(storageConfig.bucket)
|
||||||
|
.key(thumbnailKey)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val pairList = withContext(Dispatchers.IO) {
|
||||||
|
awaitAll(
|
||||||
|
async {
|
||||||
|
if (dataMediaSave.thumbnailInputStream != null) {
|
||||||
|
s3Client.putObject(
|
||||||
|
thumbnailUploadRequest,
|
||||||
|
RequestBody.fromBytes(dataMediaSave.thumbnailInputStream)
|
||||||
|
)
|
||||||
|
"thumbnail" to s3Client.utilities()
|
||||||
|
.getUrl(GetUrlRequest.builder().bucket(storageConfig.bucket).key(thumbnailKey).build())
|
||||||
|
} else {
|
||||||
|
"thumbnail" to null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
s3Client.putObject(fileUploadRequest, RequestBody.fromBytes(dataMediaSave.fileInputStream))
|
||||||
|
"file" to s3Client.utilities()
|
||||||
|
.getUrl(GetUrlRequest.builder().bucket(storageConfig.bucket).key(dataMediaSave.name).build())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}.toMap()
|
||||||
|
return SuccessSavedMedia(
|
||||||
|
dataMediaSave.name,
|
||||||
|
pairList.getValue("file").toString(),
|
||||||
|
pairList.getValue("thumbnail").toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(id: String) {
|
||||||
|
val fileDeleteRequest = DeleteObjectRequest.builder().bucket(storageConfig.bucket).key(id).build()
|
||||||
|
val thumbnailDeleteRequest =
|
||||||
|
DeleteObjectRequest.builder().bucket(storageConfig.bucket).key("thumbnail-$id").build()
|
||||||
|
s3Client.deleteObject(fileDeleteRequest)
|
||||||
|
s3Client.deleteObject(thumbnailDeleteRequest)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package dev.usbharu.hideout.service.media
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.ProcessedFile
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
interface ThumbnailGenerateService {
|
||||||
|
fun generate(bufferedImage: InputStream, width: Int, height: Int): ProcessedFile?
|
||||||
|
fun generate(outputStream: ByteArray, width: Int, height: Int): ProcessedFile?
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package dev.usbharu.hideout.service.media
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.ProcessedFile
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class ThumbnailGenerateServiceImpl : ThumbnailGenerateService {
|
||||||
|
override fun generate(bufferedImage: InputStream, width: Int, height: Int): ProcessedFile? {
|
||||||
|
val image = ImageIO.read(bufferedImage)
|
||||||
|
return internalGenerate(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun generate(outputStream: ByteArray, width: Int, height: Int): ProcessedFile? {
|
||||||
|
val image = ImageIO.read(outputStream.inputStream())
|
||||||
|
return internalGenerate(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun internalGenerate(image: BufferedImage): ProcessedFile {
|
||||||
|
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||||
|
ImageIO.write(image, "jpeg", byteArrayOutputStream)
|
||||||
|
return ProcessedFile(byteArrayOutputStream.toByteArray(), "jpg")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package dev.usbharu.hideout.service.media.converter
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.ProcessedFile
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
interface MediaConverter {
|
||||||
|
fun isSupport(fileType: FileType): Boolean
|
||||||
|
fun convert(inputStream: InputStream): ProcessedFile
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package dev.usbharu.hideout.service.media.converter
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.ProcessedFile
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
interface MediaConverterRoot {
|
||||||
|
suspend fun convert(
|
||||||
|
fileType: FileType,
|
||||||
|
contentType: String,
|
||||||
|
filename: String,
|
||||||
|
inputStream: InputStream
|
||||||
|
): ProcessedFile
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package dev.usbharu.hideout.service.media.converter
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.ProcessedFile
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class MediaConverterRootImpl(private val converters: List<MediaConverter>) : MediaConverterRoot {
|
||||||
|
override suspend fun convert(
|
||||||
|
fileType: FileType,
|
||||||
|
contentType: String,
|
||||||
|
filename: String,
|
||||||
|
inputStream: InputStream
|
||||||
|
): ProcessedFile {
|
||||||
|
val convert = converters.find {
|
||||||
|
it.isSupport(fileType)
|
||||||
|
}?.convert(inputStream)
|
||||||
|
if (convert != null) {
|
||||||
|
return convert
|
||||||
|
}
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
if (filename.contains('.')) {
|
||||||
|
ProcessedFile(inputStream.readAllBytes(), filename.substringAfterLast("."))
|
||||||
|
} else {
|
||||||
|
ProcessedFile(inputStream.readAllBytes(), contentType.substringAfterLast("/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package dev.usbharu.hideout.service.media.converter
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.ProcessedMedia
|
||||||
|
|
||||||
|
interface MediaProcessService {
|
||||||
|
suspend fun process(
|
||||||
|
fileType: FileType,
|
||||||
|
contentType: String,
|
||||||
|
fileName: String,
|
||||||
|
file: ByteArray,
|
||||||
|
thumbnail: ByteArray?
|
||||||
|
): ProcessedMedia
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package dev.usbharu.hideout.service.media.converter
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.ProcessedMedia
|
||||||
|
import dev.usbharu.hideout.exception.media.MediaConvertException
|
||||||
|
import dev.usbharu.hideout.service.media.ThumbnailGenerateService
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class MediaProcessServiceImpl(
|
||||||
|
private val mediaConverterRoot: MediaConverterRoot,
|
||||||
|
private val thumbnailGenerateService: ThumbnailGenerateService
|
||||||
|
) : MediaProcessService {
|
||||||
|
override suspend fun process(
|
||||||
|
fileType: FileType,
|
||||||
|
contentType: String,
|
||||||
|
filename: String,
|
||||||
|
file: ByteArray,
|
||||||
|
thumbnail: ByteArray?
|
||||||
|
): ProcessedMedia {
|
||||||
|
val fileInputStream = try {
|
||||||
|
mediaConverterRoot.convert(fileType, contentType, filename, file.inputStream().buffered())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.warn("Failed convert media.", e)
|
||||||
|
throw MediaConvertException("Failed convert media.", e)
|
||||||
|
}
|
||||||
|
val thumbnailInputStream = try {
|
||||||
|
thumbnail?.let { mediaConverterRoot.convert(fileType, contentType, filename, it.inputStream().buffered()) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.warn("Failed convert thumbnail media.", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
return ProcessedMedia(
|
||||||
|
fileInputStream,
|
||||||
|
thumbnailGenerateService.generate(
|
||||||
|
thumbnailInputStream?.byteArray ?: file,
|
||||||
|
2048,
|
||||||
|
2048
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(MediaProcessServiceImpl::class.java)
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,11 @@ hideout:
|
||||||
key-id: a
|
key-id: a
|
||||||
private-key: "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ=="
|
private-key: "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ=="
|
||||||
public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB"
|
public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
jackson:
|
jackson:
|
||||||
serialization:
|
serialization:
|
||||||
|
|
|
@ -17,6 +17,8 @@ tags:
|
||||||
description: instance
|
description: instance
|
||||||
- name: timeline
|
- name: timeline
|
||||||
description: timeline
|
description: timeline
|
||||||
|
- name: media
|
||||||
|
description: media
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
/api/v2/instance:
|
/api/v2/instance:
|
||||||
|
@ -291,9 +293,48 @@ paths:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/Status"
|
$ref: "#/components/schemas/Status"
|
||||||
|
/api/v1/media:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- media
|
||||||
|
security:
|
||||||
|
- OAuth2:
|
||||||
|
- "write:media"
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/V1MediaRequest"
|
||||||
|
encoding:
|
||||||
|
file:
|
||||||
|
contentType: image/jpeg, image/png
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: 成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/MediaAttachment"
|
||||||
|
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
|
V1MediaRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
file:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
thumbnail:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
focus:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- file
|
||||||
|
|
||||||
AccountsCreateRequest:
|
AccountsCreateRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
Loading…
Reference in New Issue