Commit 2d9db234 authored by Him188's avatar Him188

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
parent 96a58252
...@@ -87,6 +87,10 @@ internal class FriendImpl( ...@@ -87,6 +87,10 @@ internal class FriendImpl(
@JvmSynthetic @JvmSynthetic
@OptIn(MiraiInternalAPI::class, ExperimentalStdlibApi::class, ExperimentalTime::class) @OptIn(MiraiInternalAPI::class, ExperimentalStdlibApi::class, ExperimentalTime::class)
override suspend fun uploadImage(image: ExternalImage): Image = try { 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) { if (BeforeImageUploadEvent(this, image).broadcast().isCancelled) {
throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup") throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup")
} }
...@@ -96,10 +100,10 @@ internal class FriendImpl( ...@@ -96,10 +100,10 @@ internal class FriendImpl(
srcUin = bot.id.toInt(), srcUin = bot.id.toInt(),
dstUin = id.toInt(), dstUin = id.toInt(),
fileId = 0, fileId = 0,
fileMd5 = image.md5, fileMd5 = @Suppress("INVISIBLE_MEMBER") image.md5,
fileSize = @Suppress("INVISIBLE_MEMBER") fileSize = @Suppress("INVISIBLE_MEMBER")
image.input.size.toInt(), image.input.size.toInt(),
fileName = image.md5.toUHexString("") + "." + ExternalImage.defaultFormatName, fileName = @Suppress("INVISIBLE_MEMBER") image.md5.toUHexString("") + "." + ExternalImage.defaultFormatName,
imgOriginal = 1 imgOriginal = 1
) )
).sendAndExpect<LongConn.OffPicUp.Response>() ).sendAndExpect<LongConn.OffPicUp.Response>()
......
...@@ -406,6 +406,10 @@ internal class GroupImpl( ...@@ -406,6 +406,10 @@ internal class GroupImpl(
@OptIn(ExperimentalTime::class) @OptIn(ExperimentalTime::class)
@JvmSynthetic @JvmSynthetic
override suspend fun uploadImage(image: ExternalImage): OfflineGroupImage = try { 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) { if (BeforeImageUploadEvent(this, image).broadcast().isCancelled) {
throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup") throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup")
} }
......
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
package net.mamoe.mirai.qqandroid.network.highway package net.mamoe.mirai.qqandroid.network.highway
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
...@@ -34,9 +36,9 @@ import net.mamoe.mirai.qqandroid.utils.addSuppressedMirai ...@@ -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.serialization.readProtoBuf
import net.mamoe.mirai.qqandroid.utils.io.withUse import net.mamoe.mirai.qqandroid.utils.io.withUse
import net.mamoe.mirai.qqandroid.utils.toIpV4AddressString import net.mamoe.mirai.qqandroid.utils.toIpV4AddressString
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.internal.ReusableInput
import net.mamoe.mirai.utils.verbose import net.mamoe.mirai.utils.verbose
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
import kotlin.math.roundToInt import kotlin.math.roundToInt
...@@ -44,12 +46,12 @@ import kotlin.time.ExperimentalTime ...@@ -44,12 +46,12 @@ import kotlin.time.ExperimentalTime
import kotlin.time.measureTime import kotlin.time.measureTime
@OptIn(MiraiInternalAPI::class, InternalSerializationApi::class) @OptIn(MiraiInternalAPI::class, InternalSerializationApi::class)
@Suppress("SpellCheckingInspection", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") @Suppress("SpellCheckingInspection")
internal suspend fun HttpClient.postImage( internal suspend fun HttpClient.postImage(
htcmd: String, htcmd: String,
uin: Long, uin: Long,
groupcode: Long?, groupcode: Long?,
imageInput: ExternalImage.ReusableInput, // Input from kotlinx.io, InputStream from kotlinx.io MPP, ByteReadChannel from ktor imageInput: ReusableInput,
uKeyHex: String uKeyHex: String
): Boolean = post<HttpStatusCode> { ): Boolean = post<HttpStatusCode> {
url { url {
...@@ -90,7 +92,7 @@ internal object HighwayHelper { ...@@ -90,7 +92,7 @@ internal object HighwayHelper {
bot: QQAndroidBot, bot: QQAndroidBot,
servers: List<Pair<Int, Int>>, servers: List<Pair<Int, Int>>,
uKey: ByteArray, uKey: ByteArray,
image: ExternalImage.ReusableInput, image: ReusableInput,
kind: String, kind: String,
commandId: Int commandId: Int
) = uploadImageToServers(bot, servers, uKey, image.md5, image, kind, commandId) ) = uploadImageToServers(bot, servers, uKey, image.md5, image, kind, commandId)
...@@ -102,7 +104,7 @@ internal object HighwayHelper { ...@@ -102,7 +104,7 @@ internal object HighwayHelper {
servers: List<Pair<Int, Int>>, servers: List<Pair<Int, Int>>,
uKey: ByteArray, uKey: ByteArray,
md5: ByteArray, md5: ByteArray,
input: ExternalImage.ReusableInput, input: ReusableInput,
kind: String, kind: String,
commandId: Int commandId: Int
) = servers.retryWithServers( ) = servers.retryWithServers(
...@@ -139,7 +141,7 @@ internal object HighwayHelper { ...@@ -139,7 +141,7 @@ internal object HighwayHelper {
serverIp: String, serverIp: String,
serverPort: Int, serverPort: Int,
ticket: ByteArray, ticket: ByteArray,
imageInput: ExternalImage.ReusableInput, imageInput: ReusableInput,
fileMd5: ByteArray, fileMd5: ByteArray,
commandId: Int // group=2, friend=1 commandId: Int // group=2, friend=1
) { ) {
......
...@@ -21,10 +21,10 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY ...@@ -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.ByteArrayPool
import net.mamoe.mirai.qqandroid.utils.MiraiPlatformUtils import net.mamoe.mirai.qqandroid.utils.MiraiPlatformUtils
import net.mamoe.mirai.qqandroid.utils.io.serialization.toByteArray 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.MiraiInternalAPI
import net.mamoe.mirai.utils.internal.ChunkedFlowSession import net.mamoe.mirai.utils.internal.ChunkedFlowSession
import net.mamoe.mirai.utils.internal.ChunkedInput import net.mamoe.mirai.utils.internal.ChunkedInput
import net.mamoe.mirai.utils.internal.ReusableInput
import net.mamoe.mirai.utils.internal.map import net.mamoe.mirai.utils.internal.map
@OptIn(MiraiInternalAPI::class, InternalSerializationApi::class) @OptIn(MiraiInternalAPI::class, InternalSerializationApi::class)
...@@ -37,7 +37,7 @@ internal fun createImageDataPacketSequence( ...@@ -37,7 +37,7 @@ internal fun createImageDataPacketSequence(
commandId: Int, commandId: Int,
localId: Int = 2052, localId: Int = 2052,
ticket: ByteArray, ticket: ByteArray,
data: ExternalImage.ReusableInput, data: ReusableInput,
fileMd5: ByteArray, fileMd5: ByteArray,
sizePerPacket: Int = ByteArrayPool.BUFFER_SIZE sizePerPacket: Int = ByteArrayPool.BUFFER_SIZE
): ChunkedFlowSession<ByteReadPacket> { ): ChunkedFlowSession<ByteReadPacket> {
......
/*
* 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
}
}
\ No newline at end of file
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlinx.coroutines.Job
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.network.BotNetworkHandler import net.mamoe.mirai.network.BotNetworkHandler
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
...@@ -24,30 +25,20 @@ import kotlin.jvm.JvmStatic ...@@ -24,30 +25,20 @@ import kotlin.jvm.JvmStatic
*/ */
@Suppress("PropertyName") @Suppress("PropertyName")
open class BotConfiguration { open class BotConfiguration {
/** /** 日志记录器 */
* 日志记录器
*/
var botLoggerSupplier: ((Bot) -> MiraiLogger) = { DefaultLogger("Bot(${it.id})") } var botLoggerSupplier: ((Bot) -> MiraiLogger) = { DefaultLogger("Bot(${it.id})") }
/** /** 网络层日志构造器 */
* 网络层日志构造器
*/
@OptIn(MiraiInternalAPI::class) @OptIn(MiraiInternalAPI::class)
var networkLoggerSupplier: ((BotNetworkHandler) -> MiraiLogger) = { DefaultLogger("Network(${it.bot.id})") } var networkLoggerSupplier: ((BotNetworkHandler) -> MiraiLogger) = { DefaultLogger("Network(${it.bot.id})") }
/** /** 设备信息覆盖. 默认使用随机的设备信息. */
* 设备信息覆盖. 默认使用随机的设备信息.
*/
var deviceInfo: ((Context) -> DeviceInfo)? = null var deviceInfo: ((Context) -> DeviceInfo)? = null
/** /** 父 [CoroutineContext]. [Bot] 创建后会覆盖其 [Job], 但会将这个 [Job] 作为父 [Job] */
* 父 [CoroutineContext]
*/
var parentCoroutineContext: CoroutineContext = EmptyCoroutineContext var parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
/** /** 心跳周期. 过长会导致被服务器断开连接. */
* 心跳周期. 过长会导致被服务器断开连接.
*/
var heartbeatPeriodMillis: Long = 60.secondsToMillis var heartbeatPeriodMillis: Long = 60.secondsToMillis
/** /**
...@@ -56,31 +47,26 @@ open class BotConfiguration { ...@@ -56,31 +47,26 @@ open class BotConfiguration {
*/ */
var heartbeatTimeoutMillis: Long = 2.secondsToMillis var heartbeatTimeoutMillis: Long = 2.secondsToMillis
/** /** 心跳失败后的第一次重连前的等待时间. */
* 心跳失败后的第一次重连前的等待时间.
*/
var firstReconnectDelayMillis: Long = 5.secondsToMillis var firstReconnectDelayMillis: Long = 5.secondsToMillis
/** /** 重连失败后, 继续尝试的每次等待时间 */
* 重连失败后, 继续尝试的每次等待时间
*/
var reconnectPeriodMillis: Long = 5.secondsToMillis var reconnectPeriodMillis: Long = 5.secondsToMillis
/** /** 最多尝试多少次重连 */
* 最多尝试多少次重连
*/
var reconnectionRetryTimes: Int = Int.MAX_VALUE var reconnectionRetryTimes: Int = Int.MAX_VALUE
/** /** 验证码处理器 */
* 验证码处理器
*/
var loginSolver: LoginSolver = LoginSolver.Default var loginSolver: LoginSolver = LoginSolver.Default
/** /** 使用协议类型 */
* 使用协议类型 @SinceMirai("1.0.0")
*/ var protocol: MiraiProtocol = MiraiProtocol.ANDROID_PAD
/** 缓存策略 */
@SinceMirai("1.0.0") @SinceMirai("1.0.0")
val protocol: MiraiProtocol = MiraiProtocol.ANDROID_PAD @MiraiExperimentalAPI
var fileCacheStrategy: FileCacheStrategy = FileCacheStrategy.PlatformDefault
@SinceMirai("1.0.0") @SinceMirai("1.0.0")
enum class MiraiProtocol( enum class MiraiProtocol(
...@@ -105,9 +91,7 @@ open class BotConfiguration { ...@@ -105,9 +91,7 @@ open class BotConfiguration {
} }
companion object { companion object {
/** /** 默认的配置实例. 可以进行修改 */
* 默认的配置实例
*/
@JvmStatic @JvmStatic
val Default = BotConfiguration() val Default = BotConfiguration()
} }
...@@ -144,11 +128,31 @@ open class BotConfiguration { ...@@ -144,11 +128,31 @@ open class BotConfiguration {
* ``` * ```
*/ */
@ConfigurationDsl @ConfigurationDsl
suspend fun inheritCoroutineContext() { suspend inline fun inheritCoroutineContext() {
parentCoroutineContext = coroutineContext parentCoroutineContext = coroutineContext
} }
@DslMarker @DslMarker
annotation class ConfigurationDsl 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) @OptIn(ExperimentalMultiplatform::class)
......
...@@ -11,15 +11,14 @@ ...@@ -11,15 +11,14 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import io.ktor.utils.io.ByteWriteChannel
import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.User import net.mamoe.mirai.contact.User
import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.sendTo import net.mamoe.mirai.message.data.sendTo
import net.mamoe.mirai.utils.internal.ChunkedFlowSession import net.mamoe.mirai.utils.internal.DeferredReusableInput
import net.mamoe.mirai.utils.internal.ChunkedInput import net.mamoe.mirai.utils.internal.ReusableInput
import kotlin.jvm.JvmField import kotlin.jvm.JvmField
import kotlin.jvm.JvmSynthetic import kotlin.jvm.JvmSynthetic
...@@ -31,24 +30,16 @@ import kotlin.jvm.JvmSynthetic ...@@ -31,24 +30,16 @@ import kotlin.jvm.JvmSynthetic
* @see ExternalImage.sendTo 上传图片并以纯图片消息发送给联系人 * @see ExternalImage.sendTo 上传图片并以纯图片消息发送给联系人
* @See ExternalImage.upload 上传图片并得到 [Image] 消息 * @See ExternalImage.upload 上传图片并得到 [Image] 消息
*/ */
@OptIn(MiraiInternalAPI::class)
class ExternalImage internal constructor( class ExternalImage internal constructor(
@JvmField @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 internal 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
}
init { 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 { companion object {
...@@ -75,10 +66,16 @@ class ExternalImage internal constructor( ...@@ -75,10 +66,16 @@ class ExternalImage internal constructor(
* SHARPP: 1004 * 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)})]" internal fun calculateImageResourceId(): String = generateImageId(md5)
fun calculateImageResourceId(): String = generateImageId(md5)
} }
/** /**
......
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
}
}
\ No newline at end of file
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)
}
\ No newline at end of file
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
}
\ No newline at end of file
...@@ -9,9 +9,6 @@ ...@@ -9,9 +9,6 @@
package net.mamoe.mirai.utils.internal package net.mamoe.mirai.utils.internal
import net.mamoe.mirai.utils.ExternalImage internal expect fun ByteArray.asReusableInput(): ReusableInput
internal fun asReusableInput0(input: ByteArray): ReusableInput = input.asReusableInput()
internal expect fun ByteArray.asReusableInput(): ExternalImage.ReusableInput \ No newline at end of file
internal fun asReusableInput0(input: ByteArray): ExternalImage.ReusableInput = input.asReusableInput()
\ No newline at end of file
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")
}
}
}
}
\ No newline at end of file
...@@ -37,7 +37,7 @@ import java.net.URL ...@@ -37,7 +37,7 @@ import java.net.URL
*/ */
@Throws(OverFileSizeMaxException::class) @Throws(OverFileSizeMaxException::class)
suspend fun <C : Contact> BufferedImage.sendTo(contact: C): MessageReceipt<C> = suspend fun <C : Contact> BufferedImage.sendTo(contact: C): MessageReceipt<C> =
withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact) toExternalImage().sendTo(contact)
/** /**
* 在 [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片发送到指定联系人 * 在 [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片发送到指定联系人
...@@ -45,7 +45,7 @@ suspend fun <C : Contact> BufferedImage.sendTo(contact: C): MessageReceipt<C> = ...@@ -45,7 +45,7 @@ suspend fun <C : Contact> BufferedImage.sendTo(contact: C): MessageReceipt<C> =
*/ */
@Throws(OverFileSizeMaxException::class) @Throws(OverFileSizeMaxException::class)
suspend fun <C : Contact> URL.sendAsImageTo(contact: C): MessageReceipt<C> = suspend fun <C : Contact> URL.sendAsImageTo(contact: C): MessageReceipt<C> =
withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact) toExternalImage().sendTo(contact)
/** /**
* 在 [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片发送到指定联系人 * 在 [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片发送到指定联系人
...@@ -53,7 +53,7 @@ suspend fun <C : Contact> URL.sendAsImageTo(contact: C): MessageReceipt<C> = ...@@ -53,7 +53,7 @@ suspend fun <C : Contact> URL.sendAsImageTo(contact: C): MessageReceipt<C> =
*/ */
@Throws(OverFileSizeMaxException::class) @Throws(OverFileSizeMaxException::class)
suspend fun <C : Contact> Input.sendAsImageTo(contact: C): MessageReceipt<C> = suspend fun <C : Contact> Input.sendAsImageTo(contact: C): MessageReceipt<C> =
withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact) toExternalImage().sendTo(contact)
/** /**
* 在 [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人 * 在 [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人
...@@ -61,7 +61,7 @@ suspend fun <C : Contact> Input.sendAsImageTo(contact: C): MessageReceipt<C> = ...@@ -61,7 +61,7 @@ suspend fun <C : Contact> Input.sendAsImageTo(contact: C): MessageReceipt<C> =
*/ */
@Throws(OverFileSizeMaxException::class) @Throws(OverFileSizeMaxException::class)
suspend fun <C : Contact> InputStream.sendAsImageTo(contact: C): MessageReceipt<C> = suspend fun <C : Contact> InputStream.sendAsImageTo(contact: C): MessageReceipt<C> =
withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact) toExternalImage().sendTo(contact)
/** /**
* 在 [Dispatchers.IO] 中将文件作为图片发送到指定联系人 * 在 [Dispatchers.IO] 中将文件作为图片发送到指定联系人
...@@ -70,7 +70,7 @@ suspend fun <C : Contact> InputStream.sendAsImageTo(contact: C): MessageReceipt< ...@@ -70,7 +70,7 @@ suspend fun <C : Contact> InputStream.sendAsImageTo(contact: C): MessageReceipt<
@Throws(OverFileSizeMaxException::class) @Throws(OverFileSizeMaxException::class)
suspend fun <C : Contact> File.sendAsImageTo(contact: C): MessageReceipt<C> { suspend fun <C : Contact> File.sendAsImageTo(contact: C): MessageReceipt<C> {
require(this.exists() && this.canRead()) require(this.exists() && this.canRead())
return withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact) return toExternalImage().sendTo(contact)
} }
// endregion // endregion
...@@ -84,7 +84,7 @@ suspend fun <C : Contact> File.sendAsImageTo(contact: C): MessageReceipt<C> { ...@@ -84,7 +84,7 @@ suspend fun <C : Contact> File.sendAsImageTo(contact: C): MessageReceipt<C> {
@JvmSynthetic @JvmSynthetic
@Throws(OverFileSizeMaxException::class) @Throws(OverFileSizeMaxException::class)
suspend fun BufferedImage.upload(contact: Contact): Image = suspend fun BufferedImage.upload(contact: Contact): Image =
withContext(Dispatchers.IO) { toExternalImage() }.upload(contact) toExternalImage().upload(contact)
/** /**
* 在 [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片上传后构造 [Image] * 在 [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片上传后构造 [Image]
...@@ -92,7 +92,7 @@ suspend fun BufferedImage.upload(contact: Contact): Image = ...@@ -92,7 +92,7 @@ suspend fun BufferedImage.upload(contact: Contact): Image =
*/ */
@Throws(OverFileSizeMaxException::class) @Throws(OverFileSizeMaxException::class)
suspend fun URL.uploadAsImage(contact: Contact): Image = suspend fun URL.uploadAsImage(contact: Contact): Image =
withContext(Dispatchers.IO) { toExternalImage() }.upload(contact) toExternalImage().upload(contact)
/** /**
* 在 [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片上传后构造 [Image] * 在 [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片上传后构造 [Image]
...@@ -100,7 +100,7 @@ suspend fun URL.uploadAsImage(contact: Contact): Image = ...@@ -100,7 +100,7 @@ suspend fun URL.uploadAsImage(contact: Contact): Image =
*/ */
@Throws(OverFileSizeMaxException::class) @Throws(OverFileSizeMaxException::class)
suspend fun Input.uploadAsImage(contact: Contact): Image = suspend fun Input.uploadAsImage(contact: Contact): Image =
withContext(Dispatchers.IO) { toExternalImage() }.upload(contact) toExternalImage().upload(contact)
/** /**
* 在 [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片上传后构造 [Image] * 在 [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片上传后构造 [Image]
...@@ -108,7 +108,7 @@ suspend fun Input.uploadAsImage(contact: Contact): Image = ...@@ -108,7 +108,7 @@ suspend fun Input.uploadAsImage(contact: Contact): Image =
*/ */
@Throws(OverFileSizeMaxException::class) @Throws(OverFileSizeMaxException::class)
suspend fun InputStream.uploadAsImage(contact: Contact): Image = suspend fun InputStream.uploadAsImage(contact: Contact): Image =
withContext(Dispatchers.IO) { toExternalImage() }.upload(contact) toExternalImage().upload(contact)
/** /**
* 在 [Dispatchers.IO] 中将文件作为图片上传后构造 [Image] * 在 [Dispatchers.IO] 中将文件作为图片上传后构造 [Image]
...@@ -117,7 +117,7 @@ suspend fun InputStream.uploadAsImage(contact: Contact): Image = ...@@ -117,7 +117,7 @@ suspend fun InputStream.uploadAsImage(contact: Contact): Image =
@Throws(OverFileSizeMaxException::class) @Throws(OverFileSizeMaxException::class)
suspend fun File.uploadAsImage(contact: Contact): Image { suspend fun File.uploadAsImage(contact: Contact): Image {
require(this.isFile && this.exists() && this.canRead()) { "file ${this.path} is not readable" } 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 // endregion
......
...@@ -11,22 +11,14 @@ ...@@ -11,22 +11,14 @@
package net.mamoe.mirai.utils 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.Input
import kotlinx.io.core.copyTo import net.mamoe.mirai.Bot
import kotlinx.io.errors.IOException import net.mamoe.mirai.utils.internal.DeferredReusableInput
import kotlinx.io.streams.asOutput
import net.mamoe.mirai.utils.internal.asReusableInput import net.mamoe.mirai.utils.internal.asReusableInput
import net.mamoe.mirai.utils.internal.md5
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream
import java.net.URL import java.net.URL
import java.security.MessageDigest
import javax.imageio.ImageIO
/* /*
* 将各类型图片容器转为 [ExternalImage] * 将各类型图片容器转为 [ExternalImage]
...@@ -34,126 +26,55 @@ import javax.imageio.ImageIO ...@@ -34,126 +26,55 @@ import javax.imageio.ImageIO
/** /**
* 将 [BufferedImage] 保存临时文件, 然后构造 [ExternalImage] * 将 [BufferedImage] 保存临时文件, 然后构造 [ExternalImage]
*/ */
@JvmOverloads @JvmOverloads
@Throws(IOException::class) fun BufferedImage.toExternalImage(formatName: String = "png"): ExternalImage =
fun BufferedImage.toExternalImage(formatName: String = "png"): ExternalImage { ExternalImage(DeferredReusableInput(this, formatName))
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() }
/** /**
* 直接使用文件 [inputStream] 构造 [ExternalImage] * 将文件作为 [ExternalImage] 使用. 只会在需要的时候打开文件并读取数据.
* @param deleteOnClose 若为 `true`, 图片发送后将会删除这个文件
*/ */
@OptIn(MiraiInternalAPI::class) @JvmOverloads
@Throws(IOException::class) fun File.toExternalImage(deleteOnClose: Boolean = false): ExternalImage = ExternalImage(asReusableInput(deleteOnClose))
fun File.toExternalImage(): ExternalImage {
@Suppress("DEPRECATION_ERROR")
return ExternalImage(
input = this.asReusableInput()
)
}
/** /**
* 在 [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) @JvmName("toExternalImage")
fun URL.toExternalImage(): ExternalImage { fun InputStream.toExternalImage(): ExternalImage = ExternalImage(DeferredReusableInput(this, null))
val file = createTempFile().apply { deleteOnExit() }
file.outputStream().use { output ->
openStream().use { input ->
input.copyTo(output)
}
output.flush()
}
return file.toExternalImage()
}
/** /**
* 在 [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()
}
/** @PlannedRemoval("1.2.0")
* 在 [IO] 中进行 [InputStream.toExternalImage] @Deprecated("no need", ReplaceWith("toExternalImage()"))
*/ fun Input.suspendToExternalImage(): ExternalImage = toExternalImage()
suspend inline fun InputStream.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() }
/** @PlannedRemoval("1.2.0")
* 保存为临时文件然后调用 [File.toExternalImage]. @Deprecated("no need", ReplaceWith("toExternalImage()"))
* fun InputStream.suspendToExternalImage(): ExternalImage = 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")
* 在 [IO] 中进行 [Input.toExternalImage] @Deprecated("no need", ReplaceWith("toExternalImage()"))
*/ fun URL.suspendToExternalImage(): ExternalImage = toExternalImage()
suspend inline fun Input.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() }
/** @PlannedRemoval("1.2.0")
* 保存为临时文件然后调用 [File.toExternalImage]. @Deprecated("no need", ReplaceWith("toExternalImage()"))
*/ fun File.suspendToExternalImage(): ExternalImage = toExternalImage()
suspend fun ByteReadChannel.toExternalImage(): ExternalImage {
val file = createTempFile().apply { deleteOnExit() } @PlannedRemoval("1.2.0")
file.outputStream().use { @Deprecated("no need", ReplaceWith("toExternalImage()"))
withContext(IO) { copyTo(it) } fun BufferedImage.suspendToExternalImage(): ExternalImage = toExternalImage()
it.flush()
}
return file.suspendToExternalImage()
}
\ No newline at end of file
@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
}
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
}
\ No newline at end of file
...@@ -5,12 +5,11 @@ import kotlinx.coroutines.Dispatchers ...@@ -5,12 +5,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import net.mamoe.mirai.message.data.toLongUnsigned import net.mamoe.mirai.message.data.toLongUnsigned
import net.mamoe.mirai.utils.ExternalImage
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
internal actual fun ByteArray.asReusableInput(): ExternalImage.ReusableInput { internal actual fun ByteArray.asReusableInput(): ReusableInput {
return object : ExternalImage.ReusableInput { return object : ReusableInput {
override val md5: ByteArray = md5() override val md5: ByteArray = md5()
override val size: Long get() = this@asReusableInput.size.toLongUnsigned() override val size: Long get() = this@asReusableInput.size.toLongUnsigned()
...@@ -32,8 +31,8 @@ internal actual fun ByteArray.asReusableInput(): ExternalImage.ReusableInput { ...@@ -32,8 +31,8 @@ internal actual fun ByteArray.asReusableInput(): ExternalImage.ReusableInput {
} }
} }
internal fun File.asReusableInput(): ExternalImage.ReusableInput { internal fun File.asReusableInput(deleteOnClose: Boolean): ReusableInput {
return object : ExternalImage.ReusableInput { return object : ReusableInput {
override val md5: ByteArray = inputStream().use { it.md5() } override val md5: ByteArray = inputStream().use { it.md5() }
override val size: Long get() = length() override val size: Long get() = length()
...@@ -41,7 +40,10 @@ internal fun File.asReusableInput(): ExternalImage.ReusableInput { ...@@ -41,7 +40,10 @@ internal fun File.asReusableInput(): ExternalImage.ReusableInput {
val stream = inputStream() val stream = inputStream()
return object : ChunkedFlowSession<ChunkedInput> { return object : ChunkedFlowSession<ChunkedInput> {
override val flow: Flow<ChunkedInput> = stream.chunkedFlow(sizePerPacket) 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 { ...@@ -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) { private suspend fun InputStream.copyTo(out: ByteWriteChannel): Long = withContext(Dispatchers.IO) {
var bytesCopied: Long = 0 var bytesCopied: Long = 0
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment