ExternalResource.kt 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. /*
  2. * Copyright 2019-2022 Mamoe Technologies and contributors.
  3. *
  4. * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
  5. * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
  6. *
  7. * https://github.com/mamoe/mirai/blob/dev/LICENSE
  8. */
  9. @file:Suppress("EXPERIMENTAL_API_USAGE", "unused")
  10. package net.mamoe.mirai.utils
  11. import io.ktor.utils.io.core.*
  12. import kotlinx.coroutines.CompletableDeferred
  13. import kotlinx.coroutines.Deferred
  14. import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
  15. import net.mamoe.mirai.contact.Contact
  16. import net.mamoe.mirai.contact.Contact.Companion.sendImage
  17. import net.mamoe.mirai.contact.Contact.Companion.uploadImage
  18. import net.mamoe.mirai.internal.utils.*
  19. import net.mamoe.mirai.message.MessageReceipt
  20. import net.mamoe.mirai.message.data.Image
  21. import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo
  22. import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
  23. import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
  24. import kotlin.contracts.InvocationKind
  25. import kotlin.contracts.contract
  26. import kotlin.jvm.JvmName
  27. import kotlin.jvm.JvmOverloads
  28. import kotlin.jvm.JvmStatic
  29. /**
  30. * 一个*不可变的*外部资源. 仅包含资源内容, 大小, 文件类型, 校验值而不包含文件名, 文件位置等. 外部资源有可能是一个文件, 也有可能只存在于内存, 或者以任意其他方式实现.
  31. *
  32. * [ExternalResource] 在创建之后就应该保持其属性的不变, 即任何时候获取其属性都应该得到相同结果, 任何时候打开流都得到的一样的数据.
  33. *
  34. * # 创建
  35. * - [File.toExternalResource]
  36. * - [RandomAccessFile.toExternalResource]
  37. * - [ByteArray.toExternalResource]
  38. * - [InputStream.toExternalResource]
  39. *
  40. * ## 在 Kotlin 获得和使用 [ExternalResource] 实例
  41. *
  42. * ```
  43. * file.toExternalResource().use { resource -> // 安全地使用资源
  44. * contact.uploadImage(resource) // 用来上传图片
  45. * contact.files.uploadNewFile("/foo/test.txt", file) // 或者用来上传文件
  46. * }
  47. * ```
  48. *
  49. * 注意, 若使用 [InputStream], 必须手动关闭 [InputStream]. 一种使用情况示例:
  50. *
  51. * ```
  52. * inputStream.use { input -> // 安全地使用 InputStream
  53. * input.toExternalResource().use { resource -> // 安全地使用资源
  54. * contact.uploadImage(resource) // 用来上传图片
  55. * contact.files.uploadNewFile("/foo/test.txt", file) // 或者用来上传文件
  56. * }
  57. * }
  58. * ```
  59. *
  60. * ## 在 Java 获得和使用 [ExternalResource] 实例
  61. *
  62. * ```
  63. * try (ExternalResource resource = ExternalResource.create(file)) { // 使用文件 file
  64. * contact.uploadImage(resource); // 用来上传图片
  65. * contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件
  66. * }
  67. * ```
  68. *
  69. * 注意, 若使用 [InputStream], 必须手动关闭 [InputStream]. 一种使用情况示例:
  70. *
  71. * ```java
  72. * try (InputStream stream = ...) { // 安全地使用 InputStream
  73. * try (ExternalResource resource = ExternalResource.create(stream)) { // 安全地使用资源
  74. * contact.uploadImage(resource); // 用来上传图片
  75. * contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件
  76. * }
  77. * }
  78. * ```
  79. *
  80. * # 释放
  81. *
  82. * 当 [ExternalResource] 创建时就可能会打开一个文件 (如使用 [File.toExternalResource]).
  83. * 类似于 [InputStream], [ExternalResource] 需要被 [关闭][close].
  84. *
  85. * ## 未释放资源的补救策略
  86. *
  87. * 自 2.7 起, 每个 mirai 内置的 [ExternalResource] 实现都有引用跟踪, 当 [ExternalResource] 被 GC 后会执行被动释放.
  88. * 这依赖于 JVM 垃圾收集策略, 因此不可靠, 资源仍然需要手动 close.
  89. *
  90. * ## 使用单次自动释放
  91. *
  92. * 若创建的资源仅需要*很快地*使用一次, 可使用 [toAutoCloseable] 获得在使用一次后就会自动关闭的资源.
  93. *
  94. * 示例:
  95. * ```java
  96. * contact.uploadImage(ExternalResource.create(file).toAutoCloseable()); // 创建并立即使用单次自动释放的资源
  97. * ```
  98. *
  99. * **注意**: 如果仅使用 [toAutoCloseable] 而不通过 [Contact.uploadImage] 等 mirai 内置方法使用资源, 资源仍然会处于打开状态且不会被自动关闭.
  100. * 最终资源会由上述*未释放资源的补救策略*关闭, 但这依赖于 JVM 垃圾收集策略而不可靠.
  101. * 因此建议在创建单次自动释放的资源后就尽快使用它, 否则仍然需要考虑在正确的时间及时关闭资源.
  102. *
  103. * # 实现 [ExternalResource]
  104. *
  105. * 可以自行实现 [ExternalResource]. 但通常上述创建方法已足够使用.
  106. *
  107. * 建议继承 [AbstractExternalResource], 这将支持上文提到的资源自动释放功能.
  108. *
  109. * 实现时需保持 [ExternalResource] 在构造后就不可变, 并且所有属性都总是返回一个固定值.
  110. *
  111. * @see ExternalResource.uploadAsImage 将资源作为图片上传, 得到 [Image]
  112. * @see ExternalResource.sendAsImageTo 将资源作为图片发送
  113. * @see Contact.uploadImage 上传一个资源作为图片, 得到 [Image]
  114. * @see Contact.sendImage 发送一个资源作为图片
  115. *
  116. * @see FileCacheStrategy
  117. */
  118. public expect interface ExternalResource : Closeable {
  119. /**
  120. * 是否在 _使用一次_ 后自动 [close].
  121. *
  122. * 该属性仅供调用方参考. 如 [Contact.uploadImage] 会在方法结束时关闭 [isAutoClose] 为 `true` 的 [ExternalResource], 无论上传图片是否成功.
  123. *
  124. * 所有 mirai 内置的上传图片, 上传语音等方法都支持该行为.
  125. *
  126. * @since 2.8
  127. */
  128. @MiraiExperimentalApi
  129. public open val isAutoClose: Boolean
  130. /**
  131. * 文件内容 MD5. 16 bytes
  132. */
  133. public val md5: ByteArray
  134. /**
  135. * 文件内容 SHA1. 16 bytes
  136. * @since 2.5
  137. */
  138. public open val sha1: ByteArray
  139. /**
  140. * 文件格式,如 "png", "amr". 当无法自动识别格式时为 [DEFAULT_FORMAT_NAME].
  141. *
  142. * 默认会从文件头识别, 支持的文件类型:
  143. * png, jpg, gif, tif, bmp, amr, silk
  144. *
  145. * @see net.mamoe.mirai.utils.getFileType
  146. * @see net.mamoe.mirai.utils.FILE_TYPES
  147. * @see DEFAULT_FORMAT_NAME
  148. */
  149. public val formatName: String
  150. /**
  151. * 文件大小 bytes
  152. */
  153. public val size: Long
  154. /**
  155. * 当 [close] 时会 [CompletableDeferred.complete] 的 [Deferred].
  156. */
  157. public val closed: Deferred<Unit>
  158. /**
  159. * 打开 [Input]. 在返回的 [Input] 被 [关闭][Input.close] 前无法再次打开流.
  160. *
  161. * 关闭此流不会关闭 [ExternalResource].
  162. * @throws IllegalStateException 当上一个流未关闭又尝试打开新的流时抛出
  163. *
  164. * @since SINCE_NATIVE_TARGET
  165. */
  166. public fun input(): Input
  167. @MiraiInternalApi
  168. public open fun calculateResourceId(): String
  169. /**
  170. * 该 [ExternalResource] 的数据来源, 可能有以下的返回
  171. *
  172. * - [File] 本地文件
  173. * - [java.nio.file.Path] 某个具体文件路径
  174. * - [java.nio.ByteBuffer] RAM
  175. * - [java.net.URI] uri
  176. * - [ByteArray] RAM
  177. * - Or more...
  178. *
  179. * implementation note:
  180. *
  181. * - 对于无法二次读取的数据来源 (如 [InputStream]), 返回 `null`
  182. * - 对于一个来自网络的资源, 请返回 [java.net.URI] (not URL, 或者其他库的 URI/URL 类型)
  183. * - 不要返回 [String], 没有约定 [String] 代表什么
  184. * - 数据源外漏会严重影响 [inputStream] 等的执行的可以返回 `null` (如 [RandomAccessFile])
  185. *
  186. * @since 2.8.0
  187. */
  188. public open val origin: Any?
  189. /**
  190. * 创建一个在 _使用一次_ 后就会自动 [close] 的 [ExternalResource].
  191. *
  192. * @since 2.8.0
  193. */
  194. public open fun toAutoCloseable(): ExternalResource
  195. public companion object {
  196. /**
  197. * 在无法识别文件格式时使用的默认格式名. "mirai".
  198. *
  199. * @see ExternalResource.formatName
  200. */
  201. public val DEFAULT_FORMAT_NAME: String
  202. ///////////////////////////////////////////////////////////////////////////
  203. // region toExternalResource
  204. ///////////////////////////////////////////////////////////////////////////
  205. /**
  206. * 创建 [ExternalResource]. 注意, 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭.
  207. *
  208. * @param formatName 查看 [ExternalResource.formatName]
  209. */
  210. @JvmStatic
  211. @JvmOverloads
  212. @JvmName("create")
  213. public fun ByteArray.toExternalResource(formatName: String? = null): ExternalResource
  214. // endregion
  215. ///////////////////////////////////////////////////////////////////////////
  216. // region sendAsImageTo
  217. ///////////////////////////////////////////////////////////////////////////
  218. /**
  219. * 将图片作为单独的消息发送给指定联系人.
  220. *
  221. * **注意**:本函数不会关闭 [ExternalResource].
  222. *
  223. * @see Contact.uploadImage 上传图片
  224. * @see Contact.sendMessage 最终调用, 发送消息.
  225. *
  226. * @throws OverFileSizeMaxException
  227. */
  228. @JvmBlockingBridge
  229. @JvmStatic
  230. @JvmName("sendAsImage")
  231. public suspend fun <C : Contact> ExternalResource.sendAsImageTo(contact: C): MessageReceipt<C>
  232. // endregion
  233. ///////////////////////////////////////////////////////////////////////////
  234. // region uploadAsImage
  235. ///////////////////////////////////////////////////////////////////////////
  236. /**
  237. * 上传图片并构造 [Image]. 这个函数可能需消耗一段时间.
  238. *
  239. * **注意**:本函数不会关闭 [ExternalResource].
  240. *
  241. * @param contact 图片上传对象. 由于好友图片与群图片不通用, 上传时必须提供目标联系人.
  242. *
  243. * @see Contact.uploadImage 最终调用, 上传图片.
  244. */
  245. @JvmStatic
  246. @JvmBlockingBridge
  247. public suspend fun ExternalResource.uploadAsImage(contact: Contact): Image
  248. // endregion
  249. }
  250. }
  251. /**
  252. * 执行 [action], 如果 [ExternalResource.isAutoClose], 在执行完成后调用 [ExternalResource.close].
  253. *
  254. * @since 2.8
  255. */
  256. @MiraiExperimentalApi
  257. // Continuing mark it as experimental until Kotlin's contextual receivers design is published.
  258. // We might be able to make `action` a type `context(ExternalResource) () -> R`.
  259. public inline fun <T : ExternalResource, R> T.withAutoClose(action: () -> R): R {
  260. contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
  261. trySafely(
  262. block = { return action() },
  263. finally = { if (isAutoClose) close() }
  264. )
  265. }
  266. /**
  267. * 执行 [action], 如果 [ExternalResource.isAutoClose], 在执行完成后调用 [ExternalResource.close].
  268. *
  269. * @since 2.8
  270. */
  271. @MiraiExperimentalApi
  272. public inline fun <T : ExternalResource, R> T.runAutoClose(action: T.() -> R): R {
  273. contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
  274. return withAutoClose { action() }
  275. }
  276. /**
  277. * 执行 [action], 如果 [ExternalResource.isAutoClose], 在执行完成后调用 [ExternalResource.close].
  278. *
  279. * @since 2.8
  280. */
  281. @MiraiExperimentalApi
  282. public inline fun <T : ExternalResource, R> T.useAutoClose(action: (resource: T) -> R): R {
  283. contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
  284. return runAutoClose(action)
  285. }