mirror of https://github.com/usbharu/Hideout.git
commit
7c105e946c
|
@ -179,7 +179,9 @@ dependencies {
|
||||||
|
|
||||||
implementation("org.postgresql:postgresql:42.6.0")
|
implementation("org.postgresql:postgresql:42.6.0")
|
||||||
implementation("com.twelvemonkeys.imageio:imageio-webp:3.10.0")
|
implementation("com.twelvemonkeys.imageio:imageio-webp:3.10.0")
|
||||||
|
implementation("org.apache.tika:tika-core:2.9.1")
|
||||||
|
implementation("net.coobird:thumbnailator:0.4.20")
|
||||||
|
implementation("org.bytedeco:javacv-platform:1.5.9")
|
||||||
|
|
||||||
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")
|
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")
|
||||||
|
|
||||||
|
|
|
@ -13,9 +13,9 @@ insert into POSTS (ID, "userId", OVERVIEW, TEXT, "createdAt", VISIBILITY, URL, "
|
||||||
VALUES (1242, 11, null, 'test post', 12345680, 0, 'https://example.com/users/test-user11/posts/1242', null, null, false,
|
VALUES (1242, 11, null, 'test post', 12345680, 0, 'https://example.com/users/test-user11/posts/1242', null, null, false,
|
||||||
'https://example.com/users/test-user11/posts/1242');
|
'https://example.com/users/test-user11/posts/1242');
|
||||||
|
|
||||||
insert into MEDIA (ID, NAME, URL, REMOTE_URL, THUMBNAIL_URL, TYPE, BLURHASH)
|
insert into MEDIA (ID, NAME, URL, REMOTE_URL, THUMBNAIL_URL, TYPE, BLURHASH, MIME_TYPE, DESCRIPTION)
|
||||||
VALUES (1, 'test-media', 'https://example.com/media/test-media.png', null, null, 0, null),
|
VALUES (1, 'test-media', 'https://example.com/media/test-media.png', null, null, 0, null, 'image/png', null),
|
||||||
(2, 'test-media2', 'https://example.com/media/test-media2.png', null, null, 0, null);
|
(2, 'test-media2', 'https://example.com/media/test-media2.png', null, null, 0, null, 'image/png', null);
|
||||||
|
|
||||||
insert into POSTSMEDIA(POST_ID, MEDIA_ID)
|
insert into POSTSMEDIA(POST_ID, MEDIA_ID)
|
||||||
VALUES (1242, 1),
|
VALUES (1242, 1),
|
||||||
|
|
|
@ -41,7 +41,5 @@ open class Delete : Object {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String = "Delete(`object`=$`object`, published=$published) ${super.toString()}"
|
||||||
return "Delete(`object`=$`object`, published=$published) ${super.toString()}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,13 +62,9 @@ class ContextDeserializer : JsonDeserializer<String>() {
|
||||||
|
|
||||||
class ContextSerializer : JsonSerializer<List<String>>() {
|
class ContextSerializer : JsonSerializer<List<String>>() {
|
||||||
|
|
||||||
override fun isEmpty(value: List<String>?): Boolean {
|
override fun isEmpty(value: List<String>?): Boolean = value.isNullOrEmpty()
|
||||||
return value.isNullOrEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isEmpty(provider: SerializerProvider?, value: List<String>?): Boolean {
|
override fun isEmpty(provider: SerializerProvider?, value: List<String>?): Boolean = value.isNullOrEmpty()
|
||||||
return value.isNullOrEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun serialize(value: List<String>?, gen: JsonGenerator?, serializers: SerializerProvider) {
|
override fun serialize(value: List<String>?, gen: JsonGenerator?, serializers: SerializerProvider) {
|
||||||
if (value.isNullOrEmpty()) {
|
if (value.isNullOrEmpty()) {
|
||||||
|
|
|
@ -46,9 +46,7 @@ open class Object : JsonLd {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String = "Object(type=$type, name=$name, actor=$actor, id=$id) ${super.toString()}"
|
||||||
return "Object(type=$type, name=$name, actor=$actor, id=$id) ${super.toString()}"
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|
|
@ -12,7 +12,7 @@ class UserAPControllerImpl(private val apUserService: APUserService) : UserAPCon
|
||||||
override suspend fun userAp(username: String): ResponseEntity<Person> {
|
override suspend fun userAp(username: String): ResponseEntity<Person> {
|
||||||
val person = try {
|
val person = try {
|
||||||
apUserService.getPersonByName(username)
|
apUserService.getPersonByName(username)
|
||||||
} catch (e: FailedToGetResourcesException) {
|
} catch (_: FailedToGetResourcesException) {
|
||||||
return ResponseEntity.notFound().build()
|
return ResponseEntity.notFound().build()
|
||||||
}
|
}
|
||||||
person.context += listOf("https://www.w3.org/ns/activitystreams")
|
person.context += listOf("https://www.w3.org/ns/activitystreams")
|
||||||
|
|
|
@ -22,7 +22,7 @@ class APReceiveDeleteServiceImpl(
|
||||||
|
|
||||||
val post = try {
|
val post = try {
|
||||||
postQueryService.findByApId(deleteId)
|
postQueryService.findByApId(deleteId)
|
||||||
} catch (e: FailedToGetResourcesException) {
|
} catch (_: FailedToGetResourcesException) {
|
||||||
return@transaction ActivityPubStringResponse(HttpStatusCode.OK, "Resource not found or already deleted")
|
return@transaction ActivityPubStringResponse(HttpStatusCode.OK, "Resource not found or already deleted")
|
||||||
}
|
}
|
||||||
postRepository.delete(post.id)
|
postRepository.delete(post.id)
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package dev.usbharu.hideout.activitypub.service.objects.note
|
package dev.usbharu.hideout.activitypub.service.objects.note
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import dev.usbharu.hideout.activitypub.domain.exception.FailedToGetActivityPubResourceException
|
import dev.usbharu.hideout.activitypub.domain.exception.FailedToGetActivityPubResourceException
|
||||||
import dev.usbharu.hideout.activitypub.domain.exception.IllegalActivityPubObjectException
|
import dev.usbharu.hideout.activitypub.domain.exception.IllegalActivityPubObjectException
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.Note
|
import dev.usbharu.hideout.activitypub.domain.model.Note
|
||||||
|
@ -24,7 +23,6 @@ import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.slf4j.MDCContext
|
import kotlinx.coroutines.slf4j.MDCContext
|
||||||
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
|
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Qualifier
|
|
||||||
import org.springframework.cache.annotation.Cacheable
|
import org.springframework.cache.annotation.Cacheable
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
@ -50,7 +48,6 @@ class APNoteServiceImpl(
|
||||||
private val postRepository: PostRepository,
|
private val postRepository: PostRepository,
|
||||||
private val apUserService: APUserService,
|
private val apUserService: APUserService,
|
||||||
private val postQueryService: PostQueryService,
|
private val postQueryService: PostQueryService,
|
||||||
@Qualifier("activitypub") private val objectMapper: ObjectMapper,
|
|
||||||
private val postService: PostService,
|
private val postService: PostService,
|
||||||
private val apResourceResolveService: APResourceResolveService,
|
private val apResourceResolveService: APResourceResolveService,
|
||||||
private val postBuilder: Post.PostBuilder,
|
private val postBuilder: Post.PostBuilder,
|
||||||
|
@ -133,7 +130,8 @@ class APNoteServiceImpl(
|
||||||
RemoteMedia(
|
RemoteMedia(
|
||||||
(it.name ?: it.url)!!,
|
(it.name ?: it.url)!!,
|
||||||
it.url!!,
|
it.url!!,
|
||||||
it.mediaType ?: "application/octet-stream"
|
it.mediaType ?: "application/octet-stream",
|
||||||
|
description = it.name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ class HttpClientConfig {
|
||||||
fun httpClient(): HttpClient = HttpClient(CIO).config {
|
fun httpClient(): HttpClient = HttpClient(CIO).config {
|
||||||
install(Logging) {
|
install(Logging) {
|
||||||
logger = Logger.DEFAULT
|
logger = Logger.DEFAULT
|
||||||
level = LogLevel.INFO
|
level = LogLevel.ALL
|
||||||
}
|
}
|
||||||
install(HttpCache) {
|
install(HttpCache) {
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@ import java.security.interfaces.RSAPrivateKey
|
||||||
import java.security.interfaces.RSAPublicKey
|
import java.security.interfaces.RSAPublicKey
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@EnableWebSecurity(debug = true)
|
@EnableWebSecurity(debug = false)
|
||||||
@Configuration
|
@Configuration
|
||||||
@Suppress("FunctionMaxLength", "TooManyFunctions")
|
@Suppress("FunctionMaxLength", "TooManyFunctions")
|
||||||
class SecurityConfig {
|
class SecurityConfig {
|
||||||
|
@ -73,7 +73,12 @@ class SecurityConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Order(1)
|
@Order(1)
|
||||||
fun httpSignatureFilterChain(http: HttpSecurity, httpSignatureFilter: HttpSignatureFilter): SecurityFilterChain {
|
fun httpSignatureFilterChain(
|
||||||
|
http: HttpSecurity,
|
||||||
|
httpSignatureFilter: HttpSignatureFilter,
|
||||||
|
introspector: HandlerMappingIntrospector
|
||||||
|
): SecurityFilterChain {
|
||||||
|
val builder = MvcRequestMatcher.Builder(introspector)
|
||||||
http
|
http
|
||||||
.securityMatcher("/inbox", "/outbox", "/users/*/inbox", "/users/*/outbox", "/users/*/posts/*")
|
.securityMatcher("/inbox", "/outbox", "/users/*/inbox", "/users/*/outbox", "/users/*/posts/*")
|
||||||
.addFilter(httpSignatureFilter)
|
.addFilter(httpSignatureFilter)
|
||||||
|
@ -82,7 +87,12 @@ class SecurityConfig {
|
||||||
HttpSignatureFilter::class.java
|
HttpSignatureFilter::class.java
|
||||||
)
|
)
|
||||||
.authorizeHttpRequests {
|
.authorizeHttpRequests {
|
||||||
it.requestMatchers("/inbox", "/outbox", "/users/*/inbox", "/users/*/outbox").authenticated()
|
it.requestMatchers(
|
||||||
|
builder.pattern("/inbox"),
|
||||||
|
builder.pattern("/outbox"),
|
||||||
|
builder.pattern("/users/*/inbox"),
|
||||||
|
builder.pattern("/users/*/outbox")
|
||||||
|
).authenticated()
|
||||||
it.anyRequest().permitAll()
|
it.anyRequest().permitAll()
|
||||||
}
|
}
|
||||||
.csrf {
|
.csrf {
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package dev.usbharu.hideout.core.domain.exception.media
|
||||||
|
|
||||||
|
class MediaProcessException : 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
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package dev.usbharu.hideout.core.domain.model.media
|
package dev.usbharu.hideout.core.domain.model.media
|
||||||
|
|
||||||
import dev.usbharu.hideout.core.service.media.FileType
|
import dev.usbharu.hideout.core.service.media.FileType
|
||||||
|
import dev.usbharu.hideout.core.service.media.MimeType
|
||||||
|
|
||||||
data class Media(
|
data class Media(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
|
@ -9,5 +10,7 @@ data class Media(
|
||||||
val remoteUrl: String?,
|
val remoteUrl: String?,
|
||||||
val thumbnailUrl: String?,
|
val thumbnailUrl: String?,
|
||||||
val type: FileType,
|
val type: FileType,
|
||||||
val blurHash: String?
|
val mimeType: MimeType,
|
||||||
|
val blurHash: String?,
|
||||||
|
val description: String? = null
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,7 +3,9 @@ package dev.usbharu.hideout.core.infrastructure.exposedrepository
|
||||||
import dev.usbharu.hideout.application.service.id.IdGenerateService
|
import dev.usbharu.hideout.application.service.id.IdGenerateService
|
||||||
import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException
|
import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException
|
||||||
import dev.usbharu.hideout.core.domain.model.media.MediaRepository
|
import dev.usbharu.hideout.core.domain.model.media.MediaRepository
|
||||||
|
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Media.mimeType
|
||||||
import dev.usbharu.hideout.core.service.media.FileType
|
import dev.usbharu.hideout.core.service.media.FileType
|
||||||
|
import dev.usbharu.hideout.core.service.media.MimeType
|
||||||
import dev.usbharu.hideout.util.singleOr
|
import dev.usbharu.hideout.util.singleOr
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
|
@ -26,6 +28,8 @@ class MediaRepositoryImpl(private val idGenerateService: IdGenerateService) : Me
|
||||||
it[thumbnailUrl] = media.thumbnailUrl
|
it[thumbnailUrl] = media.thumbnailUrl
|
||||||
it[type] = media.type.ordinal
|
it[type] = media.type.ordinal
|
||||||
it[blurhash] = media.blurHash
|
it[blurhash] = media.blurHash
|
||||||
|
it[mimeType] = media.mimeType.type + "/" + media.mimeType.subtype
|
||||||
|
it[description] = media.description
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Media.insert {
|
Media.insert {
|
||||||
|
@ -36,6 +40,8 @@ class MediaRepositoryImpl(private val idGenerateService: IdGenerateService) : Me
|
||||||
it[thumbnailUrl] = media.thumbnailUrl
|
it[thumbnailUrl] = media.thumbnailUrl
|
||||||
it[type] = media.type.ordinal
|
it[type] = media.type.ordinal
|
||||||
it[blurhash] = media.blurHash
|
it[blurhash] = media.blurHash
|
||||||
|
it[mimeType] = media.mimeType.type + "/" + media.mimeType.subtype
|
||||||
|
it[description] = media.description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return media
|
return media
|
||||||
|
@ -59,18 +65,24 @@ class MediaRepositoryImpl(private val idGenerateService: IdGenerateService) : Me
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ResultRow.toMedia(): EntityMedia {
|
fun ResultRow.toMedia(): EntityMedia {
|
||||||
|
val fileType = FileType.values().first { it.ordinal == this[Media.type] }
|
||||||
|
val mimeType = this[Media.mimeType]
|
||||||
return EntityMedia(
|
return EntityMedia(
|
||||||
id = this[Media.id],
|
id = this[Media.id],
|
||||||
name = this[Media.name],
|
name = this[Media.name],
|
||||||
url = this[Media.url],
|
url = this[Media.url],
|
||||||
remoteUrl = this[Media.remoteUrl],
|
remoteUrl = this[Media.remoteUrl],
|
||||||
thumbnailUrl = this[Media.thumbnailUrl],
|
thumbnailUrl = this[Media.thumbnailUrl],
|
||||||
type = FileType.values().first { it.ordinal == this[Media.type] },
|
type = fileType,
|
||||||
blurHash = this[Media.blurhash],
|
blurHash = this[Media.blurhash],
|
||||||
|
mimeType = MimeType(mimeType.substringBefore("/"), mimeType.substringAfter("/"), fileType),
|
||||||
|
description = this[Media.description]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ResultRow.toMediaOrNull(): EntityMedia? {
|
fun ResultRow.toMediaOrNull(): EntityMedia? {
|
||||||
|
val fileType = FileType.values().first { it.ordinal == (this.getOrNull(Media.type) ?: return null) }
|
||||||
|
val mimeType = this.getOrNull(Media.mimeType) ?: return null
|
||||||
return EntityMedia(
|
return EntityMedia(
|
||||||
id = this.getOrNull(Media.id) ?: return null,
|
id = this.getOrNull(Media.id) ?: return null,
|
||||||
name = this.getOrNull(Media.name) ?: return null,
|
name = this.getOrNull(Media.name) ?: return null,
|
||||||
|
@ -79,6 +91,8 @@ fun ResultRow.toMediaOrNull(): EntityMedia? {
|
||||||
thumbnailUrl = this[Media.thumbnailUrl],
|
thumbnailUrl = this[Media.thumbnailUrl],
|
||||||
type = FileType.values().first { it.ordinal == this.getOrNull(Media.type) },
|
type = FileType.values().first { it.ordinal == this.getOrNull(Media.type) },
|
||||||
blurHash = this[Media.blurhash],
|
blurHash = this[Media.blurhash],
|
||||||
|
mimeType = MimeType(mimeType.substringBefore("/"), mimeType.substringAfter("/"), fileType),
|
||||||
|
description = this[Media.description]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,5 +104,7 @@ object Media : Table("media") {
|
||||||
val thumbnailUrl = varchar("thumbnail_url", 255).nullable()
|
val thumbnailUrl = varchar("thumbnail_url", 255).nullable()
|
||||||
val type = integer("type")
|
val type = integer("type")
|
||||||
val blurhash = varchar("blurhash", 255).nullable()
|
val blurhash = varchar("blurhash", 255).nullable()
|
||||||
|
val mimeType = varchar("mime_type", 255)
|
||||||
|
val description = varchar("description", 4000).nullable()
|
||||||
override val primaryKey = PrimaryKey(id)
|
override val primaryKey = PrimaryKey(id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,6 @@ class PostRepositoryImpl(
|
||||||
.let(postQueryMapper::map)
|
.let(postQueryMapper::map)
|
||||||
.singleOr { FailedToGetResourcesException("id: $id was not found.", it) }
|
.singleOr { FailedToGetResourcesException("id: $id was not found.", it) }
|
||||||
|
|
||||||
|
|
||||||
override suspend fun delete(id: Long) {
|
override suspend fun delete(id: Long) {
|
||||||
Posts.deleteWhere { Posts.id eq id }
|
Posts.deleteWhere { Posts.id eq id }
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
package dev.usbharu.hideout.core.service.media
|
||||||
|
|
||||||
|
import org.apache.tika.Tika
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class ApatcheTikaFileTypeDeterminationService : FileTypeDeterminationService {
|
||||||
|
override fun fileType(
|
||||||
|
byteArray: ByteArray,
|
||||||
|
filename: String,
|
||||||
|
contentType: String?
|
||||||
|
): MimeType {
|
||||||
|
logger.info("START Detect file type name: {}", filename)
|
||||||
|
|
||||||
|
val tika = Tika()
|
||||||
|
|
||||||
|
val detect = try {
|
||||||
|
tika.detect(byteArray, filename)
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
logger.warn("FAILED Detect file type", e)
|
||||||
|
"application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
val type = detect.substringBefore("/")
|
||||||
|
val fileType = when (type) {
|
||||||
|
"image" -> {
|
||||||
|
FileType.Image
|
||||||
|
}
|
||||||
|
|
||||||
|
"video" -> {
|
||||||
|
FileType.Video
|
||||||
|
}
|
||||||
|
|
||||||
|
"audio" -> {
|
||||||
|
FileType.Audio
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
FileType.Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val mimeType = MimeType(type, detect.substringAfter("/"), fileType)
|
||||||
|
|
||||||
|
logger.info("SUCCESS Detect file type name: {},MimeType: {}", filename, mimeType)
|
||||||
|
return mimeType
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fileType(path: Path, filename: String): MimeType {
|
||||||
|
logger.info("START Detect file type name: {}", filename)
|
||||||
|
|
||||||
|
val tika = Tika()
|
||||||
|
|
||||||
|
val detect = try {
|
||||||
|
tika.detect(path)
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
logger.warn("FAILED Detect file type", e)
|
||||||
|
"application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
val type = detect.substringBefore("/")
|
||||||
|
val fileType = when (type) {
|
||||||
|
"image" -> {
|
||||||
|
FileType.Image
|
||||||
|
}
|
||||||
|
|
||||||
|
"video" -> {
|
||||||
|
FileType.Video
|
||||||
|
}
|
||||||
|
|
||||||
|
"audio" -> {
|
||||||
|
FileType.Audio
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
FileType.Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val mimeType = MimeType(type, detect.substringAfter("/"), fileType)
|
||||||
|
|
||||||
|
logger.info("SUCCESS Detect file type name: {},MimeType: {}", filename, mimeType)
|
||||||
|
return mimeType
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(ApatcheTikaFileTypeDeterminationService::class.java)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,8 @@
|
||||||
package dev.usbharu.hideout.core.service.media
|
package dev.usbharu.hideout.core.service.media
|
||||||
|
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
interface FileTypeDeterminationService {
|
interface FileTypeDeterminationService {
|
||||||
fun fileType(byteArray: ByteArray, filename: String, contentType: String?): FileType
|
fun fileType(byteArray: ByteArray, filename: String, contentType: String?): MimeType
|
||||||
|
fun fileType(path: Path, filename: String): MimeType
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
package dev.usbharu.hideout.core.service.media
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,5 +2,6 @@ package dev.usbharu.hideout.core.service.media
|
||||||
|
|
||||||
interface MediaDataStore {
|
interface MediaDataStore {
|
||||||
suspend fun save(dataMediaSave: MediaSave): SavedMedia
|
suspend fun save(dataMediaSave: MediaSave): SavedMedia
|
||||||
|
suspend fun save(dataSaveRequest: MediaSaveRequest): SavedMedia
|
||||||
suspend fun delete(id: String)
|
suspend fun delete(id: String)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package dev.usbharu.hideout.core.service.media
|
||||||
|
|
||||||
|
interface MediaFileRenameService {
|
||||||
|
/**
|
||||||
|
* メディアをリネームします
|
||||||
|
*
|
||||||
|
* @param uploadName アップロードされた時点でのファイル名
|
||||||
|
* @param uploadMimeType アップロードされた時点でのMimeType
|
||||||
|
* @param processedName 処理後のファイル名
|
||||||
|
* @param processedMimeType 処理後のMimeType
|
||||||
|
* @return リネーム後のファイル名
|
||||||
|
*/
|
||||||
|
fun rename(uploadName: String, uploadMimeType: MimeType, processedName: String, processedMimeType: MimeType): String
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package dev.usbharu.hideout.core.service.media
|
||||||
|
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
data class MediaSaveRequest(
|
||||||
|
val name: String,
|
||||||
|
val preffix: String,
|
||||||
|
val filePath: Path,
|
||||||
|
val thumbnailPath: Path?
|
||||||
|
)
|
|
@ -1,149 +1,136 @@
|
||||||
package dev.usbharu.hideout.core.service.media
|
package dev.usbharu.hideout.core.service.media
|
||||||
|
|
||||||
import dev.usbharu.hideout.core.domain.exception.media.MediaFileSizeIsZeroException
|
|
||||||
import dev.usbharu.hideout.core.domain.exception.media.MediaSaveException
|
import dev.usbharu.hideout.core.domain.exception.media.MediaSaveException
|
||||||
import dev.usbharu.hideout.core.domain.exception.media.UnsupportedMediaException
|
import dev.usbharu.hideout.core.domain.exception.media.UnsupportedMediaException
|
||||||
import dev.usbharu.hideout.core.domain.model.media.Media
|
import dev.usbharu.hideout.core.domain.model.media.Media
|
||||||
import dev.usbharu.hideout.core.domain.model.media.MediaRepository
|
import dev.usbharu.hideout.core.domain.model.media.MediaRepository
|
||||||
import dev.usbharu.hideout.core.service.media.converter.MediaProcessService
|
import dev.usbharu.hideout.core.service.media.converter.MediaProcessService
|
||||||
import dev.usbharu.hideout.mastodon.interfaces.api.media.MediaRequest
|
import dev.usbharu.hideout.mastodon.interfaces.api.media.MediaRequest
|
||||||
import io.ktor.client.*
|
import dev.usbharu.hideout.util.withDelete
|
||||||
import io.ktor.client.request.*
|
|
||||||
import io.ktor.client.statement.*
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.util.*
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.util.*
|
import java.nio.file.Files
|
||||||
import javax.imageio.ImageIO
|
import javax.imageio.ImageIO
|
||||||
import dev.usbharu.hideout.core.domain.model.media.Media as EntityMedia
|
import dev.usbharu.hideout.core.domain.model.media.Media as EntityMedia
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Suppress("TooGenericExceptionCaught")
|
@Suppress("TooGenericExceptionCaught")
|
||||||
class MediaServiceImpl(
|
open class MediaServiceImpl(
|
||||||
private val mediaDataStore: MediaDataStore,
|
private val mediaDataStore: MediaDataStore,
|
||||||
private val fileTypeDeterminationService: FileTypeDeterminationService,
|
private val fileTypeDeterminationService: FileTypeDeterminationService,
|
||||||
private val mediaBlurhashService: MediaBlurhashService,
|
private val mediaBlurhashService: MediaBlurhashService,
|
||||||
private val mediaRepository: MediaRepository,
|
private val mediaRepository: MediaRepository,
|
||||||
private val mediaProcessService: MediaProcessService,
|
private val mediaProcessServices: List<MediaProcessService>,
|
||||||
private val httpClient: HttpClient
|
private val remoteMediaDownloadService: RemoteMediaDownloadService,
|
||||||
|
private val renameService: MediaFileRenameService
|
||||||
) : MediaService {
|
) : MediaService {
|
||||||
|
@Suppress("LongMethod")
|
||||||
override suspend fun uploadLocalMedia(mediaRequest: MediaRequest): EntityMedia {
|
override suspend fun uploadLocalMedia(mediaRequest: MediaRequest): EntityMedia {
|
||||||
|
val fileName = mediaRequest.file.name
|
||||||
logger.info(
|
logger.info(
|
||||||
"Media upload. filename:${mediaRequest.file.name} size:${mediaRequest.file.size} " +
|
"Media upload. filename:$fileName " +
|
||||||
"contentType:${mediaRequest.file.contentType}"
|
"contentType:${mediaRequest.file.contentType}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (mediaRequest.file.size == 0L) {
|
val tempFile = Files.createTempFile("hideout-tmp-file", ".tmp")
|
||||||
throw MediaFileSizeIsZeroException("Media file size is zero.")
|
|
||||||
}
|
|
||||||
|
|
||||||
val fileType = fileTypeDeterminationService.fileType(
|
tempFile.withDelete().use {
|
||||||
mediaRequest.file.bytes,
|
Files.newOutputStream(tempFile).use { outputStream ->
|
||||||
mediaRequest.file.name,
|
mediaRequest.file.inputStream.use {
|
||||||
mediaRequest.file.contentType
|
it.transferTo(outputStream)
|
||||||
)
|
|
||||||
if (fileType != FileType.Image) {
|
|
||||||
throw UnsupportedMediaException("FileType: $fileType is not supported.")
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
val mimeType = fileTypeDeterminationService.fileType(tempFile, fileName)
|
||||||
|
|
||||||
val process = mediaProcessService.process(
|
val process = findMediaProcessor(mimeType).process(
|
||||||
fileType,
|
mimeType,
|
||||||
mediaRequest.file.contentType.orEmpty(),
|
fileName,
|
||||||
mediaRequest.file.name,
|
tempFile,
|
||||||
mediaRequest.file.bytes,
|
null
|
||||||
mediaRequest.thumbnail?.bytes
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val dataMediaSave = MediaSave(
|
val dataMediaSave = MediaSaveRequest(
|
||||||
"${UUID.randomUUID()}.${process.file.extension}",
|
renameService.rename(
|
||||||
|
mediaRequest.file.name,
|
||||||
|
mimeType,
|
||||||
|
process.filePath.fileName.toString(),
|
||||||
|
process.fileMimeType
|
||||||
|
),
|
||||||
"",
|
"",
|
||||||
process.file.byteArray,
|
process.filePath,
|
||||||
process.thumbnail?.byteArray
|
process.thumbnailPath
|
||||||
)
|
)
|
||||||
|
dataMediaSave.filePath.withDelete().use {
|
||||||
|
dataMediaSave.thumbnailPath.withDelete().use {
|
||||||
val save = try {
|
val save = try {
|
||||||
mediaDataStore.save(dataMediaSave)
|
mediaDataStore.save(dataMediaSave)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.warn("Failed save media", e)
|
logger.warn("Failed to save the media", e)
|
||||||
throw MediaSaveException("Failed save media.", e)
|
throw MediaSaveException("Failed to save the media.", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (save.success.not()) {
|
if (save.success.not()) {
|
||||||
save as FaildSavedMedia
|
save as FaildSavedMedia
|
||||||
logger.warn("Failed save media. reason: ${save.reason}")
|
logger.warn("Failed to save the media. reason: ${save.reason}")
|
||||||
logger.warn(save.description, save.trace)
|
logger.warn(save.description, save.trace)
|
||||||
throw MediaSaveException("Failed save media.")
|
throw MediaSaveException("Failed to save the media.")
|
||||||
}
|
}
|
||||||
save as SuccessSavedMedia
|
save as SuccessSavedMedia
|
||||||
|
val blurHash = generateBlurhash(process)
|
||||||
val blurHash = withContext(Dispatchers.IO) {
|
|
||||||
mediaBlurhashService.generateBlurhash(ImageIO.read(mediaRequest.file.bytes.inputStream()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return mediaRepository.save(
|
return mediaRepository.save(
|
||||||
EntityMedia(
|
EntityMedia(
|
||||||
id = mediaRepository.generateId(),
|
id = mediaRepository.generateId(),
|
||||||
name = mediaRequest.file.name,
|
name = fileName,
|
||||||
url = save.url,
|
url = save.url,
|
||||||
remoteUrl = null,
|
remoteUrl = null,
|
||||||
thumbnailUrl = save.thumbnailUrl,
|
thumbnailUrl = save.thumbnailUrl,
|
||||||
type = fileType,
|
type = process.fileMimeType.fileType,
|
||||||
blurHash = blurHash
|
mimeType = process.fileMimeType,
|
||||||
|
blurHash = blurHash,
|
||||||
|
description = mediaRequest.description
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: 仮の処理として保存したように動かす
|
// TODO: 仮の処理として保存したように動かす
|
||||||
override suspend fun uploadRemoteMedia(remoteMedia: RemoteMedia): Media {
|
override suspend fun uploadRemoteMedia(remoteMedia: RemoteMedia): Media {
|
||||||
logger.info("MEDIA Remote media. filename:${remoteMedia.name} url:${remoteMedia.url}")
|
logger.info("MEDIA Remote media. filename:${remoteMedia.name} url:${remoteMedia.url}")
|
||||||
|
|
||||||
val httpResponse = httpClient.get(remoteMedia.url)
|
remoteMediaDownloadService.download(remoteMedia.url).withDelete().use {
|
||||||
val bytes = httpResponse.bodyAsChannel().toByteArray()
|
val mimeType = fileTypeDeterminationService.fileType(it.path, remoteMedia.name)
|
||||||
|
|
||||||
val contentType = httpResponse.contentType()?.toString()
|
val process = findMediaProcessor(mimeType).process(mimeType, remoteMedia.name, it.path, null)
|
||||||
val fileType =
|
|
||||||
fileTypeDeterminationService.fileType(bytes, remoteMedia.name, contentType)
|
|
||||||
|
|
||||||
if (fileType != FileType.Image) {
|
val mediaSaveRequest = MediaSaveRequest(
|
||||||
throw UnsupportedMediaException("FileType: $fileType is not supported.")
|
renameService.rename(
|
||||||
}
|
remoteMedia.name,
|
||||||
|
mimeType,
|
||||||
val processedMedia = mediaProcessService.process(
|
process.filePath.fileName.toString(),
|
||||||
fileType = fileType,
|
process.fileMimeType
|
||||||
contentType = contentType.orEmpty(),
|
),
|
||||||
fileName = remoteMedia.name,
|
|
||||||
file = bytes,
|
|
||||||
thumbnail = null
|
|
||||||
)
|
|
||||||
|
|
||||||
val mediaSave = MediaSave(
|
|
||||||
"${UUID.randomUUID()}.${processedMedia.file.extension}",
|
|
||||||
"",
|
"",
|
||||||
processedMedia.file.byteArray,
|
process.filePath,
|
||||||
processedMedia.thumbnail?.byteArray
|
process.thumbnailPath
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mediaSaveRequest.filePath.withDelete().use {
|
||||||
|
mediaSaveRequest.filePath.withDelete().use {
|
||||||
val save = try {
|
val save = try {
|
||||||
mediaDataStore.save(mediaSave)
|
mediaDataStore.save(mediaSaveRequest)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.warn("Failed save media", e)
|
logger.warn("Failed to save the media", e)
|
||||||
throw MediaSaveException("Failed save media.", e)
|
throw MediaSaveException("Failed to save the media.", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (save.success.not()) {
|
if (save is FaildSavedMedia) {
|
||||||
save as FaildSavedMedia
|
logger.warn("Failed to save the media. reason: ${save.reason}")
|
||||||
logger.warn("Failed save media. reason: ${save.reason}")
|
|
||||||
logger.warn(save.description, save.trace)
|
logger.warn(save.description, save.trace)
|
||||||
throw MediaSaveException("Failed save media.")
|
throw MediaSaveException("Failed to save the media.")
|
||||||
}
|
}
|
||||||
save as SuccessSavedMedia
|
save as SuccessSavedMedia
|
||||||
|
val blurhash = generateBlurhash(process)
|
||||||
val blurhash = withContext(Dispatchers.IO) {
|
|
||||||
mediaBlurhashService.generateBlurhash(ImageIO.read(bytes.inputStream()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return mediaRepository.save(
|
return mediaRepository.save(
|
||||||
EntityMedia(
|
EntityMedia(
|
||||||
id = mediaRepository.generateId(),
|
id = mediaRepository.generateId(),
|
||||||
|
@ -151,11 +138,56 @@ class MediaServiceImpl(
|
||||||
url = save.url,
|
url = save.url,
|
||||||
remoteUrl = remoteMedia.url,
|
remoteUrl = remoteMedia.url,
|
||||||
thumbnailUrl = save.thumbnailUrl,
|
thumbnailUrl = save.thumbnailUrl,
|
||||||
type = fileType,
|
type = process.fileMimeType.fileType,
|
||||||
|
mimeType = process.fileMimeType,
|
||||||
blurHash = blurhash
|
blurHash = blurhash
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun findMediaProcessor(mimeType: MimeType): MediaProcessService {
|
||||||
|
try {
|
||||||
|
return mediaProcessServices.first {
|
||||||
|
try {
|
||||||
|
it.isSupport(mimeType)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: NoSuchElementException) {
|
||||||
|
throw UnsupportedMediaException("MediaType: $mimeType isn't supported.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun generateBlurhash(process: ProcessedMediaPath): String {
|
||||||
|
val path = if (process.thumbnailPath != null && process.thumbnailMimeType != null) {
|
||||||
|
process.thumbnailPath
|
||||||
|
} else {
|
||||||
|
process.filePath
|
||||||
|
}
|
||||||
|
val mimeType = if (process.thumbnailPath != null && process.thumbnailMimeType != null) {
|
||||||
|
process.thumbnailMimeType
|
||||||
|
} else {
|
||||||
|
process.fileMimeType
|
||||||
|
}
|
||||||
|
|
||||||
|
val imageReadersByMIMEType = ImageIO.getImageReadersByMIMEType(mimeType.type + "/" + mimeType.subtype)
|
||||||
|
for (imageReader in imageReadersByMIMEType) {
|
||||||
|
try {
|
||||||
|
val bufferedImage = ImageIO.createImageInputStream(path.toFile()).use {
|
||||||
|
imageReader.input = it
|
||||||
|
imageReader.read(0)
|
||||||
|
}
|
||||||
|
return mediaBlurhashService.generateBlurhash(bufferedImage)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.warn("Failed to read thumbnail", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val logger = LoggerFactory.getLogger(MediaServiceImpl::class.java)
|
private val logger = LoggerFactory.getLogger(MediaServiceImpl::class.java)
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
package dev.usbharu.hideout.core.service.media
|
||||||
|
|
||||||
|
data class MimeType(val type: String, val subtype: String, val fileType: FileType)
|
|
@ -0,0 +1,10 @@
|
||||||
|
package dev.usbharu.hideout.core.service.media
|
||||||
|
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
data class ProcessedMediaPath(
|
||||||
|
val filePath: Path,
|
||||||
|
val thumbnailPath: Path?,
|
||||||
|
val fileMimeType: MimeType,
|
||||||
|
val thumbnailMimeType: MimeType?
|
||||||
|
)
|
|
@ -3,5 +3,6 @@ package dev.usbharu.hideout.core.service.media
|
||||||
data class RemoteMedia(
|
data class RemoteMedia(
|
||||||
val name: String,
|
val name: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
val mediaType: String
|
val mediaType: String,
|
||||||
|
val description: String? = null
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package dev.usbharu.hideout.core.service.media
|
||||||
|
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
interface RemoteMediaDownloadService {
|
||||||
|
suspend fun download(url: String): Path
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package dev.usbharu.hideout.core.service.media
|
||||||
|
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.utils.io.jvm.javaio.*
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import kotlin.io.path.outputStream
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class RemoteMediaDownloadServiceImpl(private val httpClient: HttpClient) : RemoteMediaDownloadService {
|
||||||
|
override suspend fun download(url: String): Path {
|
||||||
|
logger.info("START Download remote file. url: {}", url)
|
||||||
|
val httpResponse = httpClient.get(url)
|
||||||
|
httpResponse.contentLength()
|
||||||
|
val createTempFile = Files.createTempFile("hideout-remote-download", ".tmp")
|
||||||
|
|
||||||
|
logger.debug("Save to {} url: {} ", createTempFile, url)
|
||||||
|
|
||||||
|
httpResponse.bodyAsChannel().toInputStream().use { inputStream ->
|
||||||
|
createTempFile.outputStream().use {
|
||||||
|
inputStream.transferTo(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("SUCCESS Download remote file. url: {}", url)
|
||||||
|
return createTempFile
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(RemoteMediaDownloadServiceImpl::class.java)
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import software.amazon.awssdk.core.sync.RequestBody
|
import software.amazon.awssdk.core.sync.RequestBody
|
||||||
import software.amazon.awssdk.services.s3.S3Client
|
import software.amazon.awssdk.services.s3.S3Client
|
||||||
|
@ -54,6 +55,58 @@ class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun save(dataSaveRequest: MediaSaveRequest): SavedMedia {
|
||||||
|
logger.info("MEDIA upload. {}", dataSaveRequest.name)
|
||||||
|
|
||||||
|
val fileUploadRequest = PutObjectRequest.builder()
|
||||||
|
.bucket(storageConfig.bucket)
|
||||||
|
.key(dataSaveRequest.name)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
logger.info("MEDIA upload. bucket: {} key: {}", storageConfig.bucket, dataSaveRequest.name)
|
||||||
|
|
||||||
|
val thumbnailKey = "thumbnail-${dataSaveRequest.name}"
|
||||||
|
val thumbnailUploadRequest = PutObjectRequest.builder()
|
||||||
|
.bucket(storageConfig.bucket)
|
||||||
|
.key(thumbnailKey)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
logger.info("MEDIA upload. bucket: {} key: {}", storageConfig.bucket, thumbnailKey)
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
awaitAll(
|
||||||
|
async {
|
||||||
|
if (dataSaveRequest.thumbnailPath != null) {
|
||||||
|
s3Client.putObject(
|
||||||
|
thumbnailUploadRequest,
|
||||||
|
RequestBody.fromFile(dataSaveRequest.thumbnailPath)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
s3Client.putObject(fileUploadRequest, RequestBody.fromFile(dataSaveRequest.filePath))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val successSavedMedia = SuccessSavedMedia(
|
||||||
|
name = dataSaveRequest.name,
|
||||||
|
url = "${storageConfig.publicUrl}/${storageConfig.bucket}/${dataSaveRequest.name}",
|
||||||
|
thumbnailUrl = "${storageConfig.publicUrl}/${storageConfig.bucket}/$thumbnailKey"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("SUCCESS Media upload. {}", dataSaveRequest.name)
|
||||||
|
logger.debug(
|
||||||
|
"name: {} url: {} thumbnail url: {}",
|
||||||
|
successSavedMedia.name,
|
||||||
|
successSavedMedia.url,
|
||||||
|
successSavedMedia.thumbnailUrl
|
||||||
|
)
|
||||||
|
|
||||||
|
return successSavedMedia
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun delete(id: String) {
|
override suspend fun delete(id: String) {
|
||||||
val fileDeleteRequest = DeleteObjectRequest.builder().bucket(storageConfig.bucket).key(id).build()
|
val fileDeleteRequest = DeleteObjectRequest.builder().bucket(storageConfig.bucket).key(id).build()
|
||||||
val thumbnailDeleteRequest =
|
val thumbnailDeleteRequest =
|
||||||
|
@ -61,4 +114,8 @@ class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig
|
||||||
s3Client.deleteObject(fileDeleteRequest)
|
s3Client.deleteObject(fileDeleteRequest)
|
||||||
s3Client.deleteObject(thumbnailDeleteRequest)
|
s3Client.deleteObject(thumbnailDeleteRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(S3MediaDataStore::class.java)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package dev.usbharu.hideout.core.service.media
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Qualifier("uuid")
|
||||||
|
@Service
|
||||||
|
class UUIDMediaFileRenameService : MediaFileRenameService {
|
||||||
|
override fun rename(
|
||||||
|
uploadName: String,
|
||||||
|
uploadMimeType: MimeType,
|
||||||
|
processedName: String,
|
||||||
|
processedMimeType: MimeType
|
||||||
|
): String = "${UUID.randomUUID()}.${uploadMimeType.subtype}.${processedMimeType.subtype}"
|
||||||
|
}
|
|
@ -1,9 +1,14 @@
|
||||||
package dev.usbharu.hideout.core.service.media.converter
|
package dev.usbharu.hideout.core.service.media.converter
|
||||||
|
|
||||||
import dev.usbharu.hideout.core.service.media.FileType
|
import dev.usbharu.hideout.core.service.media.FileType
|
||||||
|
import dev.usbharu.hideout.core.service.media.MimeType
|
||||||
import dev.usbharu.hideout.core.service.media.ProcessedMedia
|
import dev.usbharu.hideout.core.service.media.ProcessedMedia
|
||||||
|
import dev.usbharu.hideout.core.service.media.ProcessedMediaPath
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
interface MediaProcessService {
|
interface MediaProcessService {
|
||||||
|
fun isSupport(mimeType: MimeType): Boolean
|
||||||
|
|
||||||
suspend fun process(
|
suspend fun process(
|
||||||
fileType: FileType,
|
fileType: FileType,
|
||||||
contentType: String,
|
contentType: String,
|
||||||
|
@ -11,4 +16,11 @@ interface MediaProcessService {
|
||||||
file: ByteArray,
|
file: ByteArray,
|
||||||
thumbnail: ByteArray?
|
thumbnail: ByteArray?
|
||||||
): ProcessedMedia
|
): ProcessedMedia
|
||||||
|
|
||||||
|
suspend fun process(
|
||||||
|
mimeType: MimeType,
|
||||||
|
fileName: String,
|
||||||
|
filePath: Path,
|
||||||
|
thumbnails: Path?
|
||||||
|
): ProcessedMediaPath
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
package dev.usbharu.hideout.core.service.media.converter
|
package dev.usbharu.hideout.core.service.media.converter
|
||||||
|
|
||||||
import dev.usbharu.hideout.core.domain.exception.media.MediaConvertException
|
import dev.usbharu.hideout.core.domain.exception.media.MediaConvertException
|
||||||
import dev.usbharu.hideout.core.service.media.FileType
|
import dev.usbharu.hideout.core.service.media.*
|
||||||
import dev.usbharu.hideout.core.service.media.ProcessedMedia
|
|
||||||
import dev.usbharu.hideout.core.service.media.ThumbnailGenerateService
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Suppress("TooGenericExceptionCaught")
|
@Suppress("TooGenericExceptionCaught")
|
||||||
|
@ -13,6 +12,8 @@ class MediaProcessServiceImpl(
|
||||||
private val mediaConverterRoot: MediaConverterRoot,
|
private val mediaConverterRoot: MediaConverterRoot,
|
||||||
private val thumbnailGenerateService: ThumbnailGenerateService
|
private val thumbnailGenerateService: ThumbnailGenerateService
|
||||||
) : MediaProcessService {
|
) : MediaProcessService {
|
||||||
|
override fun isSupport(mimeType: MimeType): Boolean = false
|
||||||
|
|
||||||
override suspend fun process(
|
override suspend fun process(
|
||||||
fileType: FileType,
|
fileType: FileType,
|
||||||
contentType: String,
|
contentType: String,
|
||||||
|
@ -42,6 +43,15 @@ class MediaProcessServiceImpl(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun process(
|
||||||
|
mimeType: MimeType,
|
||||||
|
fileName: String,
|
||||||
|
filePath: Path,
|
||||||
|
thumbnails: Path?
|
||||||
|
): ProcessedMediaPath {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val logger = LoggerFactory.getLogger(MediaProcessServiceImpl::class.java)
|
private val logger = LoggerFactory.getLogger(MediaProcessServiceImpl::class.java)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
package dev.usbharu.hideout.core.service.media.converter.image
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.domain.exception.media.MediaProcessException
|
||||||
|
import dev.usbharu.hideout.core.service.media.FileType
|
||||||
|
import dev.usbharu.hideout.core.service.media.MimeType
|
||||||
|
import dev.usbharu.hideout.core.service.media.ProcessedMedia
|
||||||
|
import dev.usbharu.hideout.core.service.media.ProcessedMediaPath
|
||||||
|
import dev.usbharu.hideout.core.service.media.converter.MediaProcessService
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.slf4j.MDCContext
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import net.coobird.thumbnailator.Thumbnails
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.util.*
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
import kotlin.io.path.inputStream
|
||||||
|
import kotlin.io.path.outputStream
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Qualifier("image")
|
||||||
|
class ImageMediaProcessService(private val imageMediaProcessorConfiguration: ImageMediaProcessorConfiguration?) :
|
||||||
|
MediaProcessService {
|
||||||
|
|
||||||
|
private val convertType = imageMediaProcessorConfiguration?.convert ?: "jpeg"
|
||||||
|
|
||||||
|
private val supportedTypes = imageMediaProcessorConfiguration?.supportedType ?: listOf("webp", "jpeg", "png")
|
||||||
|
|
||||||
|
private val genThumbnail = imageMediaProcessorConfiguration?.thubnail?.generate ?: true
|
||||||
|
|
||||||
|
private val width = imageMediaProcessorConfiguration?.thubnail?.width ?: 1000
|
||||||
|
private val height = imageMediaProcessorConfiguration?.thubnail?.height ?: 1000
|
||||||
|
|
||||||
|
override fun isSupport(mimeType: MimeType): Boolean {
|
||||||
|
if (mimeType.type != "image") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return supportedTypes.contains(mimeType.subtype)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun process(
|
||||||
|
fileType: FileType,
|
||||||
|
contentType: String,
|
||||||
|
fileName: String,
|
||||||
|
file: ByteArray,
|
||||||
|
thumbnail: ByteArray?
|
||||||
|
): ProcessedMedia {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun process(
|
||||||
|
mimeType: MimeType,
|
||||||
|
fileName: String,
|
||||||
|
filePath: Path,
|
||||||
|
thumbnails: Path?
|
||||||
|
): ProcessedMediaPath = withContext(Dispatchers.IO + MDCContext()) {
|
||||||
|
val bufferedImage = ImageIO.read(filePath.inputStream())
|
||||||
|
val tempFileName = UUID.randomUUID().toString()
|
||||||
|
val tempFile = Files.createTempFile(tempFileName, "tmp")
|
||||||
|
|
||||||
|
val thumbnailPath = if (genThumbnail) {
|
||||||
|
val tempThumbnailFile = Files.createTempFile("thumbnail-$tempFileName", ".tmp")
|
||||||
|
|
||||||
|
tempThumbnailFile.outputStream().use {
|
||||||
|
val write = ImageIO.write(
|
||||||
|
if (thumbnails != null) {
|
||||||
|
Thumbnails.of(thumbnails.toFile()).size(width, height).asBufferedImage()
|
||||||
|
} else {
|
||||||
|
Thumbnails.of(bufferedImage).size(width, height).asBufferedImage()
|
||||||
|
},
|
||||||
|
convertType,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
if (write) {
|
||||||
|
tempThumbnailFile
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
tempFile.outputStream().use {
|
||||||
|
if (ImageIO.write(bufferedImage, convertType, it).not()) {
|
||||||
|
logger.warn("Failed to save a temporary file. type: {} ,path: {}", convertType, tempFile)
|
||||||
|
throw MediaProcessException("Failed to save a temporary file.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ProcessedMediaPath(
|
||||||
|
tempFile,
|
||||||
|
thumbnailPath,
|
||||||
|
MimeType("image", convertType, FileType.Image),
|
||||||
|
MimeType("image", convertType, FileType.Image).takeIf { genThumbnail }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(ImageMediaProcessService::class.java)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package dev.usbharu.hideout.core.service.media.converter.image
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
|
|
||||||
|
@ConfigurationProperties("hideout.media.image")
|
||||||
|
data class ImageMediaProcessorConfiguration(
|
||||||
|
val convert: String?,
|
||||||
|
val thubnail: ImageMediaProcessorThumbnailConfiguration?,
|
||||||
|
val supportedType: List<String>?,
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ImageMediaProcessorThumbnailConfiguration(
|
||||||
|
val generate: Boolean,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int
|
||||||
|
)
|
|
@ -0,0 +1,126 @@
|
||||||
|
package dev.usbharu.hideout.core.service.media.converter.movie
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.service.media.FileType
|
||||||
|
import dev.usbharu.hideout.core.service.media.MimeType
|
||||||
|
import dev.usbharu.hideout.core.service.media.ProcessedMedia
|
||||||
|
import dev.usbharu.hideout.core.service.media.ProcessedMediaPath
|
||||||
|
import dev.usbharu.hideout.core.service.media.converter.MediaProcessService
|
||||||
|
import org.bytedeco.ffmpeg.global.avcodec
|
||||||
|
import org.bytedeco.javacv.FFmpegFrameFilter
|
||||||
|
import org.bytedeco.javacv.FFmpegFrameGrabber
|
||||||
|
import org.bytedeco.javacv.FFmpegFrameRecorder
|
||||||
|
import org.bytedeco.javacv.Java2DFrameConverter
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Qualifier("video")
|
||||||
|
class MovieMediaProcessService : MediaProcessService {
|
||||||
|
override fun isSupport(mimeType: MimeType): Boolean = mimeType.type == "video"
|
||||||
|
|
||||||
|
override suspend fun process(
|
||||||
|
fileType: FileType,
|
||||||
|
contentType: String,
|
||||||
|
fileName: String,
|
||||||
|
file: ByteArray,
|
||||||
|
thumbnail: ByteArray?
|
||||||
|
): ProcessedMedia {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun process(
|
||||||
|
mimeType: MimeType,
|
||||||
|
fileName: String,
|
||||||
|
filePath: Path,
|
||||||
|
thumbnails: Path?
|
||||||
|
): ProcessedMediaPath {
|
||||||
|
val tempFile = Files.createTempFile("hideout-movie-processor-", ".tmp")
|
||||||
|
val thumbnailFile = Files.createTempFile("hideout-movie-thumbnail-generate-", ".tmp")
|
||||||
|
logger.info("START Convert Movie Media {}", fileName)
|
||||||
|
FFmpegFrameGrabber(filePath.toFile()).use { grabber ->
|
||||||
|
grabber.start()
|
||||||
|
val width = grabber.imageWidth
|
||||||
|
val height = grabber.imageHeight
|
||||||
|
val frameRate = 60.0
|
||||||
|
|
||||||
|
logger.debug("Movie Media Width {}, Height {}", width, height)
|
||||||
|
|
||||||
|
FFmpegFrameFilter(
|
||||||
|
"fps=fps=${frameRate.toInt()}",
|
||||||
|
"anull",
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
grabber.audioChannels
|
||||||
|
).use { filter ->
|
||||||
|
|
||||||
|
filter.sampleFormat = grabber.sampleFormat
|
||||||
|
filter.sampleRate = grabber.sampleRate
|
||||||
|
filter.pixelFormat = grabber.pixelFormat
|
||||||
|
filter.frameRate = grabber.frameRate
|
||||||
|
filter.start()
|
||||||
|
|
||||||
|
val videoBitRate = min(1300000, (width * height * frameRate * 1 * 0.07).toInt())
|
||||||
|
|
||||||
|
logger.debug("Movie Media BitRate {}", videoBitRate)
|
||||||
|
|
||||||
|
FFmpegFrameRecorder(tempFile.toFile(), width, height, grabber.audioChannels).use {
|
||||||
|
it.sampleRate = grabber.sampleRate
|
||||||
|
it.format = "mp4"
|
||||||
|
it.videoCodec = avcodec.AV_CODEC_ID_H264
|
||||||
|
it.audioCodec = avcodec.AV_CODEC_ID_AAC
|
||||||
|
it.audioChannels = grabber.audioChannels
|
||||||
|
it.videoQuality = 1.0
|
||||||
|
it.frameRate = frameRate
|
||||||
|
it.setVideoOption("preset", "ultrafast")
|
||||||
|
it.timestamp = 0
|
||||||
|
it.gopSize = frameRate.toInt()
|
||||||
|
it.videoBitrate = videoBitRate
|
||||||
|
it.start()
|
||||||
|
|
||||||
|
var bufferedImage: BufferedImage? = null
|
||||||
|
|
||||||
|
val frameConverter = Java2DFrameConverter()
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val grab = grabber.grab() ?: break
|
||||||
|
|
||||||
|
if (bufferedImage == null) {
|
||||||
|
bufferedImage = frameConverter.convert(grab)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grab.image != null || grab.samples != null) {
|
||||||
|
filter.push(grab)
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
val frame = filter.pull() ?: break
|
||||||
|
it.record(frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bufferedImage != null) {
|
||||||
|
ImageIO.write(bufferedImage, "jpeg", thumbnailFile.toFile())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("SUCCESS Convert Movie Media {}", fileName)
|
||||||
|
|
||||||
|
return ProcessedMediaPath(
|
||||||
|
tempFile,
|
||||||
|
thumbnailFile,
|
||||||
|
MimeType("video", "mp4", FileType.Video),
|
||||||
|
MimeType("image", "jpeg", FileType.Image)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(MovieMediaProcessService::class.java)
|
||||||
|
}
|
||||||
|
}
|
|
@ -45,7 +45,7 @@ class ReactionServiceImpl(
|
||||||
reactionQueryService.findByPostIdAndUserIdAndEmojiId(postId, userId, 0)
|
reactionQueryService.findByPostIdAndUserIdAndEmojiId(postId, userId, 0)
|
||||||
reactionRepository.delete(findByPostIdAndUserIdAndEmojiId)
|
reactionRepository.delete(findByPostIdAndUserIdAndEmojiId)
|
||||||
apReactionService.removeReaction(findByPostIdAndUserIdAndEmojiId)
|
apReactionService.removeReaction(findByPostIdAndUserIdAndEmojiId)
|
||||||
} catch (e: FailedToGetResourcesException) {
|
} catch (_: FailedToGetResourcesException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
package dev.usbharu.hideout.util
|
||||||
|
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
fun <T : Path?> T.withDelete(): TempFile<T> = TempFile(this)
|
||||||
|
|
||||||
|
class TempFile<T : Path?>(val path: T) : AutoCloseable {
|
||||||
|
override fun close() {
|
||||||
|
path?.let { Files.deleteIfExists(it) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,7 +30,10 @@ spring:
|
||||||
database: hideout
|
database: hideout
|
||||||
# username: hideoutuser
|
# username: hideoutuser
|
||||||
# password: hideoutpass
|
# password: hideoutpass
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 40MB
|
||||||
|
max-request-size: 40MB
|
||||||
h2:
|
h2:
|
||||||
console:
|
console:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -42,5 +45,6 @@ server:
|
||||||
basedir: tomcat
|
basedir: tomcat
|
||||||
accesslog:
|
accesslog:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
max-http-form-post-size: 40MB
|
||||||
|
max-swallow-size: 40MB
|
||||||
port: 8081
|
port: 8081
|
||||||
|
|
|
@ -37,7 +37,6 @@ import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.assertThrows
|
import org.junit.jupiter.api.assertThrows
|
||||||
import org.mockito.kotlin.*
|
import org.mockito.kotlin.*
|
||||||
import utils.JsonObjectMapper.objectMapper
|
|
||||||
import utils.PostBuilder
|
import utils.PostBuilder
|
||||||
import utils.UserBuilder
|
import utils.UserBuilder
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
@ -74,7 +73,6 @@ class APNoteServiceImplTest {
|
||||||
postRepository = mock(),
|
postRepository = mock(),
|
||||||
apUserService = mock(),
|
apUserService = mock(),
|
||||||
postQueryService = mock(),
|
postQueryService = mock(),
|
||||||
objectMapper = objectMapper,
|
|
||||||
postService = mock(),
|
postService = mock(),
|
||||||
apResourceResolveService = mock(),
|
apResourceResolveService = mock(),
|
||||||
postBuilder = Post.PostBuilder(CharacterLimit()),
|
postBuilder = Post.PostBuilder(CharacterLimit()),
|
||||||
|
@ -152,7 +150,6 @@ class APNoteServiceImplTest {
|
||||||
postRepository = postRepository,
|
postRepository = postRepository,
|
||||||
apUserService = apUserService,
|
apUserService = apUserService,
|
||||||
postQueryService = postQueryService,
|
postQueryService = postQueryService,
|
||||||
objectMapper = objectMapper,
|
|
||||||
postService = mock(),
|
postService = mock(),
|
||||||
apResourceResolveService = apResourceResolveService,
|
apResourceResolveService = apResourceResolveService,
|
||||||
postBuilder = Post.PostBuilder(CharacterLimit()),
|
postBuilder = Post.PostBuilder(CharacterLimit()),
|
||||||
|
@ -221,7 +218,6 @@ class APNoteServiceImplTest {
|
||||||
postRepository = mock(),
|
postRepository = mock(),
|
||||||
apUserService = mock(),
|
apUserService = mock(),
|
||||||
postQueryService = postQueryService,
|
postQueryService = postQueryService,
|
||||||
objectMapper = objectMapper,
|
|
||||||
postService = mock(),
|
postService = mock(),
|
||||||
apResourceResolveService = apResourceResolveService,
|
apResourceResolveService = apResourceResolveService,
|
||||||
postBuilder = Post.PostBuilder(CharacterLimit()),
|
postBuilder = Post.PostBuilder(CharacterLimit()),
|
||||||
|
@ -274,7 +270,6 @@ class APNoteServiceImplTest {
|
||||||
postRepository = postRepository,
|
postRepository = postRepository,
|
||||||
apUserService = apUserService,
|
apUserService = apUserService,
|
||||||
postQueryService = mock(),
|
postQueryService = mock(),
|
||||||
objectMapper = objectMapper,
|
|
||||||
postService = postService,
|
postService = postService,
|
||||||
apResourceResolveService = mock(),
|
apResourceResolveService = mock(),
|
||||||
postBuilder = postBuilder,
|
postBuilder = postBuilder,
|
||||||
|
@ -333,7 +328,6 @@ class APNoteServiceImplTest {
|
||||||
postRepository = mock(),
|
postRepository = mock(),
|
||||||
apUserService = mock(),
|
apUserService = mock(),
|
||||||
postQueryService = mock(),
|
postQueryService = mock(),
|
||||||
objectMapper = objectMapper,
|
|
||||||
postService = mock(),
|
postService = mock(),
|
||||||
apResourceResolveService = mock(),
|
apResourceResolveService = mock(),
|
||||||
postBuilder = postBuilder,
|
postBuilder = postBuilder,
|
||||||
|
|
Loading…
Reference in New Issue