mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-05 07:30:09 +08:00
Introduce FileCacheStrategy
;
Rework `ExternalImage`, introduce `ReusableInput` for multiple attempts when uploading images; Add `BotConfiguration.fileCacheStrategy`, defaults use cache system from host OS; Introduce `DeferredReusableInput` for `*.toExternalImage` on JVM. Deprecate `*.suspendToExternalImage` as no longer need to be suspend. Open input only when required, close input after uploading files, fix #302
This commit is contained in:
parent
96a5825283
commit
2d9db234d7
@ -87,6 +87,10 @@ internal class FriendImpl(
|
||||
@JvmSynthetic
|
||||
@OptIn(MiraiInternalAPI::class, ExperimentalStdlibApi::class, ExperimentalTime::class)
|
||||
override suspend fun uploadImage(image: ExternalImage): Image = try {
|
||||
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
if (image.input is net.mamoe.mirai.utils.internal.DeferredReusableInput) {
|
||||
image.input.init(bot.configuration.fileCacheStrategy)
|
||||
}
|
||||
if (BeforeImageUploadEvent(this, image).broadcast().isCancelled) {
|
||||
throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup")
|
||||
}
|
||||
@ -96,10 +100,10 @@ internal class FriendImpl(
|
||||
srcUin = bot.id.toInt(),
|
||||
dstUin = id.toInt(),
|
||||
fileId = 0,
|
||||
fileMd5 = image.md5,
|
||||
fileMd5 = @Suppress("INVISIBLE_MEMBER") image.md5,
|
||||
fileSize = @Suppress("INVISIBLE_MEMBER")
|
||||
image.input.size.toInt(),
|
||||
fileName = image.md5.toUHexString("") + "." + ExternalImage.defaultFormatName,
|
||||
fileName = @Suppress("INVISIBLE_MEMBER") image.md5.toUHexString("") + "." + ExternalImage.defaultFormatName,
|
||||
imgOriginal = 1
|
||||
)
|
||||
).sendAndExpect<LongConn.OffPicUp.Response>()
|
||||
|
@ -406,6 +406,10 @@ internal class GroupImpl(
|
||||
@OptIn(ExperimentalTime::class)
|
||||
@JvmSynthetic
|
||||
override suspend fun uploadImage(image: ExternalImage): OfflineGroupImage = try {
|
||||
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
if (image.input is net.mamoe.mirai.utils.internal.DeferredReusableInput) {
|
||||
image.input.init(bot.configuration.fileCacheStrategy)
|
||||
}
|
||||
if (BeforeImageUploadEvent(this, image).broadcast().isCancelled) {
|
||||
throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup")
|
||||
}
|
||||
|
@ -7,6 +7,8 @@
|
||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
|
||||
|
||||
package net.mamoe.mirai.qqandroid.network.highway
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
@ -34,9 +36,9 @@ import net.mamoe.mirai.qqandroid.utils.addSuppressedMirai
|
||||
import net.mamoe.mirai.qqandroid.utils.io.serialization.readProtoBuf
|
||||
import net.mamoe.mirai.qqandroid.utils.io.withUse
|
||||
import net.mamoe.mirai.qqandroid.utils.toIpV4AddressString
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalAPI
|
||||
import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
import net.mamoe.mirai.utils.internal.ReusableInput
|
||||
import net.mamoe.mirai.utils.verbose
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.math.roundToInt
|
||||
@ -44,12 +46,12 @@ import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTime
|
||||
|
||||
@OptIn(MiraiInternalAPI::class, InternalSerializationApi::class)
|
||||
@Suppress("SpellCheckingInspection", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
|
||||
@Suppress("SpellCheckingInspection")
|
||||
internal suspend fun HttpClient.postImage(
|
||||
htcmd: String,
|
||||
uin: Long,
|
||||
groupcode: Long?,
|
||||
imageInput: ExternalImage.ReusableInput, // Input from kotlinx.io, InputStream from kotlinx.io MPP, ByteReadChannel from ktor
|
||||
imageInput: ReusableInput,
|
||||
uKeyHex: String
|
||||
): Boolean = post<HttpStatusCode> {
|
||||
url {
|
||||
@ -90,7 +92,7 @@ internal object HighwayHelper {
|
||||
bot: QQAndroidBot,
|
||||
servers: List<Pair<Int, Int>>,
|
||||
uKey: ByteArray,
|
||||
image: ExternalImage.ReusableInput,
|
||||
image: ReusableInput,
|
||||
kind: String,
|
||||
commandId: Int
|
||||
) = uploadImageToServers(bot, servers, uKey, image.md5, image, kind, commandId)
|
||||
@ -102,7 +104,7 @@ internal object HighwayHelper {
|
||||
servers: List<Pair<Int, Int>>,
|
||||
uKey: ByteArray,
|
||||
md5: ByteArray,
|
||||
input: ExternalImage.ReusableInput,
|
||||
input: ReusableInput,
|
||||
kind: String,
|
||||
commandId: Int
|
||||
) = servers.retryWithServers(
|
||||
@ -139,7 +141,7 @@ internal object HighwayHelper {
|
||||
serverIp: String,
|
||||
serverPort: Int,
|
||||
ticket: ByteArray,
|
||||
imageInput: ExternalImage.ReusableInput,
|
||||
imageInput: ReusableInput,
|
||||
fileMd5: ByteArray,
|
||||
commandId: Int // group=2, friend=1
|
||||
) {
|
||||
|
@ -21,10 +21,10 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
|
||||
import net.mamoe.mirai.qqandroid.utils.ByteArrayPool
|
||||
import net.mamoe.mirai.qqandroid.utils.MiraiPlatformUtils
|
||||
import net.mamoe.mirai.qqandroid.utils.io.serialization.toByteArray
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
import net.mamoe.mirai.utils.internal.ChunkedFlowSession
|
||||
import net.mamoe.mirai.utils.internal.ChunkedInput
|
||||
import net.mamoe.mirai.utils.internal.ReusableInput
|
||||
import net.mamoe.mirai.utils.internal.map
|
||||
|
||||
@OptIn(MiraiInternalAPI::class, InternalSerializationApi::class)
|
||||
@ -37,7 +37,7 @@ internal fun createImageDataPacketSequence(
|
||||
commandId: Int,
|
||||
localId: Int = 2052,
|
||||
ticket: ByteArray,
|
||||
data: ExternalImage.ReusableInput,
|
||||
data: ReusableInput,
|
||||
fileMd5: ByteArray,
|
||||
sizePerPacket: Int = ByteArrayPool.BUFFER_SIZE
|
||||
): ChunkedFlowSession<ByteReadPacket> {
|
||||
|
@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Mamoe Technologies and contributors.
|
||||
*
|
||||
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
||||
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
|
||||
*
|
||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai
|
||||
|
||||
import io.ktor.utils.io.ByteReadChannel
|
||||
import kotlinx.io.core.Input
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalAPI
|
||||
import net.mamoe.mirai.utils.SinceMirai
|
||||
import net.mamoe.mirai.utils.internal.InputStream
|
||||
import kotlin.jvm.JvmStatic
|
||||
|
||||
/**
|
||||
* Mirai 全局环境.
|
||||
*/
|
||||
@SinceMirai("1.0.0")
|
||||
expect object Mirai {
|
||||
|
||||
@JvmStatic
|
||||
var fileCacheStrategy: FileCacheStrategy
|
||||
|
||||
/**
|
||||
* 缓存策略.
|
||||
*
|
||||
* 图片上传时默认使用文件缓存.
|
||||
*/
|
||||
interface FileCacheStrategy {
|
||||
@MiraiExperimentalAPI
|
||||
fun newImageCache(input: Input): ExternalImage
|
||||
|
||||
@MiraiExperimentalAPI
|
||||
fun newImageCache(input: ByteReadChannel): ExternalImage
|
||||
|
||||
@MiraiExperimentalAPI
|
||||
fun newImageCache(input: InputStream): ExternalImage
|
||||
|
||||
companion object Default : FileCacheStrategy
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
@ -24,30 +25,20 @@ import kotlin.jvm.JvmStatic
|
||||
*/
|
||||
@Suppress("PropertyName")
|
||||
open class BotConfiguration {
|
||||
/**
|
||||
* 日志记录器
|
||||
*/
|
||||
/** 日志记录器 */
|
||||
var botLoggerSupplier: ((Bot) -> MiraiLogger) = { DefaultLogger("Bot(${it.id})") }
|
||||
|
||||
/**
|
||||
* 网络层日志构造器
|
||||
*/
|
||||
/** 网络层日志构造器 */
|
||||
@OptIn(MiraiInternalAPI::class)
|
||||
var networkLoggerSupplier: ((BotNetworkHandler) -> MiraiLogger) = { DefaultLogger("Network(${it.bot.id})") }
|
||||
|
||||
/**
|
||||
* 设备信息覆盖. 默认使用随机的设备信息.
|
||||
*/
|
||||
/** 设备信息覆盖. 默认使用随机的设备信息. */
|
||||
var deviceInfo: ((Context) -> DeviceInfo)? = null
|
||||
|
||||
/**
|
||||
* 父 [CoroutineContext]
|
||||
*/
|
||||
/** 父 [CoroutineContext]. [Bot] 创建后会覆盖其 [Job], 但会将这个 [Job] 作为父 [Job] */
|
||||
var parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
|
||||
|
||||
/**
|
||||
* 心跳周期. 过长会导致被服务器断开连接.
|
||||
*/
|
||||
/** 心跳周期. 过长会导致被服务器断开连接. */
|
||||
var heartbeatPeriodMillis: Long = 60.secondsToMillis
|
||||
|
||||
/**
|
||||
@ -56,31 +47,26 @@ open class BotConfiguration {
|
||||
*/
|
||||
var heartbeatTimeoutMillis: Long = 2.secondsToMillis
|
||||
|
||||
/**
|
||||
* 心跳失败后的第一次重连前的等待时间.
|
||||
*/
|
||||
/** 心跳失败后的第一次重连前的等待时间. */
|
||||
var firstReconnectDelayMillis: Long = 5.secondsToMillis
|
||||
|
||||
/**
|
||||
* 重连失败后, 继续尝试的每次等待时间
|
||||
*/
|
||||
/** 重连失败后, 继续尝试的每次等待时间 */
|
||||
var reconnectPeriodMillis: Long = 5.secondsToMillis
|
||||
|
||||
/**
|
||||
* 最多尝试多少次重连
|
||||
*/
|
||||
/** 最多尝试多少次重连 */
|
||||
var reconnectionRetryTimes: Int = Int.MAX_VALUE
|
||||
|
||||
/**
|
||||
* 验证码处理器
|
||||
*/
|
||||
/** 验证码处理器 */
|
||||
var loginSolver: LoginSolver = LoginSolver.Default
|
||||
|
||||
/**
|
||||
* 使用协议类型
|
||||
*/
|
||||
/** 使用协议类型 */
|
||||
@SinceMirai("1.0.0")
|
||||
val protocol: MiraiProtocol = MiraiProtocol.ANDROID_PAD
|
||||
var protocol: MiraiProtocol = MiraiProtocol.ANDROID_PAD
|
||||
|
||||
/** 缓存策略 */
|
||||
@SinceMirai("1.0.0")
|
||||
@MiraiExperimentalAPI
|
||||
var fileCacheStrategy: FileCacheStrategy = FileCacheStrategy.PlatformDefault
|
||||
|
||||
@SinceMirai("1.0.0")
|
||||
enum class MiraiProtocol(
|
||||
@ -105,9 +91,7 @@ open class BotConfiguration {
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* 默认的配置实例
|
||||
*/
|
||||
/** 默认的配置实例. 可以进行修改 */
|
||||
@JvmStatic
|
||||
val Default = BotConfiguration()
|
||||
}
|
||||
@ -144,11 +128,31 @@ open class BotConfiguration {
|
||||
* ```
|
||||
*/
|
||||
@ConfigurationDsl
|
||||
suspend fun inheritCoroutineContext() {
|
||||
suspend inline fun inheritCoroutineContext() {
|
||||
parentCoroutineContext = coroutineContext
|
||||
}
|
||||
|
||||
@DslMarker
|
||||
annotation class ConfigurationDsl
|
||||
|
||||
@SinceMirai("1.0.0")
|
||||
fun copy(): BotConfiguration {
|
||||
@OptIn(MiraiExperimentalAPI::class)
|
||||
return BotConfiguration().also { new ->
|
||||
new.botLoggerSupplier = botLoggerSupplier
|
||||
new.networkLoggerSupplier = networkLoggerSupplier
|
||||
new.deviceInfo = deviceInfo
|
||||
new.parentCoroutineContext = parentCoroutineContext
|
||||
new.heartbeatPeriodMillis = heartbeatPeriodMillis
|
||||
new.heartbeatTimeoutMillis = heartbeatTimeoutMillis
|
||||
new.firstReconnectDelayMillis = firstReconnectDelayMillis
|
||||
new.reconnectPeriodMillis = reconnectPeriodMillis
|
||||
new.reconnectionRetryTimes = reconnectionRetryTimes
|
||||
new.loginSolver = loginSolver
|
||||
new.protocol = protocol
|
||||
new.fileCacheStrategy = fileCacheStrategy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMultiplatform::class)
|
||||
|
@ -11,15 +11,14 @@
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import io.ktor.utils.io.ByteWriteChannel
|
||||
import net.mamoe.mirai.contact.Contact
|
||||
import net.mamoe.mirai.contact.Group
|
||||
import net.mamoe.mirai.contact.User
|
||||
import net.mamoe.mirai.message.MessageReceipt
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.message.data.sendTo
|
||||
import net.mamoe.mirai.utils.internal.ChunkedFlowSession
|
||||
import net.mamoe.mirai.utils.internal.ChunkedInput
|
||||
import net.mamoe.mirai.utils.internal.DeferredReusableInput
|
||||
import net.mamoe.mirai.utils.internal.ReusableInput
|
||||
import kotlin.jvm.JvmField
|
||||
import kotlin.jvm.JvmSynthetic
|
||||
|
||||
@ -31,24 +30,16 @@ import kotlin.jvm.JvmSynthetic
|
||||
* @see ExternalImage.sendTo 上传图片并以纯图片消息发送给联系人
|
||||
* @See ExternalImage.upload 上传图片并得到 [Image] 消息
|
||||
*/
|
||||
@OptIn(MiraiInternalAPI::class)
|
||||
class ExternalImage internal constructor(
|
||||
@JvmField
|
||||
internal val input: ReusableInput // Input from kotlinx.io, InputStream from kotlinx.io MPP, ByteReadChannel from ktor
|
||||
internal val input: ReusableInput
|
||||
) {
|
||||
val md5: ByteArray get() = this.input.md5
|
||||
|
||||
@SinceMirai("1.0.0")
|
||||
internal interface ReusableInput {
|
||||
val md5: ByteArray
|
||||
val size: Long
|
||||
|
||||
fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession<ChunkedInput>
|
||||
suspend fun writeTo(out: ByteWriteChannel): Long
|
||||
}
|
||||
internal val md5: ByteArray get() = this.input.md5
|
||||
|
||||
init {
|
||||
require(input.size < 30L * 1024 * 1024) { "Image file is too big. Maximum is 30 MiB, but recommended to be 20 MiB" }
|
||||
if (input !is DeferredReusableInput) {
|
||||
require(input.size < 30L * 1024 * 1024) { "Image file is too big. Maximum is 30 MiB, but recommended to be 20 MiB" }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -75,10 +66,16 @@ class ExternalImage internal constructor(
|
||||
* SHARPP: 1004
|
||||
*/
|
||||
|
||||
override fun toString(): String {
|
||||
if (input is DeferredReusableInput) {
|
||||
if (!input.initialized) {
|
||||
return "ExternalImage(uninitialized)"
|
||||
}
|
||||
}
|
||||
return "ExternalImage(${generateUUID(md5)})"
|
||||
}
|
||||
|
||||
override fun toString(): String = "[ExternalImage(${generateUUID(md5)})]"
|
||||
|
||||
fun calculateImageResourceId(): String = generateImageId(md5)
|
||||
internal fun calculateImageResourceId(): String = generateImageId(md5)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,60 @@
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import kotlinx.io.core.Input
|
||||
import kotlinx.io.errors.IOException
|
||||
import net.mamoe.mirai.utils.internal.InputStream
|
||||
|
||||
/**
|
||||
* 缓存策略.
|
||||
*
|
||||
* 图片上传时默认使用文件缓存.
|
||||
*/
|
||||
@MiraiExperimentalAPI
|
||||
expect interface FileCacheStrategy {
|
||||
/**
|
||||
* 将 [input] 缓存为 [ExternalImage].
|
||||
* 此函数应 close 这个 [Input]
|
||||
*/
|
||||
@MiraiExperimentalAPI
|
||||
@Throws(IOException::class)
|
||||
fun newImageCache(input: Input): ExternalImage
|
||||
|
||||
/**
|
||||
* 将 [input] 缓存为 [ExternalImage].
|
||||
* 此函数应 close 这个 [InputStream]
|
||||
*/
|
||||
@MiraiExperimentalAPI
|
||||
@Throws(IOException::class)
|
||||
fun newImageCache(input: InputStream): ExternalImage
|
||||
|
||||
/**
|
||||
* 将 [input] 缓存为 [ExternalImage].
|
||||
* 此 [input] 的内容应是不变的.
|
||||
*/
|
||||
@MiraiExperimentalAPI
|
||||
@Throws(IOException::class)
|
||||
fun newImageCache(input: ByteArray): ExternalImage
|
||||
|
||||
/**
|
||||
* 默认的缓存方案. 在 JVM 平台使用系统临时文件.
|
||||
*/
|
||||
@MiraiExperimentalAPI
|
||||
object PlatformDefault : FileCacheStrategy
|
||||
|
||||
/**
|
||||
* 使用内存直接存储所有图片文件.
|
||||
*/
|
||||
object MemoryCache : FileCacheStrategy {
|
||||
@MiraiExperimentalAPI
|
||||
@Throws(IOException::class)
|
||||
override fun newImageCache(input: Input): ExternalImage
|
||||
|
||||
@MiraiExperimentalAPI
|
||||
@Throws(IOException::class)
|
||||
override fun newImageCache(input: InputStream): ExternalImage
|
||||
|
||||
@MiraiExperimentalAPI
|
||||
@Throws(IOException::class)
|
||||
override fun newImageCache(input: ByteArray): ExternalImage
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package net.mamoe.mirai.utils.internal
|
||||
|
||||
import net.mamoe.mirai.utils.FileCacheStrategy
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalAPI
|
||||
|
||||
internal expect class DeferredReusableInput(input: Any, extraArg: Any?) : ReusableInput {
|
||||
val initialized: Boolean
|
||||
|
||||
@OptIn(MiraiExperimentalAPI::class)
|
||||
suspend fun init(strategy: FileCacheStrategy)
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package net.mamoe.mirai.utils.internal
|
||||
|
||||
import io.ktor.utils.io.ByteWriteChannel
|
||||
import net.mamoe.mirai.utils.SinceMirai
|
||||
|
||||
@SinceMirai("1.0.0")
|
||||
internal interface ReusableInput {
|
||||
val md5: ByteArray
|
||||
val size: Long
|
||||
|
||||
fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession<ChunkedInput>
|
||||
suspend fun writeTo(out: ByteWriteChannel): Long
|
||||
}
|
@ -9,9 +9,6 @@
|
||||
|
||||
package net.mamoe.mirai.utils.internal
|
||||
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
internal expect fun ByteArray.asReusableInput(): ReusableInput
|
||||
|
||||
|
||||
internal expect fun ByteArray.asReusableInput(): ExternalImage.ReusableInput
|
||||
|
||||
internal fun asReusableInput0(input: ByteArray): ExternalImage.ReusableInput = input.asReusableInput()
|
||||
internal fun asReusableInput0(input: ByteArray): ReusableInput = input.asReusableInput()
|
@ -1,46 +0,0 @@
|
||||
package net.mamoe.mirai
|
||||
|
||||
import io.ktor.utils.io.ByteReadChannel
|
||||
import kotlinx.io.core.Input
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalAPI
|
||||
import net.mamoe.mirai.utils.internal.InputStream
|
||||
|
||||
/**
|
||||
* Mirai 全局环境.
|
||||
*/
|
||||
actual object Mirai {
|
||||
actual var fileCacheStrategy: FileCacheStrategy
|
||||
get() = TODO("Not yet implemented")
|
||||
set(value) {}
|
||||
|
||||
actual interface FileCacheStrategy {
|
||||
@MiraiExperimentalAPI
|
||||
actual fun newImageCache(input: Input): ExternalImage
|
||||
|
||||
@MiraiExperimentalAPI
|
||||
actual fun newImageCache(input: ByteReadChannel): ExternalImage
|
||||
|
||||
@MiraiExperimentalAPI
|
||||
actual fun newImageCache(input: InputStream): ExternalImage
|
||||
|
||||
actual companion object Default : FileCacheStrategy {
|
||||
@MiraiExperimentalAPI
|
||||
actual override fun newImageCache(input: Input): ExternalImage {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@MiraiExperimentalAPI
|
||||
actual override fun newImageCache(input: ByteReadChannel): ExternalImage {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@MiraiExperimentalAPI
|
||||
actual override fun newImageCache(input: InputStream): ExternalImage {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -37,7 +37,7 @@ import java.net.URL
|
||||
*/
|
||||
@Throws(OverFileSizeMaxException::class)
|
||||
suspend fun <C : Contact> BufferedImage.sendTo(contact: C): MessageReceipt<C> =
|
||||
withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact)
|
||||
toExternalImage().sendTo(contact)
|
||||
|
||||
/**
|
||||
* 在 [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片发送到指定联系人
|
||||
@ -45,7 +45,7 @@ suspend fun <C : Contact> BufferedImage.sendTo(contact: C): MessageReceipt<C> =
|
||||
*/
|
||||
@Throws(OverFileSizeMaxException::class)
|
||||
suspend fun <C : Contact> URL.sendAsImageTo(contact: C): MessageReceipt<C> =
|
||||
withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact)
|
||||
toExternalImage().sendTo(contact)
|
||||
|
||||
/**
|
||||
* 在 [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片发送到指定联系人
|
||||
@ -53,7 +53,7 @@ suspend fun <C : Contact> URL.sendAsImageTo(contact: C): MessageReceipt<C> =
|
||||
*/
|
||||
@Throws(OverFileSizeMaxException::class)
|
||||
suspend fun <C : Contact> Input.sendAsImageTo(contact: C): MessageReceipt<C> =
|
||||
withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact)
|
||||
toExternalImage().sendTo(contact)
|
||||
|
||||
/**
|
||||
* 在 [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人
|
||||
@ -61,7 +61,7 @@ suspend fun <C : Contact> Input.sendAsImageTo(contact: C): MessageReceipt<C> =
|
||||
*/
|
||||
@Throws(OverFileSizeMaxException::class)
|
||||
suspend fun <C : Contact> InputStream.sendAsImageTo(contact: C): MessageReceipt<C> =
|
||||
withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact)
|
||||
toExternalImage().sendTo(contact)
|
||||
|
||||
/**
|
||||
* 在 [Dispatchers.IO] 中将文件作为图片发送到指定联系人
|
||||
@ -70,7 +70,7 @@ suspend fun <C : Contact> InputStream.sendAsImageTo(contact: C): MessageReceipt<
|
||||
@Throws(OverFileSizeMaxException::class)
|
||||
suspend fun <C : Contact> File.sendAsImageTo(contact: C): MessageReceipt<C> {
|
||||
require(this.exists() && this.canRead())
|
||||
return withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact)
|
||||
return toExternalImage().sendTo(contact)
|
||||
}
|
||||
|
||||
// endregion
|
||||
@ -84,7 +84,7 @@ suspend fun <C : Contact> File.sendAsImageTo(contact: C): MessageReceipt<C> {
|
||||
@JvmSynthetic
|
||||
@Throws(OverFileSizeMaxException::class)
|
||||
suspend fun BufferedImage.upload(contact: Contact): Image =
|
||||
withContext(Dispatchers.IO) { toExternalImage() }.upload(contact)
|
||||
toExternalImage().upload(contact)
|
||||
|
||||
/**
|
||||
* 在 [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片上传后构造 [Image]
|
||||
@ -92,7 +92,7 @@ suspend fun BufferedImage.upload(contact: Contact): Image =
|
||||
*/
|
||||
@Throws(OverFileSizeMaxException::class)
|
||||
suspend fun URL.uploadAsImage(contact: Contact): Image =
|
||||
withContext(Dispatchers.IO) { toExternalImage() }.upload(contact)
|
||||
toExternalImage().upload(contact)
|
||||
|
||||
/**
|
||||
* 在 [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片上传后构造 [Image]
|
||||
@ -100,7 +100,7 @@ suspend fun URL.uploadAsImage(contact: Contact): Image =
|
||||
*/
|
||||
@Throws(OverFileSizeMaxException::class)
|
||||
suspend fun Input.uploadAsImage(contact: Contact): Image =
|
||||
withContext(Dispatchers.IO) { toExternalImage() }.upload(contact)
|
||||
toExternalImage().upload(contact)
|
||||
|
||||
/**
|
||||
* 在 [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片上传后构造 [Image]
|
||||
@ -108,7 +108,7 @@ suspend fun Input.uploadAsImage(contact: Contact): Image =
|
||||
*/
|
||||
@Throws(OverFileSizeMaxException::class)
|
||||
suspend fun InputStream.uploadAsImage(contact: Contact): Image =
|
||||
withContext(Dispatchers.IO) { toExternalImage() }.upload(contact)
|
||||
toExternalImage().upload(contact)
|
||||
|
||||
/**
|
||||
* 在 [Dispatchers.IO] 中将文件作为图片上传后构造 [Image]
|
||||
@ -117,7 +117,7 @@ suspend fun InputStream.uploadAsImage(contact: Contact): Image =
|
||||
@Throws(OverFileSizeMaxException::class)
|
||||
suspend fun File.uploadAsImage(contact: Contact): Image {
|
||||
require(this.isFile && this.exists() && this.canRead()) { "file ${this.path} is not readable" }
|
||||
return withContext(Dispatchers.IO) { toExternalImage() }.upload(contact)
|
||||
return toExternalImage().upload(contact)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
@ -11,22 +11,14 @@
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.io.ByteReadChannel
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.io.core.Input
|
||||
import kotlinx.io.core.copyTo
|
||||
import kotlinx.io.errors.IOException
|
||||
import kotlinx.io.streams.asOutput
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.utils.internal.DeferredReusableInput
|
||||
import net.mamoe.mirai.utils.internal.asReusableInput
|
||||
import net.mamoe.mirai.utils.internal.md5
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.net.URL
|
||||
import java.security.MessageDigest
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
/*
|
||||
* 将各类型图片容器转为 [ExternalImage]
|
||||
@ -34,126 +26,55 @@ import javax.imageio.ImageIO
|
||||
|
||||
|
||||
/**
|
||||
* 将 [BufferedImage] 保存稳临时文件, 然后构造 [ExternalImage]
|
||||
* 将 [BufferedImage] 保存为临时文件, 然后构造 [ExternalImage]
|
||||
*/
|
||||
@JvmOverloads
|
||||
@Throws(IOException::class)
|
||||
fun BufferedImage.toExternalImage(formatName: String = "png"): ExternalImage {
|
||||
val file = createTempFile().apply { deleteOnExit() }
|
||||
|
||||
val digest = MessageDigest.getInstance("md5")
|
||||
digest.reset()
|
||||
|
||||
file.outputStream().use { out ->
|
||||
ImageIO.write(this@toExternalImage, formatName, object : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
out.write(b)
|
||||
digest.update(b.toByte())
|
||||
}
|
||||
|
||||
override fun write(b: ByteArray) {
|
||||
out.write(b)
|
||||
digest.update(b)
|
||||
}
|
||||
|
||||
override fun write(b: ByteArray, off: Int, len: Int) {
|
||||
out.write(b, off, len)
|
||||
digest.update(b, off, len)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION_ERROR")
|
||||
return ExternalImage(file.asReusableInput())
|
||||
}
|
||||
|
||||
suspend inline fun BufferedImage.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() }
|
||||
fun BufferedImage.toExternalImage(formatName: String = "png"): ExternalImage =
|
||||
ExternalImage(DeferredReusableInput(this, formatName))
|
||||
|
||||
/**
|
||||
* 直接使用文件 [inputStream] 构造 [ExternalImage]
|
||||
* 将文件作为 [ExternalImage] 使用. 只会在需要的时候打开文件并读取数据.
|
||||
* @param deleteOnClose 若为 `true`, 图片发送后将会删除这个文件
|
||||
*/
|
||||
@OptIn(MiraiInternalAPI::class)
|
||||
@Throws(IOException::class)
|
||||
fun File.toExternalImage(): ExternalImage {
|
||||
@Suppress("DEPRECATION_ERROR")
|
||||
return ExternalImage(
|
||||
input = this.asReusableInput()
|
||||
)
|
||||
}
|
||||
@JvmOverloads
|
||||
fun File.toExternalImage(deleteOnClose: Boolean = false): ExternalImage = ExternalImage(asReusableInput(deleteOnClose))
|
||||
|
||||
/**
|
||||
* 在 [IO] 中进行 [File.toExternalImage]
|
||||
* 将 [URL] 委托为 [ExternalImage].
|
||||
* 只会在上传图片时才读取 [URL] 的内容. 具体行为取决于相关 [Bot] 的 [FileCacheStrategy]
|
||||
*/
|
||||
suspend inline fun File.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() }
|
||||
fun URL.toExternalImage(): ExternalImage = ExternalImage(DeferredReusableInput(this, null))
|
||||
|
||||
/**
|
||||
* 下载文件到临时目录然后调用 [File.toExternalImage]
|
||||
* 将 [InputStream] 委托为 [ExternalImage].
|
||||
* 只会在上传图片时才读取 [InputStream] 的内容. 具体行为取决于相关 [Bot] 的 [FileCacheStrategy]
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun URL.toExternalImage(): ExternalImage {
|
||||
val file = createTempFile().apply { deleteOnExit() }
|
||||
file.outputStream().use { output ->
|
||||
openStream().use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
output.flush()
|
||||
}
|
||||
return file.toExternalImage()
|
||||
}
|
||||
@JvmName("toExternalImage")
|
||||
fun InputStream.toExternalImage(): ExternalImage = ExternalImage(DeferredReusableInput(this, null))
|
||||
|
||||
/**
|
||||
* 在 [IO] 中进行 [URL.toExternalImage]
|
||||
* 将 [Input] 委托为 [ExternalImage].
|
||||
* 只会在上传图片时才读取 [Input] 的内容. 具体行为取决于相关 [Bot] 的 [FileCacheStrategy]
|
||||
*/
|
||||
suspend inline fun URL.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() }
|
||||
fun Input.toExternalImage(): ExternalImage = ExternalImage(DeferredReusableInput(this, null))
|
||||
|
||||
/**
|
||||
* 保存为临时文件然后调用 [File.toExternalImage]
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun InputStream.toExternalImage(): ExternalImage {
|
||||
val file = createTempFile().apply { deleteOnExit() }
|
||||
file.outputStream().use {
|
||||
this.copyTo(it)
|
||||
it.flush()
|
||||
}
|
||||
this.close()
|
||||
return file.toExternalImage()
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 [IO] 中进行 [InputStream.toExternalImage]
|
||||
*/
|
||||
suspend inline fun InputStream.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() }
|
||||
@PlannedRemoval("1.2.0")
|
||||
@Deprecated("no need", ReplaceWith("toExternalImage()"))
|
||||
fun Input.suspendToExternalImage(): ExternalImage = toExternalImage()
|
||||
|
||||
/**
|
||||
* 保存为临时文件然后调用 [File.toExternalImage].
|
||||
*
|
||||
* 需要函数调用者 close [this]
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun Input.toExternalImage(): ExternalImage {
|
||||
val file = createTempFile().apply { deleteOnExit() }
|
||||
file.outputStream().asOutput().use {
|
||||
this.copyTo(it)
|
||||
it.flush()
|
||||
}
|
||||
return file.toExternalImage()
|
||||
}
|
||||
@PlannedRemoval("1.2.0")
|
||||
@Deprecated("no need", ReplaceWith("toExternalImage()"))
|
||||
fun InputStream.suspendToExternalImage(): ExternalImage = toExternalImage()
|
||||
|
||||
/**
|
||||
* 在 [IO] 中进行 [Input.toExternalImage]
|
||||
*/
|
||||
suspend inline fun Input.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() }
|
||||
@PlannedRemoval("1.2.0")
|
||||
@Deprecated("no need", ReplaceWith("toExternalImage()"))
|
||||
fun URL.suspendToExternalImage(): ExternalImage = toExternalImage()
|
||||
|
||||
/**
|
||||
* 保存为临时文件然后调用 [File.toExternalImage].
|
||||
*/
|
||||
suspend fun ByteReadChannel.toExternalImage(): ExternalImage {
|
||||
val file = createTempFile().apply { deleteOnExit() }
|
||||
file.outputStream().use {
|
||||
withContext(IO) { copyTo(it) }
|
||||
it.flush()
|
||||
}
|
||||
@PlannedRemoval("1.2.0")
|
||||
@Deprecated("no need", ReplaceWith("toExternalImage()"))
|
||||
fun File.suspendToExternalImage(): ExternalImage = toExternalImage()
|
||||
|
||||
return file.suspendToExternalImage()
|
||||
}
|
||||
@PlannedRemoval("1.2.0")
|
||||
@Deprecated("no need", ReplaceWith("toExternalImage()"))
|
||||
fun BufferedImage.suspendToExternalImage(): ExternalImage = toExternalImage()
|
||||
|
@ -0,0 +1,213 @@
|
||||
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import kotlinx.io.core.Closeable
|
||||
import kotlinx.io.core.Input
|
||||
import kotlinx.io.core.readAvailable
|
||||
import kotlinx.io.core.readBytes
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.utils.internal.InputStream
|
||||
import net.mamoe.mirai.utils.internal.asReusableInput
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.net.URL
|
||||
import java.security.MessageDigest
|
||||
import javax.imageio.ImageIO
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
/**
|
||||
* 缓存策略.
|
||||
*
|
||||
* 图片上传时默认使用文件缓存.
|
||||
*
|
||||
* @see BotConfiguration.fileCacheStrategy 为 [Bot] 指定缓存策略
|
||||
*/
|
||||
@MiraiExperimentalAPI
|
||||
actual interface FileCacheStrategy {
|
||||
/**
|
||||
* 将 [input] 缓存为 [ExternalImage].
|
||||
* 此函数应 close 这个 [Input]
|
||||
*/
|
||||
@MiraiExperimentalAPI
|
||||
@Throws(IOException::class)
|
||||
actual fun newImageCache(input: Input): ExternalImage
|
||||
|
||||
/**
|
||||
* 将 [input] 缓存为 [ExternalImage].
|
||||
* 此函数应 close 这个 [InputStream]
|
||||
*/
|
||||
@MiraiExperimentalAPI
|
||||
@Throws(IOException::class)
|
||||
actual fun newImageCache(input: InputStream): ExternalImage
|
||||
|
||||
/**
|
||||
* 将 [input] 缓存为 [ExternalImage].
|
||||
* 此 [input] 的内容应是不变的.
|
||||
*/
|
||||
@MiraiExperimentalAPI
|
||||
@Throws(IOException::class)
|
||||
actual fun newImageCache(input: ByteArray): ExternalImage
|
||||
|
||||
/**
|
||||
* 将 [input] 缓存为 [ExternalImage].
|
||||
* 此 [input] 的内容应是不变的.
|
||||
*/
|
||||
@MiraiExperimentalAPI
|
||||
@Throws(IOException::class)
|
||||
fun newImageCache(input: BufferedImage, format: String = "png"): ExternalImage
|
||||
|
||||
/**
|
||||
* 将 [input] 缓存为 [ExternalImage].
|
||||
*/
|
||||
@MiraiExperimentalAPI
|
||||
@Throws(IOException::class)
|
||||
fun newImageCache(input: URL, format: String = "png"): ExternalImage
|
||||
|
||||
/**
|
||||
* 默认的缓存方案, 使用系统临时文件夹存储.
|
||||
*/
|
||||
@MiraiExperimentalAPI
|
||||
actual object PlatformDefault : FileCacheStrategy by TempCache(null)
|
||||
|
||||
/**
|
||||
* 使用内存直接存储所有图片文件.
|
||||
*/
|
||||
actual object MemoryCache : FileCacheStrategy {
|
||||
@MiraiExperimentalAPI
|
||||
@Throws(IOException::class)
|
||||
actual override fun newImageCache(input: Input): ExternalImage {
|
||||
return newImageCache(input.readBytes())
|
||||
}
|
||||
|
||||
@MiraiExperimentalAPI
|
||||
@Throws(IOException::class)
|
||||
actual override fun newImageCache(input: InputStream): ExternalImage {
|
||||
return newImageCache(input.readBytes())
|
||||
}
|
||||
|
||||
@MiraiExperimentalAPI
|
||||
@Throws(IOException::class)
|
||||
actual override fun newImageCache(input: ByteArray): ExternalImage {
|
||||
return ExternalImage(input.asReusableInput())
|
||||
}
|
||||
|
||||
@MiraiExperimentalAPI
|
||||
override fun newImageCache(input: BufferedImage, format: String): ExternalImage {
|
||||
val out = ByteArrayOutputStream()
|
||||
ImageIO.write(input, format, out)
|
||||
return newImageCache(out.toByteArray())
|
||||
}
|
||||
|
||||
@MiraiExperimentalAPI
|
||||
override fun newImageCache(input: URL, format: String): ExternalImage {
|
||||
val out = ByteArrayOutputStream()
|
||||
input.openConnection().getInputStream().use { it.copyTo(out) }
|
||||
return newImageCache(out.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用系统临时文件夹缓存图片文件. 在图片使用完毕后删除临时文件.
|
||||
*/
|
||||
@MiraiExperimentalAPI
|
||||
class TempCache @JvmOverloads constructor(
|
||||
/**
|
||||
* 缓存图片存放位置
|
||||
*/
|
||||
val directory: File? = null
|
||||
) : FileCacheStrategy {
|
||||
@MiraiExperimentalAPI
|
||||
@Throws(IOException::class)
|
||||
override fun newImageCache(input: Input): ExternalImage {
|
||||
return ExternalImage(createTempFile(directory = directory).apply {
|
||||
deleteOnExit()
|
||||
input.withOut(this.outputStream()) { copyTo(it) }
|
||||
}.asReusableInput(true))
|
||||
}
|
||||
|
||||
@MiraiExperimentalAPI
|
||||
@Throws(IOException::class)
|
||||
override fun newImageCache(input: InputStream): ExternalImage {
|
||||
return ExternalImage(createTempFile(directory = directory).apply {
|
||||
deleteOnExit()
|
||||
input.withOut(this.outputStream()) { copyTo(it) }
|
||||
}.asReusableInput(true))
|
||||
}
|
||||
|
||||
@MiraiExperimentalAPI
|
||||
@Throws(IOException::class)
|
||||
override fun newImageCache(input: ByteArray): ExternalImage {
|
||||
return ExternalImage(input.asReusableInput())
|
||||
}
|
||||
|
||||
@MiraiExperimentalAPI
|
||||
override fun newImageCache(input: BufferedImage, format: String): ExternalImage {
|
||||
val file = createTempFile(directory = directory).apply { deleteOnExit() }
|
||||
|
||||
val digest = MessageDigest.getInstance("md5")
|
||||
digest.reset()
|
||||
|
||||
file.outputStream().use { out ->
|
||||
ImageIO.write(input, format, object : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
out.write(b)
|
||||
digest.update(b.toByte())
|
||||
}
|
||||
|
||||
override fun write(b: ByteArray) {
|
||||
out.write(b)
|
||||
digest.update(b)
|
||||
}
|
||||
|
||||
override fun write(b: ByteArray, off: Int, len: Int) {
|
||||
out.write(b, off, len)
|
||||
digest.update(b, off, len)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION_ERROR")
|
||||
return ExternalImage(file.asReusableInput(true, digest.digest()))
|
||||
}
|
||||
|
||||
@MiraiExperimentalAPI
|
||||
override fun newImageCache(input: URL, format: String): ExternalImage {
|
||||
return ExternalImage(createTempFile(directory = directory).apply {
|
||||
deleteOnExit()
|
||||
input.openConnection().getInputStream().withOut(this.outputStream()) { copyTo(it) }
|
||||
}.asReusableInput(true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
internal inline fun <I : Closeable, O : Closeable, R> I.withOut(output: O, block: I.(output: O) -> R): R {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
return use { output.use { block(this, output) } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies this stream to the given output stream, returning the number of bytes copied
|
||||
*
|
||||
* **Note** It is the caller's responsibility to close both of these resources.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
internal fun Input.copyTo(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE): Long {
|
||||
var bytesCopied: Long = 0
|
||||
val buffer = ByteArray(bufferSize)
|
||||
var bytes = readAvailable(buffer)
|
||||
while (bytes >= 0) {
|
||||
out.write(buffer, 0, bytes)
|
||||
bytesCopied += bytes
|
||||
bytes = readAvailable(buffer)
|
||||
}
|
||||
return bytesCopied
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package net.mamoe.mirai.utils.internal
|
||||
|
||||
import io.ktor.utils.io.ByteWriteChannel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.io.core.Input
|
||||
import net.mamoe.mirai.utils.FileCacheStrategy
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalAPI
|
||||
import java.awt.image.BufferedImage
|
||||
import java.net.URL
|
||||
|
||||
internal actual class DeferredReusableInput actual constructor(
|
||||
val input: Any,
|
||||
val extraArg: Any?
|
||||
) : ReusableInput {
|
||||
|
||||
|
||||
@OptIn(MiraiExperimentalAPI::class)
|
||||
actual suspend fun init(strategy: FileCacheStrategy) = withContext(Dispatchers.IO) {
|
||||
if (delegate != null) {
|
||||
return@withContext
|
||||
}
|
||||
delegate = when (input) {
|
||||
is InputStream -> strategy.newImageCache(input)
|
||||
is ByteArray -> strategy.newImageCache(input)
|
||||
is Input -> strategy.newImageCache(input)
|
||||
is BufferedImage -> strategy.newImageCache(input, extraArg as String)
|
||||
is URL -> strategy.newImageCache(input)
|
||||
else -> error("Internal error: unsupported DeferredReusableInput.input: ${input::class.qualifiedName}")
|
||||
}.input
|
||||
}
|
||||
|
||||
private var delegate: ReusableInput? = null
|
||||
|
||||
override val md5: ByteArray
|
||||
get() = delegate?.md5 ?: error("DeferredReusableInput not yet initialized")
|
||||
override val size: Long
|
||||
get() = delegate?.size ?: error("DeferredReusableInput not yet initialized")
|
||||
|
||||
override fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession<ChunkedInput> {
|
||||
return delegate?.chunkedFlow(sizePerPacket) ?: error("DeferredReusableInput not yet initialized")
|
||||
}
|
||||
|
||||
override suspend fun writeTo(out: ByteWriteChannel): Long {
|
||||
return delegate?.writeTo(out) ?: error("DeferredReusableInput not yet initialized")
|
||||
}
|
||||
|
||||
actual val initialized: Boolean get() = delegate != null
|
||||
}
|
@ -5,12 +5,11 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.mamoe.mirai.message.data.toLongUnsigned
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
internal actual fun ByteArray.asReusableInput(): ExternalImage.ReusableInput {
|
||||
return object : ExternalImage.ReusableInput {
|
||||
internal actual fun ByteArray.asReusableInput(): ReusableInput {
|
||||
return object : ReusableInput {
|
||||
override val md5: ByteArray = md5()
|
||||
override val size: Long get() = this@asReusableInput.size.toLongUnsigned()
|
||||
|
||||
@ -32,8 +31,8 @@ internal actual fun ByteArray.asReusableInput(): ExternalImage.ReusableInput {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun File.asReusableInput(): ExternalImage.ReusableInput {
|
||||
return object : ExternalImage.ReusableInput {
|
||||
internal fun File.asReusableInput(deleteOnClose: Boolean): ReusableInput {
|
||||
return object : ReusableInput {
|
||||
override val md5: ByteArray = inputStream().use { it.md5() }
|
||||
override val size: Long get() = length()
|
||||
|
||||
@ -41,7 +40,10 @@ internal fun File.asReusableInput(): ExternalImage.ReusableInput {
|
||||
val stream = inputStream()
|
||||
return object : ChunkedFlowSession<ChunkedInput> {
|
||||
override val flow: Flow<ChunkedInput> = stream.chunkedFlow(sizePerPacket)
|
||||
override fun close() = stream.close()
|
||||
override fun close() {
|
||||
stream.close()
|
||||
if (deleteOnClose) this@asReusableInput.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,6 +53,27 @@ internal fun File.asReusableInput(): ExternalImage.ReusableInput {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun File.asReusableInput(deleteOnClose: Boolean, md5: ByteArray): ReusableInput {
|
||||
return object : ReusableInput {
|
||||
override val md5: ByteArray get() = md5
|
||||
override val size: Long get() = length()
|
||||
|
||||
override fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession<ChunkedInput> {
|
||||
val stream = inputStream()
|
||||
return object : ChunkedFlowSession<ChunkedInput> {
|
||||
override val flow: Flow<ChunkedInput> = stream.chunkedFlow(sizePerPacket)
|
||||
override fun close() {
|
||||
stream.close()
|
||||
if (deleteOnClose) this@asReusableInput.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun writeTo(out: ByteWriteChannel): Long {
|
||||
return inputStream().use { it.copyTo(out) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun InputStream.copyTo(out: ByteWriteChannel): Long = withContext(Dispatchers.IO) {
|
||||
var bytesCopied: Long = 0
|
||||
|
Loading…
Reference in New Issue
Block a user