All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.utils.ExternalResource.kt Maven / Gradle / Ivy

There is a newer version: 2.12.3
Show newest version
/*
 * Copyright 2019-2022 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/dev/LICENSE
 */

@file:Suppress("EXPERIMENTAL_API_USAGE", "unused")

package net.mamoe.mirai.utils

import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.Mirai
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Contact.Companion.sendImage
import net.mamoe.mirai.contact.Contact.Companion.uploadImage
import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.internal.utils.*
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.FileMessage
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.sendTo
import net.mamoe.mirai.message.data.toVoice
import net.mamoe.mirai.utils.AbstractExternalResource.ResourceCleanCallback
import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
import java.io.*
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract


/**
 * 一个*不可变的*外部资源. 仅包含资源内容, 大小, 文件类型, 校验值而不包含文件名, 文件位置等. 外部资源有可能是一个文件, 也有可能只存在于内存, 或者以任意其他方式实现.
 *
 * [ExternalResource] 在创建之后就应该保持其属性的不变, 即任何时候获取其属性都应该得到相同结果, 任何时候打开流都得到的一样的数据.
 *
 * # 创建
 * - [File.toExternalResource]
 * - [RandomAccessFile.toExternalResource]
 * - [ByteArray.toExternalResource]
 * - [InputStream.toExternalResource]
 *
 * ## 在 Kotlin 获得和使用 [ExternalResource] 实例
 *
 * ```
 * file.toExternalResource().use { resource -> // 安全地使用资源
 *     contact.uploadImage(resource) // 用来上传图片
 *     contact.files.uploadNewFile("/foo/test.txt", file) // 或者用来上传文件
 * }
 * ```
 *
 * 注意, 若使用 [InputStream], 必须手动关闭 [InputStream]. 一种使用情况示例:
 *
 * ```
 * inputStream.use { input -> // 安全地使用 InputStream
 *     input.toExternalResource().use { resource -> // 安全地使用资源
 *         contact.uploadImage(resource) // 用来上传图片
 *         contact.files.uploadNewFile("/foo/test.txt", file) // 或者用来上传文件
 *     }
 * }
 * ```
 *
 * ## 在 Java 获得和使用 [ExternalResource] 实例
 *
 * ```
 * try (ExternalResource resource = ExternalResource.create(file)) { // 使用文件 file
 *     contact.uploadImage(resource); // 用来上传图片
 *     contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件
 * }
 * ```
 *
 * 注意, 若使用 [InputStream], 必须手动关闭 [InputStream]. 一种使用情况示例:
 *
 * ```java
 * try (InputStream stream = ...) { // 安全地使用 InputStream
 *     try (ExternalResource resource = ExternalResource.create(stream)) { // 安全地使用资源
 *         contact.uploadImage(resource); // 用来上传图片
 *         contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件
 *     }
 * }
 * ```
 *
 * # 释放
 *
 * 当 [ExternalResource] 创建时就可能会打开一个文件 (如使用 [File.toExternalResource]).
 * 类似于 [InputStream], [ExternalResource] 需要被 [关闭][close].
 *
 * ## 未释放资源的补救策略
 *
 * 自 2.7 起, 每个 mirai 内置的 [ExternalResource] 实现都有引用跟踪, 当 [ExternalResource] 被 GC 后会执行被动释放.
 * 这依赖于 JVM 垃圾收集策略, 因此不可靠, 资源仍然需要手动 close.
 *
 * ## 使用单次自动释放
 *
 * 若创建的资源仅需要*很快地*使用一次, 可使用 [toAutoCloseable] 获得在使用一次后就会自动关闭的资源.
 *
 * 示例:
 * ```java
 * contact.uploadImage(ExternalResource.create(file).toAutoCloseable()); // 创建并立即使用单次自动释放的资源
 * ```
 *
 * **注意**: 如果仅使用 [toAutoCloseable] 而不通过 [Contact.uploadImage] 等 mirai 内置方法使用资源, 资源仍然会处于打开状态且不会被自动关闭.
 * 最终资源会由上述*未释放资源的补救策略*关闭, 但这依赖于 JVM 垃圾收集策略而不可靠.
 * 因此建议在创建单次自动释放的资源后就尽快使用它, 否则仍然需要考虑在正确的时间及时关闭资源.
 *
 * # 实现 [ExternalResource]
 *
 * 可以自行实现 [ExternalResource]. 但通常上述创建方法已足够使用.
 *
 * 建议继承 [AbstractExternalResource], 这将支持上文提到的资源自动释放功能.
 *
 * 实现时需保持 [ExternalResource] 在构造后就不可变, 并且所有属性都总是返回一个固定值.
 *
 * @see ExternalResource.uploadAsImage 将资源作为图片上传, 得到 [Image]
 * @see ExternalResource.sendAsImageTo 将资源作为图片发送
 * @see Contact.uploadImage 上传一个资源作为图片, 得到 [Image]
 * @see Contact.sendImage 发送一个资源作为图片
 *
 * @see FileCacheStrategy
 */
public interface ExternalResource : Closeable {

    /**
     * 是否在 _使用一次_ 后自动 [close].
     *
     * 该属性仅供调用方参考. 如 [Contact.uploadImage] 会在方法结束时关闭 [isAutoClose] 为 `true` 的 [ExternalResource], 无论上传图片是否成功.
     *
     * 所有 mirai 内置的上传图片, 上传语音等方法都支持该行为.
     *
     * @since 2.8
     */
    @MiraiExperimentalApi
    public val isAutoClose: Boolean
        get() = false

    /**
     * 文件内容 MD5. 16 bytes
     */
    public val md5: ByteArray

    /**
     * 文件内容 SHA1. 16 bytes
     * @since 2.5
     */
    public val sha1: ByteArray
        get() =
            throw UnsupportedOperationException("ExternalResource.sha1 is not implemented by ${this::class.simpleName}")
    // 如果你要实现 [ExternalResource], 你也应该实现 [sha1].
    // 这里默认抛出 [UnsupportedOperationException] 是为了 (姑且) 兼容 2.5 以前的版本的实现.


    /**
     * 文件格式,如 "png", "amr". 当无法自动识别格式时为 [DEFAULT_FORMAT_NAME].
     *
     * 默认会从文件头识别, 支持的文件类型:
     * png, jpg, gif, tif, bmp, amr, silk
     *
     * @see net.mamoe.mirai.utils.getFileType
     * @see net.mamoe.mirai.utils.FILE_TYPES
     * @see DEFAULT_FORMAT_NAME
     */
    public val formatName: String

    /**
     * 文件大小 bytes
     */
    public val size: Long

    /**
     * 当 [close] 时会 [CompletableDeferred.complete] 的 [Deferred].
     */
    public val closed: Deferred

    /**
     * 打开 [InputStream]. 在返回的 [InputStream] 被 [关闭][InputStream.close] 前无法再次打开流.
     *
     * 关闭此流不会关闭 [ExternalResource].
     * @throws IllegalStateException 当上一个流未关闭又尝试打开新的流时抛出
     */
    public fun inputStream(): InputStream

    @MiraiInternalApi
    public fun calculateResourceId(): String {
        return generateImageId(md5, formatName.ifEmpty { DEFAULT_FORMAT_NAME })
    }

    /**
     * 该 [ExternalResource] 的数据来源, 可能有以下的返回
     *
     * - [File] 本地文件
     * - [java.nio.file.Path] 某个具体文件路径
     * - [java.nio.ByteBuffer] RAM
     * - [java.net.URI] uri
     * - [ByteArray] RAM
     * - Or more...
     *
     * implementation note:
     *
     * - 对于无法二次读取的数据来源 (如 [InputStream]), 返回 `null`
     * - 对于一个来自网络的资源, 请返回 [java.net.URI] (not URL, 或者其他库的 URI/URL 类型)
     * - 不要返回 [String], 没有约定 [String] 代表什么
     * - 数据源外漏会严重影响 [inputStream] 等的执行的可以返回 `null` (如 [RandomAccessFile])
     *
     * @since 2.8.0
     */
    public val origin: Any? get() = null

    /**
     * 创建一个在 _使用一次_ 后就会自动 [close] 的 [ExternalResource].
     *
     * @since 2.8.0
     */
    public fun toAutoCloseable(): ExternalResource {
        return if (isAutoClose) this else {
            val delegate = this
            object : ExternalResource by delegate {
                override val isAutoClose: Boolean get() = true
                override fun toString(): String = "ExternalResourceWithAutoClose(delegate=$delegate)"
                override fun toAutoCloseable(): ExternalResource {
                    return this
                }
            }
        }
    }


    public companion object {
        /**
         * 在无法识别文件格式时使用的默认格式名. "mirai".
         *
         * @see ExternalResource.formatName
         */
        public const val DEFAULT_FORMAT_NAME: String = "mirai"

        ///////////////////////////////////////////////////////////////////////////
        // region toExternalResource
        ///////////////////////////////////////////////////////////////////////////

        /**
         * **打开文件**并创建 [ExternalResource].
         * 注意, 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭.
         *
         * 将以只读模式打开这个文件 (因此文件会处于被占用状态), 直到 [ExternalResource.close].
         *
         * @param formatName 查看 [ExternalResource.formatName]
         */
        @JvmStatic
        @JvmOverloads
        @JvmName("create")
        public fun File.toExternalResource(formatName: String? = null): ExternalResource =
            // although RandomAccessFile constructor throws IOException, actual performance influence is minor so not propagating IOException
            RandomAccessFile(this, "r").toExternalResource(formatName).also {
                it.cast().origin = this@toExternalResource
            }

        /**
         * 创建 [ExternalResource].
         * 注意, 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭, 届时将会关闭 [RandomAccessFile].
         *
         * **注意**:若关闭 [RandomAccessFile], 也会间接关闭 [ExternalResource].
         *
         * @see closeOriginalFileOnClose 若为 `true`, 在 [ExternalResource.close] 时将会同步关闭 [RandomAccessFile]. 否则不会.
         *
         * @param formatName 查看 [ExternalResource.formatName]
         */
        @JvmStatic
        @JvmOverloads
        @JvmName("create")
        public fun RandomAccessFile.toExternalResource(
            formatName: String? = null,
            closeOriginalFileOnClose: Boolean = true,
        ): ExternalResource =
            ExternalResourceImplByFile(this, formatName, closeOriginalFileOnClose)

        /**
         * 创建 [ExternalResource]. 注意, 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭.
         *
         * @param formatName 查看 [ExternalResource.formatName]
         */
        @JvmStatic
        @JvmOverloads
        @JvmName("create")
        public fun ByteArray.toExternalResource(formatName: String? = null): ExternalResource =
            ExternalResourceImplByByteArray(this, formatName)


        /**
         * 立即使用 [FileCacheStrategy] 缓存 [InputStream] 并创建 [ExternalResource].
         * 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭.
         *
         * **注意**:本函数不会关闭流.
         *
         * ### 在 Java 获得和使用 [ExternalResource] 实例
         *
         * ```
         * try(ExternalResource resource = ExternalResource.create(file)) { // 使用文件 file
         *     contact.uploadImage(resource); // 用来上传图片
         *     contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件
         * }
         * ```
         *
         * 注意, 若使用 [InputStream], 必须手动关闭 [InputStream]. 一种使用情况示例:
         *
         * ```
         * try(InputStream stream = ...) {
         *     try(ExternalResource resource = ExternalResource.create(stream)) {
         *         contact.uploadImage(resource); // 用来上传图片
         *         contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件
         *     }
         * }
         * ```
         *
         *
         * @param formatName 查看 [ExternalResource.formatName]
         * @see ExternalResource
         */
        @JvmStatic
        @JvmOverloads
        @JvmName("create")
        @Throws(IOException::class) // not in BIO context so propagate IOException
        public fun InputStream.toExternalResource(formatName: String? = null): ExternalResource =
            Mirai.FileCacheStrategy.newCache(this, formatName)

        // endregion


        /* note:
        于 2.8.0-M1 添加 (#1392)

        于 2.8.0-RC 移动至 `toExternalResource`(#1588)
         */
        @JvmName("createAutoCloseable")
        @JvmStatic
        @Deprecated(
            level = DeprecationLevel.HIDDEN,
            message = "Moved to `toExternalResource()`",
            replaceWith = ReplaceWith("resource.toAutoCloseable()"),
        )
        @DeprecatedSinceMirai(errorSince = "2.8", hiddenSince = "2.10")
        public fun createAutoCloseable(resource: ExternalResource): ExternalResource {
            return resource.toAutoCloseable()
        }

        ///////////////////////////////////////////////////////////////////////////
        // region sendAsImageTo
        ///////////////////////////////////////////////////////////////////////////

        /**
         * 将图片作为单独的消息发送给指定联系人.
         *
         * **注意**:本函数不会关闭 [ExternalResource].
         *
         * @see Contact.uploadImage 上传图片
         * @see Contact.sendMessage 最终调用, 发送消息.
         *
         * @throws OverFileSizeMaxException
         */
        @JvmBlockingBridge
        @JvmStatic
        @JvmName("sendAsImage")
        public suspend fun  ExternalResource.sendAsImageTo(contact: C): MessageReceipt =
            contact.uploadImage(this).sendTo(contact)

        /**
         * 读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人.
         *
         * 注意:本函数不会关闭流.
         *
         * @param formatName 查看 [ExternalResource.formatName]
         * @throws OverFileSizeMaxException
         */
        @JvmStatic
        @JvmBlockingBridge
        @JvmName("sendAsImage")
        @JvmOverloads
        public suspend fun  InputStream.sendAsImageTo(
            contact: C,
            formatName: String? = null,
        ): MessageReceipt =
            runBIO {
                // toExternalResource throws IOException however we're in BIO context so not propagating IOException to sendAsImageTo
                toExternalResource(formatName)
            }.withUse { sendAsImageTo(contact) }

        /**
         * 将文件作为图片发送到指定联系人.
         * @param formatName 查看 [ExternalResource.formatName]
         * @throws OverFileSizeMaxException
         */
        @JvmStatic
        @JvmBlockingBridge
        @JvmName("sendAsImage")
        @JvmOverloads
        public suspend fun  File.sendAsImageTo(contact: C, formatName: String? = null): MessageReceipt {
            require(this.exists() && this.canRead())
            return toExternalResource(formatName).withUse { sendAsImageTo(contact) }
        }

        // endregion

        ///////////////////////////////////////////////////////////////////////////
        // region uploadAsImage
        ///////////////////////////////////////////////////////////////////////////

        /**
         * 上传图片并构造 [Image]. 这个函数可能需消耗一段时间.
         *
         * **注意**:本函数不会关闭 [ExternalResource].
         *
         * @param contact 图片上传对象. 由于好友图片与群图片不通用, 上传时必须提供目标联系人.
         *
         * @see Contact.uploadImage 最终调用, 上传图片.
         */
        @JvmStatic
        @JvmBlockingBridge
        public suspend fun ExternalResource.uploadAsImage(contact: Contact): Image = contact.uploadImage(this)

        /**
         * 读取 [InputStream] 到临时文件并将其作为图片上传后构造 [Image].
         *
         * 注意:本函数不会关闭流.
         *
         * @param formatName 查看 [ExternalResource.formatName]
         * @throws OverFileSizeMaxException
         */
        @JvmStatic
        @JvmBlockingBridge
        @JvmOverloads
        public suspend fun InputStream.uploadAsImage(contact: Contact, formatName: String? = null): Image =
            // toExternalResource throws IOException however we're in BIO context so not propagating IOException to sendAsImageTo
            runBIO { toExternalResource(formatName) }.withUse { uploadAsImage(contact) }

        // endregion

        ///////////////////////////////////////////////////////////////////////////
        // region uploadAsFile
        ///////////////////////////////////////////////////////////////////////////

        /**
         * 将文件作为图片上传后构造 [Image].
         *
         * @param formatName 查看 [ExternalResource.formatName]
         * @throws OverFileSizeMaxException
         */
        @JvmStatic
        @JvmBlockingBridge
        @JvmOverloads
        public suspend fun File.uploadAsImage(contact: Contact, formatName: String? = null): Image =
            toExternalResource(formatName).withUse { uploadAsImage(contact) }

        /**
         * 上传文件并获取文件消息.
         *
         * 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型.
         *
         * 需要调用方手动[关闭资源][ExternalResource.close].
         *
         * ## 已弃用
         * 查看 [RemoteFile.upload] 获取更多信息.
         *
         * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt'
         * @since 2.5
         * @see RemoteFile.path
         * @see RemoteFile.upload
         */
        @Suppress("DEPRECATION", "DEPRECATION_ERROR")
        @JvmStatic
        @JvmBlockingBridge
        @JvmOverloads
        @Deprecated(
            "Use sendTo instead.",
            ReplaceWith(
                "this.sendTo(contact, path, callback)",
                "net.mamoe.mirai.utils.ExternalResource.Companion.sendTo"
            ),
            level = DeprecationLevel.HIDDEN
        ) // deprecated since 2.7-M1
        @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
        public suspend fun File.uploadTo(
            contact: FileSupported,
            path: String,
            callback: RemoteFile.ProgressionCallback? = null,
        ): FileMessage = toExternalResource().use {
            contact.filesRoot.resolve(path).upload(it, callback)
        }

        /**
         * 上传文件并获取文件消息.
         *
         * 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型.
         *
         * 需要调用方手动[关闭资源][ExternalResource.close].
         *
         * ## 已弃用
         * 查看 [RemoteFile.upload] 获取更多信息.
         *
         * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt'
         * @since 2.5
         * @see RemoteFile.path
         * @see RemoteFile.upload
         */
        @Suppress("DEPRECATION", "DEPRECATION_ERROR")
        @JvmStatic
        @JvmBlockingBridge
        @JvmName("uploadAsFile")
        @JvmOverloads
        @Deprecated(
            "Use sendAsFileTo instead.",
            ReplaceWith(
                "this.sendAsFileTo(contact, path, callback)",
                "net.mamoe.mirai.utils.ExternalResource.Companion.sendAsFileTo"
            ),
            level = DeprecationLevel.HIDDEN
        ) // deprecated since 2.7-M1
        @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
        public suspend fun ExternalResource.uploadAsFile(
            contact: FileSupported,
            path: String,
            callback: RemoteFile.ProgressionCallback? = null,
        ): FileMessage {
            return contact.filesRoot.resolve(path).upload(this, callback)
        }

        // endregion

        ///////////////////////////////////////////////////////////////////////////
        // region sendAsFileTo
        ///////////////////////////////////////////////////////////////////////////

        /**
         * 上传文件并发送文件消息.
         *
         * 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型.
         *
         * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt'
         * @since 2.5
         * @see RemoteFile.path
         * @see RemoteFile.uploadAndSend
         */
        @Suppress("DEPRECATION_ERROR", "DEPRECATION")
        @Deprecated(
            "Deprecated. Please use AbsoluteFolder.uploadNewFile",
            ReplaceWith("contact.files.uploadNewFile(path, this, callback)")
        ) // deprecated since 2.8.0-RC
        @JvmStatic
        @JvmBlockingBridge
        @JvmOverloads
        @DeprecatedSinceMirai(warningSince = "2.8")
        public suspend fun  File.sendTo(
            contact: C,
            path: String,
            callback: RemoteFile.ProgressionCallback? = null,
        ): MessageReceipt = toExternalResource().use {
            contact.filesRoot.resolve(path).upload(it, callback).sendTo(contact)
        }

        /**
         * 上传文件并发送件消息.  如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型.
         *
         * 需要调用方手动[关闭资源][ExternalResource.close].
         *
         * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt'
         * @since 2.5
         * @see RemoteFile.path
         * @see RemoteFile.uploadAndSend
         */
        @Suppress("DEPRECATION", "DEPRECATION_ERROR")
        @Deprecated(
            "Deprecated. Please use AbsoluteFolder.uploadNewFile",
            ReplaceWith("contact.files.uploadNewFile(path, this, callback)")
        ) // deprecated since 2.8.0-RC
        @JvmStatic
        @JvmBlockingBridge
        @JvmName("sendAsFile")
        @JvmOverloads
        @DeprecatedSinceMirai(warningSince = "2.8")
        public suspend fun  ExternalResource.sendAsFileTo(
            contact: C,
            path: String,
            callback: RemoteFile.ProgressionCallback? = null,
        ): MessageReceipt {
            return contact.filesRoot.resolve(path).upload(this, callback).sendTo(contact)
        }

        // endregion

        ///////////////////////////////////////////////////////////////////////////
        // region uploadAsVoice
        ///////////////////////////////////////////////////////////////////////////

        @Suppress("DEPRECATION", "DEPRECATION_ERROR")
        @JvmBlockingBridge
        @JvmStatic
        @Deprecated(
            "Use `contact.uploadAudio(resource)` instead",
            level = DeprecationLevel.HIDDEN
        ) // deprecated since 2.7
        @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
        public suspend fun ExternalResource.uploadAsVoice(contact: Contact): net.mamoe.mirai.message.data.Voice {
            @Suppress("DEPRECATION", "DEPRECATION_ERROR")
            if (contact is Group) return contact.uploadAudio(this).toVoice()
            else throw UnsupportedOperationException("Contact `$contact` is not supported uploading voice")
        }
        // endregion
    }
}

/**
 * 一个实现了基本方法的外部资源
 *
 * ## 实现
 *
 * [AbstractExternalResource] 实现了大部分必要的方法,
 * 只有 [ExternalResource.inputStream], [ExternalResource.size] 还未实现
 *
 * 其中 [ExternalResource.inputStream] 要求每次读取的内容都是一致的
 *
 * Example:
 * ```
 * class MyCustomExternalResource: AbstractExternalResource() {
 *      override fun inputStream0(): InputStream = FileInputStream("/test.txt")
 *      override val size: Long get() = File("/test.txt").length()
 * }
 * ```
 *
 * ## 资源释放
 *
 * 如同 mirai 内置的 [ExternalResource] 实现一样,
 * [AbstractExternalResource] 也会被注册进入资源泄露监视器
 * (即意味着 [AbstractExternalResource] 也要求手动关闭)
 *
 * 为了确保逻辑正确性, [AbstractExternalResource] 不允许覆盖其 [close] 方法,
 * 必须在构造 [AbstractExternalResource] 的时候给定一个 [ResourceCleanCallback] 以进行资源释放
 *
 * 对于 [ResourceCleanCallback], 有以下要求
 *
 * - 没有对 [AbstractExternalResource] 的访问 (即没有 [AbstractExternalResource] 的任何引用)
 *
 * Example:
 * ```
 * class MyRes(
 *      cleanup: ResourceCleanCallback,
 *      val delegate: Closable,
 * ): AbstractExternalResource(cleanup) {
 * }
 *
 * // 错误, 该写法会导致 Resource 永远也不会被自动释放
 * lateinit var myRes: MyRes
 * val cleanup = ResourceCleanCallback {
 *      myRes.delegate.close()
 * }
 * myRes = MyRes(cleanup, fetchDelegate())
 *
 * // 正确
 * val delegate: Closable
 * val cleanup = ResourceCleanCallback {
 *      delegate.close()
 * }
 * val myRes = MyRes(cleanup, delegate)
 * ```
 *
 * @since 2.9
 *
 * @see ExternalResource
 * @see AbstractExternalResource.setResourceCleanCallback
 * @see AbstractExternalResource.registerToLeakObserver
 */
@Suppress("MemberVisibilityCanBePrivate")
public abstract class AbstractExternalResource
@JvmOverloads
public constructor(
    displayName: String? = null,
    cleanup: ResourceCleanCallback? = null,
) : ExternalResource {

    public constructor(
        cleanup: ResourceCleanCallback? = null,
    ) : this(null, cleanup)

    public fun interface ResourceCleanCallback {
        @Throws(IOException::class)
        public fun cleanup()
    }

    override val md5: ByteArray by lazy { inputStream().md5() }
    override val sha1: ByteArray by lazy { inputStream().sha1() }
    override val formatName: String by lazy {
        inputStream().detectFileTypeAndClose() ?: ExternalResource.DEFAULT_FORMAT_NAME
    }

    private val leakObserverRegistered = atomic(false)

    /**
     * 注册 [ExternalResource] 资源泄露监视器
     *
     * 受限于类继承构造器调用顺序, [AbstractExternalResource] 无法做到在完成初始化后马上注册监视器
     *
     * 该方法以允许 实现类 在完成初始化后直接注册资源监视器以避免意外的资源泄露
     *
     * 在不调用本方法的前提下, 如果没有相关的资源访问操作, `this` 可能会被意外泄露
     *
     * 正确示例:
     * ```
     * // Kotlin
     * public class MyResource: AbstractExternalResource() {
     *      init {
     *          val res: SomeResource
     *          // 一些资源初始化
     *          registerToLeakObserver()
     *          setResourceCleanCallback(Releaser(res))
     *      }
     *
     *      private class Releaser(
     *          private val res: SomeResource,
     *      ) : AbstractExternalResource.ResourceCleanCallback {
     *          override fun cleanup() = res.close()
     *      }
     * }
     *
     * // Java
     * public class MyResource extends AbstractExternalResource {
     *      public MyResource() throws IOException {
     *          SomeResource res;
     *          // 一些资源初始化
     *          registerToLeakObserver();
     *          setResourceCleanCallback(new Releaser(res));
     *      }
     *
     *      private static class Releaser implements ResourceCleanCallback {
     *          private final SomeResource res;
     *          Releaser(SomeResource res) { this.res = res; }
     *
     *          public void cleanup() throws IOException { res.close(); }
     *      }
     * }
     * ```
     *
     * @see setResourceCleanCallback
     */
    protected fun registerToLeakObserver() {
        // 用户自定义 AbstractExternalResource 也许会在  的时候失败
        // 于是在第一次使用 ExternalResource 相关的函数的时候注册 LeakObserver
        if (leakObserverRegistered.compareAndSet(expect = false, update = true)) {
            ExternalResourceLeakObserver.register(this, holder)
        }
    }

    /**
     * 该方法用于告知 [AbstractExternalResource] 不需要注册资源泄露监视器。
     * **仅在我知道我在干什么的前提下调用此方法**
     *
     * 不建议取消注册监视器, 这可能带来意外的错误
     *
     * @see registerToLeakObserver
     */
    protected fun dontRegisterLeakObserver() {
        leakObserverRegistered.value = true
    }

    final override fun inputStream(): InputStream {
        registerToLeakObserver()
        return inputStream0()
    }

    protected abstract fun inputStream0(): InputStream

    /**
     * 修改 `this` 的资源释放回调。
     * **仅在我知道我在干什么的前提下调用此方法**
     *
     * ```
     * class MyRes {
     * // region kotlin
     *
     *      private inner class Releaser : ResourceCleanCallback
     *
     *      private class NotInnerReleaser : ResourceCleanCallback
     *
     *      init {
     *          // 错误, 内部类, Releaser 存在对 MyRes 的引用
     *          setResourceCleanCallback(Releaser())
     *          // 错误, 匿名对象, 可能存在对 MyRes 的引用, 取决于编译器
     *          setResourceCleanCallback(object : ResourceCleanCallback {})
     *          // 正确, 无 inner 修饰, 等同于 java 的 private static class
     *          setResourceCleanCallback(NotInnerReleaser(directResource))
     *      }
     *
     * // endregion kotlin
     *
     * // region java
     *
     *      private class Releaser implements ResourceCleanCallback {}
     *      private static class StaticReleaser implements ResourceCleanCallback {}
     *
     *      MyRes() {
     *          // 错误, 内部类, 存在对 MyRes 的引用
     *          setResourceCleanCallback(new Releaser());
     *          // 错误, 匿名对象, 可能存在对 MyRes 的引用, 取决于 javac
     *          setResourceCleanCallback(new ResourceCleanCallback() {});
     *          // 正确
     *          setResourceCleanCallback(new StaticReleaser(directResource));
     *      }
     *
     * // endregion java
     * }
     * ```
     *
     * @see registerToLeakObserver
     */
    protected fun setResourceCleanCallback(cleanup: ResourceCleanCallback?) {
        holder.cleanup = cleanup
    }

    private class UsrCustomResHolder(
        @JvmField var cleanup: ResourceCleanCallback?,
        private val resourceName: String,
    ) : ExternalResourceHolder() {

        override val closed: Deferred = CompletableDeferred()

        override fun closeImpl() {
            cleanup?.cleanup()
        }

        // display on logger of ExternalResourceLeakObserver
        override fun toString(): String = resourceName
    }

    private val holder = UsrCustomResHolder(cleanup, displayName ?: buildString {
        append("ExternalResourceHolder<")
        append([email protected])
        append('@')
        append(System.identityHashCode(this@AbstractExternalResource))
        append('>')
    })

    final override val closed: Deferred get() = holder.closed.also { registerToLeakObserver() }

    @Throws(IOException::class)
    final override fun close() {
        holder.close()
    }
}

/**
 * 执行 [action], 如果 [ExternalResource.isAutoClose], 在执行完成后调用 [ExternalResource.close].
 *
 * @since 2.8
 */
@MiraiExperimentalApi
// Continuing mark it as experimental until Kotlin's contextual receivers design is published.
// We might be able to make `action` a type `context(ExternalResource) () -> R`.
public inline fun  T.withAutoClose(action: () -> R): R {
    contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
    trySafely(
        block = { return action() },
        finally = { if (isAutoClose) close() }
    )
}

/**
 * 执行 [action], 如果 [ExternalResource.isAutoClose], 在执行完成后调用 [ExternalResource.close].
 *
 * @since 2.8
 */
@MiraiExperimentalApi
public inline fun  T.runAutoClose(action: T.() -> R): R {
    contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
    return withAutoClose { action() }
}

/**
 * 执行 [action], 如果 [ExternalResource.isAutoClose], 在执行完成后调用 [ExternalResource.close].
 *
 * @since 2.8
 */
@MiraiExperimentalApi
public inline fun  T.useAutoClose(action: (resource: T) -> R): R {
    contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
    return runAutoClose(action)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy