commonMain.utils.RemoteFileImpl.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mirai-core-jvm Show documentation
Show all versions of mirai-core-jvm Show documentation
Mirai Protocol implementation for QQ Android
/*
* 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("DEPRECATION", "OverridingDeprecatedMember")
package net.mamoe.mirai.internal.utils
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.isOperator
import net.mamoe.mirai.internal.asQQAndroidBot
import net.mamoe.mirai.internal.contact.groupCode
import net.mamoe.mirai.internal.message.data.FileMessageImpl
import net.mamoe.mirai.internal.message.flags.AllowSendFileMessage
import net.mamoe.mirai.internal.network.highway.Highway
import net.mamoe.mirai.internal.network.highway.ResourceKind
import net.mamoe.mirai.internal.network.protocol
import net.mamoe.mirai.internal.network.protocol.data.proto.*
import net.mamoe.mirai.internal.network.protocol.packet.chat.FileManagement
import net.mamoe.mirai.internal.network.protocol.packet.chat.toResult
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.FileMessage
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
import net.mamoe.mirai.utils.RemoteFile.Companion.ROOT_PATH
import java.io.File
import java.util.*
import kotlin.contracts.contract
private val fs = FileSystem
// internal for tests
internal object FileSystem {
fun checkLegitimacy(path: String) {
val char = path.firstOrNull { it in """:*?"<>|""" }
if (char != null) {
throw IllegalArgumentException("""Chars ':*?"<>|' are not allowed in path. RemoteFile path contains illegal char: '$char'. path='$path'""")
}
}
fun isLegal(path: String): Boolean {
return path.firstOrNull { it in """:*?"<>|""" } == null
}
fun normalize(path: String): String {
checkLegitimacy(path)
return path.replace('\\', '/')
}
// net.mamoe.mirai.internal.utils.internal.utils.FileSystemTest
fun normalize(parent: String, name: String): String {
var nName = normalize(name)
if (nName.startsWith('/')) return nName // absolute path then ignore parent
nName = nName.removeSuffix("/")
var nParent = normalize(parent)
if (nParent == "/") return "/$nName"
if (!nParent.startsWith('/')) nParent = "/$nParent"
val slash = nName.indexOf('/')
if (slash != -1) {
nParent += '/' + nName.substring(0, slash)
nName = nName.substring(slash + 1)
}
return "$nParent/$nName"
}
}
internal class RemoteFileInfo(
val id: String, // fileId or folderId
val isFile: Boolean,
val path: String,
val name: String,
val parentFolderId: String,
val size: Long,
val busId: Int, // for file only
val creatorId: Long, //ownerUin, createUin
val createTime: Long, // uploadTime, createTime
val modifyTime: Long,
val downloadTimes: Int,
val sha: ByteArray, // for file only
val md5: ByteArray, // for file only
) {
companion object {
val root = RemoteFileInfo(
"/", false, "/", "/", "", 0, 0, 0, 0, 0, 0, EMPTY_BYTE_ARRAY, EMPTY_BYTE_ARRAY
)
}
}
internal fun RemoteFile.checkIsImpl(): RemoteFileImpl {
contract { returns() implies (this@checkIsImpl is RemoteFileImpl) }
return this as? RemoteFileImpl ?: error("RemoteFile must not be implemented manually.")
}
internal class RemoteFileImpl(
override val contact: Group,
override val path: String, // absolute
) : RemoteFile {
constructor(contact: Group, parent: String, name: String) : this(contact, fs.normalize(parent, name))
override var id: String? = null
override val name: String
get() = path.substringAfterLast('/')
private val bot get() = contact.bot.asQQAndroidBot()
private val client get() = bot.client
override val parent: RemoteFileImpl?
get() {
if (path == ROOT_PATH) return null
val s = path.substringBeforeLast('/')
return RemoteFileImpl(contact, s.ifEmpty { ROOT_PATH })
}
/**
* Prefer id matching.
*/
private suspend fun Flow.findMatching(): Oidb0x6d8.GetFileListRspBody.Item? {
var nameMatching: Oidb0x6d8.GetFileListRspBody.Item? = null
val idMatching = firstOrNull {
if (it.name == [email protected]) {
nameMatching = it
}
it.id == [email protected]
}
return idMatching ?: nameMatching
}
private suspend fun getFileFolderInfo(): RemoteFileInfo? {
val parent = parent ?: return RemoteFileInfo.root
val info = parent.getFilesFlow()
.filter { it.name == this.name }
.findMatching()
?: return null
return when {
info.folderInfo != null -> info.folderInfo.run {
RemoteFileInfo(
id = folderId,
isFile = false,
path = path,
name = folderName,
parentFolderId = parentFolderId,
size = 0,
busId = 0,
creatorId = createUin,
createTime = createTime.toLongUnsigned(),
modifyTime = modifyTime.toLongUnsigned(),
downloadTimes = 0,
sha = EMPTY_BYTE_ARRAY,
md5 = EMPTY_BYTE_ARRAY,
)
}
info.fileInfo != null -> info.fileInfo.run {
RemoteFileInfo(
id = fileId,
isFile = true,
path = path,
name = fileName,
parentFolderId = parentFolderId,
size = fileSize,
busId = busId,
creatorId = uploaderUin,
createTime = uploadTime.toLongUnsigned(),
modifyTime = modifyTime.toLongUnsigned(),
downloadTimes = downloadTimes,
sha = sha,
md5 = md5,
)
}
else -> null
}
}
private fun RemoteFileInfo?.checkExists(thisPath: String, kind: String = "Remote path"): RemoteFileInfo {
if (this == null) throw IllegalStateException("$kind '$thisPath' does not exist.")
return this
}
override suspend fun isFile(): Boolean = this.getFileFolderInfo().checkExists(this.path).isFile
// compiler bug
override suspend fun isDirectory(): Boolean = !isFile()
override suspend fun length(): Long = this.getFileFolderInfo().checkExists(this.path).size
override suspend fun exists(): Boolean = this.getFileFolderInfo() != null
override suspend fun getInfo(): RemoteFile.FileInfo? {
return getFileFolderInfo()?.run {
RemoteFile.FileInfo(
name = name,
id = id,
path = path,
length = size,
downloadTimes = downloadTimes,
uploaderId = creatorId,
uploadTime = createTime,
lastModifyTime = modifyTime,
sha1 = sha,
md5 = md5,
)
}
}
private suspend fun getFilesFlow(): Flow {
val info = getFileFolderInfo() ?: return emptyFlow()
return flow {
var index = 0
while (true) {
val list = bot.network.sendAndExpect(
FileManagement.GetFileList(
client,
groupCode = contact.id,
folderId = info.id,
startIndex = index
)
).toResult("RemoteFile.listFiles").getOrThrow()
index += list.itemList.size
if (list.int32RetCode != 0) return@flow
if (list.itemList.isEmpty()) return@flow
emitAll(list.itemList.asFlow())
}
}
}
private fun Oidb0x6d8.GetFileListRspBody.Item.resolveToFile(): RemoteFile? {
val item = this
return when {
item.fileInfo != null -> {
resolve(item.fileInfo.fileName)
}
item.folderInfo != null -> {
resolve(item.folderInfo.folderName)
}
else -> null
}?.also {
it.id = item.id
}
}
override suspend fun listFiles(): Flow {
return getFilesFlow().mapNotNull { item ->
item.resolveToFile()
}
}
// compiler bug
override suspend fun listFilesCollection(): List = listFiles().toList()
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@OptIn(JavaFriendlyAPI::class)
override suspend fun listFilesIterator(lazy: Boolean): Iterator {
if (!lazy) return listFiles().toList().iterator()
return object : Iterator {
private val queue = ArrayDeque(1)
@Volatile
private var index = 0
private var ended = false
private suspend fun updateItems() {
val list = bot.network.sendAndExpect(
FileManagement.GetFileList(
client,
groupCode = contact.id,
folderId = path,
startIndex = index
)
).toResult("RemoteFile.listFiles").getOrThrow()
if (list.int32RetCode != 0 || list.itemList.isEmpty()) {
ended = true
return
}
index += list.itemList.size
for (item in list.itemList) {
if (item.fileInfo != null || item.folderInfo != null) queue.add(item)
}
}
override fun hasNext(): Boolean {
if (queue.isEmpty() && !ended) runBlocking { updateItems() }
return queue.isNotEmpty()
}
override fun next(): RemoteFile {
return queue.removeFirst().resolveToFile()!!
}
}
}
override fun resolve(relative: String) = RemoteFileImpl(contact, this.path, relative)
override fun resolve(relative: RemoteFile): RemoteFileImpl {
if (relative.checkIsImpl().contact !== this.contact) error("`relative` must be obtained from the same Group as `this`.")
return resolve(relative.path).also { it.id = relative.id }
}
override suspend fun resolveById(id: String, deep: Boolean): RemoteFile? {
if (this.id == id) return this
val dirs = mutableListOf()
getFilesFlow().mapNotNull { item ->
when {
item.id == id -> item.resolveToFile()
deep && item.folderInfo != null -> {
dirs.add(item)
null
}
else -> null
}
}.firstOrNull()?.let { return it }
for (dir in dirs) {
dir.resolveToFile()?.resolveById(id, deep)?.let { return it }
}
return null
}
// compiler bug
override suspend fun resolveById(id: String): RemoteFile? = resolveById(id, deep = true)
override fun resolveSibling(relative: String): RemoteFileImpl {
val parent = this.parent
if (parent == null) {
if (fs.normalize(relative) == ROOT_PATH) error("Root path does not have sibling paths.")
return RemoteFileImpl(contact, ROOT_PATH)
}
return RemoteFileImpl(contact, parent.path, relative)
}
override fun resolveSibling(relative: RemoteFile): RemoteFileImpl {
if (relative.checkIsImpl().contact !== this.contact) error("`relative` must be obtained from the same Group as `this`.")
return resolveSibling(relative.path).also { it.id = relative.id }
}
private fun RemoteFileInfo.isOperable(): Boolean =
creatorId == bot.id || contact.botPermission.isOperator()
private fun isBotOperator(): Boolean = contact.botPermission.isOperator()
override suspend fun delete(): Boolean {
val info = getFileFolderInfo() ?: return false
if (!info.isOperable()) return false
return when {
info.isFile -> {
bot.network.sendAndExpect(
FileManagement.DeleteFile(
client,
groupCode = contact.id,
busId = info.busId,
fileId = info.id,
parentFolderId = info.parentFolderId,
)
).toResult("RemoteFile.delete", checkResp = false).getOrThrow().int32RetCode == 0
}
// recursively -> {
// this.listFiles().collect { child ->
// child.delete()
// }
// this.delete()
// }
else -> {
// natively 'recursive'
bot.network.sendAndExpect(
FileManagement.DeleteFolder(
client, contact.id, info.id
)
).toResult("RemoteFile.delete").getOrThrow().int32RetCode == 0
}
}
}
override suspend fun renameTo(name: String): Boolean {
if (path == ROOT_PATH && name != ROOT_PATH) return false
val normalized = fs.normalize(name)
if (normalized.contains('/')) throw IllegalArgumentException("'/' is not allowed in file or directory names. Given: '$name'.")
val info = getFileFolderInfo() ?: return false
if (!info.isOperable()) return false
return bot.network.sendAndExpect(
if (info.isFile) {
FileManagement.RenameFile(client, contact.id, info.busId, info.id, info.parentFolderId, normalized)
} else {
FileManagement.RenameFolder(client, contact.id, info.id, normalized)
}
).toResult("RemoteFile.renameTo", checkResp = false).getOrThrow().int32RetCode == 0
}
/**
* null means not exist
*/
private suspend fun getIdSmart(): String? {
if (path == ROOT_PATH) return ROOT_PATH
return this.id ?: this.getFileFolderInfo()?.id
}
override suspend fun moveTo(target: RemoteFile): Boolean {
if (target.checkIsImpl().contact != this.contact) {
// TODO: 2021/3/4 cross-group file move
// target.mkdir()
// val targetFolderId = target.getIdSmart() ?: return false
// this.listFiles().mapNotNull { it.checkIsImpl().getFileFolderInfo() }.collect {
// FileManagement.MoveFile(client, contact.id, it.busId, it.id, it.parentFolderId, targetFolderId)
// .sendAndExpect(bot).toResult("RemoteFile.moveTo", checkResp = false).getOrThrow()
//
// // TODO: 2021/3/3 batch packets
// }
// this.delete() // it is now empty
error("Cross-group file operation is not yet supported.")
}
if (target.path == this.path) return true
if (target.parent?.path == this.path) return false
val info = getFileFolderInfo() ?: return false
if (!info.isOperable()) return false
return if (info.isFile) {
val newParentId = target.parent?.checkIsImpl()?.getIdSmart() ?: return false
bot.network.sendAndExpect(
FileManagement.MoveFile(
client,
contact.id,
info.busId,
info.id,
info.parentFolderId,
newParentId
)
).toResult("RemoteFile.moveTo", checkResp = false).getOrThrow().int32RetCode == 0
} else {
return bot.network.sendAndExpect(FileManagement.RenameFolder(client, contact.id, info.id, target.name))
.toResult("RemoteFile.moveTo", checkResp = false).getOrThrow().int32RetCode == 0
}
}
override suspend fun mkdir(): Boolean {
if (path == ROOT_PATH) return false
if (!isBotOperator()) return false
val parentFolderId: String = parent?.getIdSmart() ?: return false
return bot.network.sendAndExpect(FileManagement.CreateFolder(client, contact.id, parentFolderId, this.name))
.toResult("RemoteFile.mkdir", checkResp = false).getOrThrow().int32RetCode == 0
}
private suspend fun upload0(
resource: ExternalResource,
callback: RemoteFile.ProgressionCallback?,
): Oidb0x6d6.UploadFileRspBody? = resource.withAutoClose {
val parent = parent ?: return null
val parentInfo = parent.getFileFolderInfo() ?: return null
val resp = bot.network.sendAndExpect(
FileManagement.RequestUpload(
client,
groupCode = contact.id,
folderId = parentInfo.id,
resource = resource,
filename = this.name
)
).toResult("RemoteFile.upload").getOrThrow()
if (resp.boolFileExist) {
return resp
}
val ext = GroupFileUploadExt(
u1 = 100,
u2 = 1,
entry = GroupFileUploadEntry(
business = ExcitingBusiInfo(
busId = resp.busId,
senderUin = bot.id,
receiverUin = contact.groupCode, // TODO: 2021/3/1 code or uin?
groupCode = contact.groupCode,
),
fileEntry = ExcitingFileEntry(
fileSize = resource.size,
md5 = resource.md5,
sha1 = resource.sha1,
fileId = resp.fileId.toByteArray(),
uploadKey = resp.checkKey,
),
clientInfo = ExcitingClientInfo(
clientType = 2,
appId = client.protocol.id.toString(),
terminalType = 2,
clientVer = "9e9c09dc",
unknown = 4,
),
fileNameInfo = ExcitingFileNameInfo(this.name),
host = ExcitingHostConfig(
hosts = listOf(
ExcitingHostInfo(
url = ExcitingUrlInfo(
unknown = 1,
host = resp.uploadIpLanV4.firstOrNull()
?: resp.uploadIpLanV6.firstOrNull()
?: resp.uploadIp,
),
port = resp.uploadPort,
),
),
),
),
u3 = 0,
).toByteArray(GroupFileUploadExt.serializer())
callback?.onBegin(this, resource)
kotlin.runCatching {
Highway.uploadResourceBdh(
bot = bot,
resource = resource,
kind = ResourceKind.GROUP_FILE,
commandId = 71,
extendInfo = ext,
dataFlag = 0,
callback = if (callback == null) null else fun(it: Long) {
callback.onProgression(this, resource, it)
}
)
}.fold(
onSuccess = {
callback?.onSuccess(this, resource)
},
onFailure = {
callback?.onFailure(this, resource, it)
}
)
return resp
}
private suspend fun uploadInternal(
resource: ExternalResource,
callback: RemoteFile.ProgressionCallback?,
): FileMessage {
val resp = upload0(resource, callback) ?: error("Failed to upload file.")
return FileMessageImpl(
resp.fileId, resp.busId, name, resource.size, allowSend = true
)
}
@Deprecated(
"Use uploadAndSend instead.",
replaceWith = ReplaceWith("this.uploadAndSend(resource, callback)"),
level = DeprecationLevel.ERROR
)
override suspend fun upload(
resource: ExternalResource,
callback: RemoteFile.ProgressionCallback?,
): FileMessage {
val msg = uploadInternal(resource, callback)
contact.sendMessage(msg + AllowSendFileMessage)
return msg
}
// compiler bug
@Deprecated(
"Use uploadAndSend instead.",
replaceWith = ReplaceWith("this.uploadAndSend(resource)"),
level = DeprecationLevel.ERROR
)
@Suppress("DEPRECATION_ERROR")
override suspend fun upload(resource: ExternalResource): FileMessage {
return upload(resource, null)
}
// compiler bug
@Deprecated(
"Use uploadAndSend instead.",
replaceWith = ReplaceWith("this.uploadAndSend(file, callback)"),
level = DeprecationLevel.ERROR
)
@Suppress("DEPRECATION_ERROR")
override suspend fun upload(file: File, callback: RemoteFile.ProgressionCallback?): FileMessage =
file.toExternalResource().use { upload(it, callback) }
//compiler bug
@Deprecated(
"Use sendFile instead.",
replaceWith = ReplaceWith("this.uploadAndSend(file)"),
level = DeprecationLevel.ERROR
)
@Suppress("DEPRECATION_ERROR")
override suspend fun upload(file: File): FileMessage {
// Dear compiler:
//
// Please generate invokeinterface.
//
// Yours Sincerely
// Him188
return file.toExternalResource().use { upload(it) }
}
override suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt {
@Suppress("DEPRECATION")
return contact.sendMessage(AllowSendFileMessage + uploadInternal(resource, null))
}
// compiler bug
override suspend fun uploadAndSend(file: File): MessageReceipt =
file.toExternalResource().use { uploadAndSend(it) }
// override suspend fun writeSession(resource: ExternalResource): FileUploadSession {
// }
override suspend fun getDownloadInfo(): RemoteFile.DownloadInfo? {
val info = getFileFolderInfo() ?: return null
if (!info.isFile) return null
val resp = bot.network.sendAndExpect(
FileManagement.RequestDownload(
client,
groupCode = contact.id,
busId = info.busId,
fileId = info.id
)
).toResult("RemoteFile.getDownloadInfo").getOrThrow()
return RemoteFile.DownloadInfo(
filename = name,
id = info.id,
path = path,
url = "http://${resp.downloadIp}/ftn_handler/${resp.downloadUrl.toUHexString("")}/?fname=" +
info.id.toByteArray().toUHexString(""),
sha1 = info.sha,
md5 = info.md5
)
}
override fun toString(): String = path
override suspend fun toMessage(): FileMessage? {
val info = getFileFolderInfo() ?: return null
if (!info.isFile) return null
return FileMessageImpl(info.id, info.busId, name, info.size)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy