ExternalResource.kt 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. /*
  2. * Copyright 2019-2021 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 kotlinx.coroutines.CompletableDeferred
  12. import kotlinx.coroutines.Deferred
  13. import net.mamoe.kjbb.JvmBlockingBridge
  14. import net.mamoe.mirai.Mirai
  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.contact.FileSupported
  19. import net.mamoe.mirai.contact.Group
  20. import net.mamoe.mirai.internal.utils.ExternalResourceImplByByteArray
  21. import net.mamoe.mirai.internal.utils.ExternalResourceImplByFile
  22. import net.mamoe.mirai.message.MessageReceipt
  23. import net.mamoe.mirai.message.data.FileMessage
  24. import net.mamoe.mirai.message.data.Image
  25. import net.mamoe.mirai.message.data.sendTo
  26. import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo
  27. import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
  28. import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
  29. import net.mamoe.mirai.utils.RemoteFile.Companion.sendFile
  30. import net.mamoe.mirai.utils.RemoteFile.Companion.uploadFile
  31. import java.io.*
  32. import kotlin.contracts.InvocationKind
  33. import kotlin.contracts.contract
  34. /**
  35. * 一个*不可变的*外部资源. 仅包含资源内容, 大小, 文件类型, 校验值而不包含文件名, 文件位置等. 外部资源有可能是一个文件, 也有可能只存在于内存, 或者以任意其他方式实现.
  36. *
  37. * [ExternalResource] 在创建之后就应该保持其属性的不变, 即任何时候获取其属性都应该得到相同结果, 任何时候打开流都得到的一样的数据.
  38. *
  39. * ## 创建
  40. * - [File.toExternalResource]
  41. * - [RandomAccessFile.toExternalResource]
  42. * - [ByteArray.toExternalResource]
  43. * - [InputStream.toExternalResource]
  44. *
  45. * ## 释放
  46. *
  47. * 当 [ExternalResource] 创建时就可能会打开一个文件 (如使用 [File.toExternalResource]).
  48. * 类似于 [InputStream], [ExternalResource] 需要被 [关闭][close].
  49. *
  50. * 自 2.7 起, 每个 mirai 内置的 [ExternalResource] 实现都有引用跟踪, 当 [ExternalResource] 被 GC 后会执行被动释放, 但是该策略并不代表不需要手动 close.
  51. *
  52. * ## 实现 [ExternalResource]
  53. *
  54. * 可以自行实现 [ExternalResource]. 但通常上述创建方法已足够使用.
  55. *
  56. * 实现时需保持 [ExternalResource] 在构造后就不可变, 并且所有属性都总是返回一个固定值.
  57. *
  58. * @see ExternalResource.uploadAsImage 将资源作为图片上传, 得到 [Image]
  59. * @see ExternalResource.sendAsImageTo 将资源作为图片发送
  60. * @see Contact.uploadImage 上传一个资源作为图片, 得到 [Image]
  61. * @see Contact.sendImage 发送一个资源作为图片
  62. *
  63. * @see FileCacheStrategy
  64. */
  65. public interface ExternalResource : Closeable {
  66. /**
  67. * 是否在 _使用一次_ 后自动 [close].
  68. *
  69. * 该属性仅供调用方参考. 如 [Contact.uploadImage] 会在方法结束时关闭 [isAutoClose] 为 `true` 的 [ExternalResource], 无论上传图片是否成功.
  70. *
  71. * 所有 mirai 内置的上传图片, 上传语音等方法都支持该行为.
  72. *
  73. * @since 2.8
  74. */
  75. @MiraiExperimentalApi
  76. public val isAutoClose: Boolean
  77. get() = false
  78. /**
  79. * 文件内容 MD5. 16 bytes
  80. */
  81. public val md5: ByteArray
  82. /**
  83. * 文件内容 SHA1. 16 bytes
  84. * @since 2.5
  85. */
  86. public val sha1: ByteArray
  87. get() =
  88. throw UnsupportedOperationException("ExternalResource.sha1 is not implemented by ${this::class.simpleName}")
  89. // 如果你要实现 [ExternalResource], 你也应该实现 [sha1].
  90. // 这里默认抛出 [UnsupportedOperationException] 是为了 (姑且) 兼容 2.5 以前的版本的实现.
  91. /**
  92. * 文件格式,如 "png", "amr". 当无法自动识别格式时为 [DEFAULT_FORMAT_NAME].
  93. *
  94. * 默认会从文件头识别, 支持的文件类型:
  95. * png, jpg, gif, tif, bmp, amr, silk
  96. *
  97. * @see net.mamoe.mirai.utils.getFileType
  98. * @see net.mamoe.mirai.utils.FILE_TYPES
  99. * @see DEFAULT_FORMAT_NAME
  100. */
  101. public val formatName: String
  102. /**
  103. * 文件大小 bytes
  104. */
  105. public val size: Long
  106. /**
  107. * 当 [close] 时会 [CompletableDeferred.complete] 的 [Deferred].
  108. */
  109. public val closed: Deferred<Unit>
  110. /**
  111. * 打开 [InputStream]. 在返回的 [InputStream] 被 [关闭][InputStream.close] 前无法再次打开流.
  112. *
  113. * 关闭此流不会关闭 [ExternalResource].
  114. * @throws IllegalStateException 当上一个流未关闭又尝试打开新的流时抛出
  115. */
  116. public fun inputStream(): InputStream
  117. @MiraiInternalApi
  118. public fun calculateResourceId(): String {
  119. return generateImageId(md5, formatName.ifEmpty { DEFAULT_FORMAT_NAME })
  120. }
  121. /**
  122. * 该 [ExternalResource] 的数据来源, 可能有以下的返回
  123. *
  124. * - [File] 本地文件
  125. * - [java.nio.file.Path] 某个具体文件路径
  126. * - [java.nio.ByteBuffer] RAM
  127. * - [java.net.URI] uri
  128. * - [ByteArray] RAM
  129. * - Or more...
  130. *
  131. * implementation note:
  132. *
  133. * - 对于无法二次读取的数据来源 (如 [InputStream]), 返回 `null`
  134. * - 对于一个来自网络的资源, 请返回 [java.net.URI] (not URL, 或者其他库的 URI/URL 类型)
  135. * - 不要返回 [String], 没有约定 [String] 代表什么
  136. * - 数据源外漏会严重影响 [inputStream] 等的执行的可以返回 `null` (如 [RandomAccessFile])
  137. *
  138. * @since TODO
  139. */
  140. public val origin: Any? get() = null
  141. public companion object {
  142. /**
  143. * 在无法识别文件格式时使用的默认格式名. "mirai".
  144. *
  145. * @see ExternalResource.formatName
  146. */
  147. public const val DEFAULT_FORMAT_NAME: String = "mirai"
  148. ///////////////////////////////////////////////////////////////////////////
  149. // region toExternalResource
  150. ///////////////////////////////////////////////////////////////////////////
  151. /**
  152. * **打开文件**并创建 [ExternalResource].
  153. *
  154. * 将以只读模式打开这个文件 (因此文件会处于被占用状态), 直到 [ExternalResource.close].
  155. *
  156. * @param formatName 查看 [ExternalResource.formatName]
  157. */
  158. @JvmStatic
  159. @JvmOverloads
  160. @JvmName("create")
  161. public fun File.toExternalResource(formatName: String? = null): ExternalResource =
  162. // although RandomAccessFile constructor throws IOException, actual performance influence is minor so not propagating IOException
  163. RandomAccessFile(this, "r").toExternalResource(formatName).also {
  164. it.cast<ExternalResourceImplByFile>().origin = this@toExternalResource
  165. }
  166. /**
  167. * 创建 [ExternalResource].
  168. *
  169. * **注意**:使用此方法时请不要关闭 [RandomAccessFile], 否则会间接关闭 [ExternalResource]
  170. *
  171. * @see closeOriginalFileOnClose 若为 `true`, 在 [ExternalResource.close] 时将会同步关闭 [RandomAccessFile]. 否则不会.
  172. *
  173. * @param formatName 查看 [ExternalResource.formatName]
  174. */
  175. @JvmStatic
  176. @JvmOverloads
  177. @JvmName("create")
  178. public fun RandomAccessFile.toExternalResource(
  179. formatName: String? = null,
  180. closeOriginalFileOnClose: Boolean = true,
  181. ): ExternalResource =
  182. ExternalResourceImplByFile(this, formatName, closeOriginalFileOnClose)
  183. /**
  184. * 创建 [ExternalResource].
  185. *
  186. * @param formatName 查看 [ExternalResource.formatName]
  187. */
  188. @JvmStatic
  189. @JvmOverloads
  190. @JvmName("create")
  191. public fun ByteArray.toExternalResource(formatName: String? = null): ExternalResource =
  192. ExternalResourceImplByByteArray(this, formatName)
  193. /**
  194. * 立即使用 [FileCacheStrategy] 缓存 [InputStream] 并创建 [ExternalResource].
  195. *
  196. * **注意**:本函数不会关闭流.
  197. *
  198. * @param formatName 查看 [ExternalResource.formatName]
  199. */
  200. @JvmStatic
  201. @JvmOverloads
  202. @JvmName("create")
  203. @Throws(IOException::class) // not in BIO context so propagate IOException
  204. public fun InputStream.toExternalResource(formatName: String? = null): ExternalResource =
  205. Mirai.FileCacheStrategy.newCache(this, formatName)
  206. /**
  207. * 创建一个在 _使用一次_ 后就会自动 [close] 的 [ExternalResource].
  208. *
  209. * @since 2.8
  210. */
  211. @JvmName("createAutoCloseable")
  212. @JvmStatic
  213. public fun ExternalResource.toAutoCloseable(): ExternalResource {
  214. return if (isAutoClose) this else {
  215. val delegate = this
  216. object : ExternalResource by delegate {
  217. override val isAutoClose: Boolean get() = true
  218. override fun toString(): String = "ExternalResourceWithAutoClose(delegate=$delegate)"
  219. }
  220. }
  221. }
  222. // endregion
  223. ///////////////////////////////////////////////////////////////////////////
  224. // region sendAsImageTo
  225. ///////////////////////////////////////////////////////////////////////////
  226. /**
  227. * 将图片作为单独的消息发送给指定联系人.
  228. *
  229. * **注意**:本函数不会关闭 [ExternalResource].
  230. *
  231. * @see Contact.uploadImage 上传图片
  232. * @see Contact.sendMessage 最终调用, 发送消息.
  233. *
  234. * @throws OverFileSizeMaxException
  235. */
  236. @JvmBlockingBridge
  237. @JvmStatic
  238. @JvmName("sendAsImage")
  239. public suspend fun <C : Contact> ExternalResource.sendAsImageTo(contact: C): MessageReceipt<C> =
  240. contact.uploadImage(this).sendTo(contact)
  241. /**
  242. * 读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人.
  243. *
  244. * 注意:本函数不会关闭流.
  245. *
  246. * @param formatName 查看 [ExternalResource.formatName]
  247. * @throws OverFileSizeMaxException
  248. */
  249. @JvmStatic
  250. @JvmBlockingBridge
  251. @JvmName("sendAsImage")
  252. @JvmOverloads
  253. public suspend fun <C : Contact> InputStream.sendAsImageTo(
  254. contact: C,
  255. formatName: String? = null,
  256. ): MessageReceipt<C> =
  257. runBIO {
  258. // toExternalResource throws IOException however we're in BIO context so not propagating IOException to sendAsImageTo
  259. toExternalResource(formatName)
  260. }.withUse { sendAsImageTo(contact) }
  261. /**
  262. * 将文件作为图片发送到指定联系人.
  263. * @param formatName 查看 [ExternalResource.formatName]
  264. * @throws OverFileSizeMaxException
  265. */
  266. @JvmStatic
  267. @JvmBlockingBridge
  268. @JvmName("sendAsImage")
  269. @JvmOverloads
  270. public suspend fun <C : Contact> File.sendAsImageTo(contact: C, formatName: String? = null): MessageReceipt<C> {
  271. require(this.exists() && this.canRead())
  272. return toExternalResource(formatName).withUse { sendAsImageTo(contact) }
  273. }
  274. // endregion
  275. ///////////////////////////////////////////////////////////////////////////
  276. // region uploadAsImage
  277. ///////////////////////////////////////////////////////////////////////////
  278. /**
  279. * 上传图片并构造 [Image]. 这个函数可能需消耗一段时间.
  280. *
  281. * **注意**:本函数不会关闭 [ExternalResource].
  282. *
  283. * @param contact 图片上传对象. 由于好友图片与群图片不通用, 上传时必须提供目标联系人.
  284. *
  285. * @see Contact.uploadImage 最终调用, 上传图片.
  286. */
  287. @JvmStatic
  288. @JvmBlockingBridge
  289. public suspend fun ExternalResource.uploadAsImage(contact: Contact): Image = contact.uploadImage(this)
  290. /**
  291. * 读取 [InputStream] 到临时文件并将其作为图片上传后构造 [Image].
  292. *
  293. * 注意:本函数不会关闭流.
  294. *
  295. * @param formatName 查看 [ExternalResource.formatName]
  296. * @throws OverFileSizeMaxException
  297. */
  298. @JvmStatic
  299. @JvmBlockingBridge
  300. @JvmOverloads
  301. public suspend fun InputStream.uploadAsImage(contact: Contact, formatName: String? = null): Image =
  302. // toExternalResource throws IOException however we're in BIO context so not propagating IOException to sendAsImageTo
  303. runBIO { toExternalResource(formatName) }.withUse { uploadAsImage(contact) }
  304. // endregion
  305. ///////////////////////////////////////////////////////////////////////////
  306. // region uploadAsFile
  307. ///////////////////////////////////////////////////////////////////////////
  308. /**
  309. * 将文件作为图片上传后构造 [Image].
  310. *
  311. * @param formatName 查看 [ExternalResource.formatName]
  312. * @throws OverFileSizeMaxException
  313. */
  314. @JvmStatic
  315. @JvmBlockingBridge
  316. @JvmOverloads
  317. public suspend fun File.uploadAsImage(contact: Contact, formatName: String? = null): Image =
  318. toExternalResource(formatName).withUse { uploadAsImage(contact) }
  319. /**
  320. * 上传文件并获取文件消息.
  321. *
  322. * 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型.
  323. *
  324. * 需要调用方手动[关闭资源][ExternalResource.close].
  325. *
  326. * ## 已弃用
  327. * 查看 [RemoteFile.upload] 获取更多信息.
  328. *
  329. * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt'
  330. * @since 2.5
  331. * @see RemoteFile.path
  332. * @see RemoteFile.upload
  333. */
  334. @Suppress("DEPRECATION")
  335. @JvmStatic
  336. @JvmBlockingBridge
  337. @JvmOverloads
  338. @Deprecated(
  339. "Use sendTo instead.",
  340. ReplaceWith(
  341. "this.sendTo(contact, path, callback)",
  342. "net.mamoe.mirai.utils.ExternalResource.Companion.sendTo"
  343. ),
  344. level = DeprecationLevel.WARNING
  345. ) // deprecated since 2.7-M1
  346. public suspend fun File.uploadTo(
  347. contact: FileSupported,
  348. path: String,
  349. callback: RemoteFile.ProgressionCallback? = null,
  350. ): FileMessage = toExternalResource().use { contact.uploadFile(path, it, callback) }
  351. /**
  352. * 上传文件并获取文件消息.
  353. *
  354. * 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型.
  355. *
  356. * 需要调用方手动[关闭资源][ExternalResource.close].
  357. *
  358. * ## 已弃用
  359. * 查看 [RemoteFile.upload] 获取更多信息.
  360. *
  361. * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt'
  362. * @since 2.5
  363. * @see RemoteFile.path
  364. * @see RemoteFile.upload
  365. */
  366. @Suppress("DEPRECATION")
  367. @JvmStatic
  368. @JvmBlockingBridge
  369. @JvmName("uploadAsFile")
  370. @JvmOverloads
  371. @Deprecated(
  372. "Use sendAsFileTo instead.",
  373. ReplaceWith(
  374. "this.sendAsFileTo(contact, path, callback)",
  375. "net.mamoe.mirai.utils.ExternalResource.Companion.sendAsFileTo"
  376. ),
  377. level = DeprecationLevel.WARNING
  378. ) // deprecated since 2.7-M1
  379. public suspend fun ExternalResource.uploadAsFile(
  380. contact: FileSupported,
  381. path: String,
  382. callback: RemoteFile.ProgressionCallback? = null,
  383. ): FileMessage = contact.uploadFile(path, this, callback)
  384. // endregion
  385. ///////////////////////////////////////////////////////////////////////////
  386. // region sendAsFileTo
  387. ///////////////////////////////////////////////////////////////////////////
  388. /**
  389. * 上传文件并发送文件消息.
  390. *
  391. * 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型.
  392. *
  393. * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt'
  394. * @since 2.5
  395. * @see RemoteFile.path
  396. * @see RemoteFile.uploadAndSend
  397. */
  398. @JvmStatic
  399. @JvmBlockingBridge
  400. @JvmOverloads
  401. public suspend fun <C : FileSupported> File.sendTo(
  402. contact: C,
  403. path: String,
  404. callback: RemoteFile.ProgressionCallback? = null,
  405. ): MessageReceipt<C> = toExternalResource().use { contact.sendFile(path, it, callback) }
  406. /**
  407. * 上传文件并发送件消息. 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型.
  408. *
  409. * 需要调用方手动[关闭资源][ExternalResource.close].
  410. *
  411. * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt'
  412. * @since 2.5
  413. * @see RemoteFile.path
  414. * @see RemoteFile.uploadAndSend
  415. */
  416. @JvmStatic
  417. @JvmBlockingBridge
  418. @JvmName("sendAsFile")
  419. @JvmOverloads
  420. public suspend fun <C : FileSupported> ExternalResource.sendAsFileTo(
  421. contact: C,
  422. path: String,
  423. callback: RemoteFile.ProgressionCallback? = null,
  424. ): MessageReceipt<C> = contact.sendFile(path, this, callback)
  425. // endregion
  426. ///////////////////////////////////////////////////////////////////////////
  427. // region uploadAsVoice
  428. ///////////////////////////////////////////////////////////////////////////
  429. @Suppress("DEPRECATION")
  430. @JvmBlockingBridge
  431. @JvmStatic
  432. @Deprecated(
  433. "Use `contact.uploadAudio(resource)` instead",
  434. level = DeprecationLevel.WARNING
  435. ) // deprecated since 2.7
  436. public suspend fun ExternalResource.uploadAsVoice(contact: Contact): net.mamoe.mirai.message.data.Voice {
  437. @Suppress("DEPRECATION")
  438. if (contact is Group) return contact.uploadVoice(this)
  439. else throw UnsupportedOperationException("Contact `$contact` is not supported uploading voice")
  440. }
  441. // endregion
  442. }
  443. }
  444. /**
  445. * 执行 [action], 如果 [ExternalResource.isAutoClose], 在执行完成后调用 [ExternalResource.close].
  446. *
  447. * @since 2.8
  448. */
  449. @MiraiExperimentalApi
  450. // Continuing mark it as experimental until Kotlin's contextual receivers design is published.
  451. // We might be able to make `action` a type `context(ExternalResource) () -> R`.
  452. public inline fun <T : ExternalResource, R> T.withAutoClose(action: () -> R): R {
  453. contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
  454. trySafely(
  455. block = { return action() },
  456. finally = { if (isAutoClose) close() }
  457. )
  458. }
  459. /**
  460. * 执行 [action], 如果 [ExternalResource.isAutoClose], 在执行完成后调用 [ExternalResource.close].
  461. *
  462. * @since 2.8
  463. */
  464. @MiraiExperimentalApi
  465. public inline fun <T : ExternalResource, R> T.runAutoClose(action: T.() -> R): R {
  466. contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
  467. return withAutoClose { action() }
  468. }
  469. /**
  470. * 执行 [action], 如果 [ExternalResource.isAutoClose], 在执行完成后调用 [ExternalResource.close].
  471. *
  472. * @since 2.8
  473. */
  474. @MiraiExperimentalApi
  475. public inline fun <T : ExternalResource, R> T.useAutoClose(action: (resource: T) -> R): R {
  476. contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
  477. return runAutoClose(action)
  478. }