瀏覽代碼

Fix RemoteFile FileSystem and implement resolve and listFiles

Him188 5 年之前
父節點
當前提交
e2a67797bb

+ 8 - 7
mirai-core-api/src/commonMain/kotlin/utils/RemoteFile.kt

@@ -16,7 +16,6 @@ import java.io.File
 import java.io.InputStream
 import java.io.OutputStream
 import java.io.RandomAccessFile
-import java.util.stream.Stream
 
 /**
  * @since 2.5
@@ -28,7 +27,7 @@ public interface RemoteFile {
 
     public val path: String
 
-    public fun parent(): RemoteFile?
+    public val parent: RemoteFile?
 
     public suspend fun isFile(): Boolean
 
@@ -41,15 +40,15 @@ public interface RemoteFile {
     public suspend fun listFiles(): Flow<RemoteFile>
 
     @JavaFriendlyAPI
-    public suspend fun listFilesStream(): Stream<RemoteFile>
+    public suspend fun listFilesIterator(): Iterator<RemoteFile>
 
-    public suspend fun resolve(relativePath: String): RemoteFile
+    public fun resolve(relativePath: String): RemoteFile
 
-    public suspend fun resolve(relative: RemoteFile): RemoteFile = resolve(relative.path)
+    public fun resolve(relative: RemoteFile): RemoteFile = resolve(relative.path)
 
-    public suspend fun resolveSibling(other: String): RemoteFile
+    public fun resolveSibling(other: String): RemoteFile
 
-    public suspend fun resolveSibling(relative: RemoteFile): RemoteFile = resolve(relative.path)
+    public fun resolveSibling(relative: RemoteFile): RemoteFile = resolve(relative.path)
 
     public suspend fun delete(recursively: Boolean): Boolean
 
@@ -61,6 +60,8 @@ public interface RemoteFile {
     public suspend fun write(resource: ExternalResource)
 
     public suspend fun open(): FileDownloadSession
+
+    public override fun toString(): String
 }
 
 /**

+ 1 - 1
mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt

@@ -73,7 +73,7 @@ internal class GroupImpl(
     override lateinit var owner: NormalMember
     override lateinit var botAsMember: NormalMember
 
-    override val filesRoot: RemoteFile by lazy { RemoteFileImpl("", "/", this) }
+    override val filesRoot: RemoteFile by lazy { RemoteFileImpl(this, "/") }
 
     override val members: ContactList<NormalMember> = ContactList(members.mapNotNullTo(ConcurrentLinkedQueue()) {
         if (it.uin == bot.id) {

+ 1 - 1
mirai-core/src/commonMain/kotlin/network/protocol/data/proto/GroupFileCommon.kt

@@ -76,7 +76,7 @@ internal class GroupFileCommon : ProtoBuf {
 
     @Serializable
     internal class FolderInfo(
-        @JvmField @ProtoNumber(1) val folderId: String = "",
+        @JvmField @ProtoNumber(1) val folderId: String = "", // uuid
         @JvmField @ProtoNumber(2) val parentFolderId: String = "",
         @JvmField @ProtoNumber(3) val folderName: String = "",
         @JvmField @ProtoNumber(4) val createTime: Int = 0,

+ 1 - 1
mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Oidb0x6d8.kt

@@ -99,7 +99,7 @@ internal class Oidb0x6d8 : ProtoBuf {
     ) : ProtoBuf {
         @Serializable
         internal class Item(
-            @JvmField @ProtoNumber(1) val type: Int = 0,
+            @JvmField @ProtoNumber(1) val type: Int = 0, // folder=2,
             @JvmField @ProtoNumber(2) val folderInfo: GroupFileCommon.FolderInfo? = null,
             @JvmField @ProtoNumber(3) val fileInfo: GroupFileCommon.FileInfo? = null
         ) : ProtoBuf

+ 2 - 0
mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt

@@ -161,6 +161,8 @@ internal object KnownPacketFactories {
         StrangerList.DelStranger,
         SummaryCard.ReqSummaryCard,
         MusicSharePacket,
+        FileManagement.RequestUpload,
+        FileManagement.GetFileList,
     )
 
     object IncomingFactories : List<IncomingPacketFactory<*>> by mutableListOf(

+ 18 - 1
mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/GroupFile.kt

@@ -7,6 +7,8 @@
  *  https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
+@file:Suppress("NOTHING_TO_INLINE")
+
 package net.mamoe.mirai.internal.network.protocol.packet.chat
 
 import kotlinx.io.core.ByteReadPacket
@@ -30,13 +32,28 @@ internal sealed class CommonOidbResponse<T> : Packet {
         val result: Int,
         val msg: String,
         val e: Throwable?,
-    ) : CommonOidbResponse<T>()
+    ) : CommonOidbResponse<T>() {
+        inline fun createException(actionName: String): IllegalStateException {
+            return IllegalStateException("Failed $actionName, result=$result, msg=$msg", e)
+        }
+    }
 
     class Success<T>(
         val resp: T
     ) : CommonOidbResponse<T>()
 }
 
+@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE")
[email protected]
+internal inline fun <T> CommonOidbResponse<T>.toResult(actionName: String): Result<T> {
+    return if (this is CommonOidbResponse.Failure) {
+        Result.failure(this.createException(actionName))
+    } else {
+        this as CommonOidbResponse.Success<T>
+        Result.success(this.resp)
+    }
+}
+
 /**
  * @param respMapper may throw any exception, which will be wrapped to CommonOidbResponse.Failure
  */

+ 83 - 27
mirai-core/src/commonMain/kotlin/utils/RemoteFileImpl.kt

@@ -12,12 +12,16 @@ package net.mamoe.mirai.internal.utils
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.flow
 import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.internal.asQQAndroidBot
+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.network.protocol.packet.sendAndExpect
 import net.mamoe.mirai.utils.*
 import java.io.InputStream
 import java.io.OutputStream
 import java.io.RandomAccessFile
-import java.util.stream.Stream
 import kotlin.coroutines.CoroutineContext
 
 private val fs = FileSystem
@@ -33,19 +37,21 @@ internal object FileSystem {
 
     fun normalize(path: String): String {
         checkLegitimacy(path)
-        return path.trimStart().replace('\\', '/').removeSuffix("/") // tolerant leading white spaces
+        return path.replace('\\', '/')
     }
 
     // TODO: 2021/2/25 add tests for FS
     // 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"
 
-        var nName = normalize(name)
-        nName = nName.removeSurrounding("/")
-
         val slash = nName.indexOf('/')
         if (slash != -1) {
             nParent += '/' + nName.substring(0, slash)
@@ -56,9 +62,23 @@ internal object FileSystem {
     }
 }
 
+internal class RemoteFileInfo(
+    val isFile: Boolean,
+    val path: String, // fileId
+    val name: String,
+    val size: Long,
+    val busId: Int, // for file only
+    val creatorId: Long, //ownerUin, createUin
+    val createTime: Long, // uploadTime, createTime
+    val modifyTime: Long,
+    val sha: ByteArray, // for file only
+    val md5: ByteArray, // for file only
+    val downloadTimes: Int,
+)
+
 internal class RemoteFileImpl(
     contact: Group,
-    override val path: String,
+    override val path: String, // absolute
 ) : RemoteFile {
     private val contactRef by contact.weakRef()
     private val contact get() = contactRef ?: error("RemoteFile is closed due to Contact closed.")
@@ -68,43 +88,77 @@ internal class RemoteFileImpl(
     override val name: String
         get() = path.substringAfterLast('/')
 
-    override fun parent(): RemoteFile? {
-        val s = path.substringBeforeLast('/', "")
-        if (s.isEmpty()) return null
-        return RemoteFileImpl(contact, s)
-    }
+    private val bot get() = contact.bot.asQQAndroidBot()
+    private val client get() = bot.client
 
-    override suspend fun isFile(): Boolean {
-        val parent = parent() ?: return false // path must == '/'
+    override val parent: RemoteFile?
+        get() {
+            val s = path.substringBeforeLast('/', "")
+            if (s.isEmpty()) return null
+            return RemoteFileImpl(contact, s)
+        }
 
-        // TODO: 2021/2/25
-        return false
+    private suspend fun getFileFolderInfo(): RemoteFileInfo? {
+        TODO()
     }
 
-    override suspend fun length(): Long {
-        TODO("Not yet implemented")
+    private fun RemoteFileInfo?.checkExists(thisPath: String): RemoteFileInfo {
+        if (this == null) throw IllegalStateException("Remote path '$thisPath' does not exist.")
+        return this
     }
 
-    override suspend fun exists(): Boolean {
-        TODO("Not yet implemented")
-    }
+    override suspend fun isFile(): Boolean = this.getFileFolderInfo().checkExists(this.path).isFile
+    override suspend fun length(): Long = this.getFileFolderInfo().checkExists(this.path).size
+    override suspend fun exists(): Boolean = this.getFileFolderInfo() != null
 
     override suspend fun listFiles(): Flow<RemoteFile> {
-        TODO("Not yet implemented")
+        return flow {
+            var index = 0
+            while (true) {
+                val list = FileManagement.GetFileList(
+                    client,
+                    groupCode = contact.id,
+                    folderId = path,
+                    startIndex = index
+                ).sendAndExpect(bot).toResult("get group file").getOrThrow()
+                index += list.itemList.size
+
+                if (list.int32RetCode != 0) return@flow
+                if (list.itemList.isEmpty()) return@flow
+
+                for (item in list.itemList) {
+                    when {
+                        item.fileInfo != null -> {
+                            emit(resolve(item.fileInfo.fileName))
+                        }
+                        item.folderInfo != null -> {
+                            emit(resolve(item.folderInfo.folderName))
+                        }
+                        else -> {
+                        }
+                    }
+                }
+            }
+        }
     }
 
     @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
-    @OptIn(net.mamoe.mirai.utils.JavaFriendlyAPI::class)
-    override suspend fun listFilesStream(): Stream<RemoteFile> {
+    @OptIn(JavaFriendlyAPI::class)
+    override suspend fun listFilesIterator(): Iterator<RemoteFile> {
         TODO("Not yet implemented")
     }
 
-    override suspend fun resolve(relativePath: String): RemoteFile {
-        TODO("Not yet implemented")
+    override fun resolve(relativePath: String): RemoteFile {
+        return RemoteFileImpl(contact, this.path, relativePath)
     }
 
-    override suspend fun resolveSibling(other: String): RemoteFile {
-        TODO("Not yet implemented")
+    override fun resolveSibling(other: String): RemoteFile {
+        val parent = this.parent
+        if (parent == null) {
+            if (fs.normalize(other) != "/") error("Remote path '/' does not have sibling paths.")
+            return RemoteFileImpl(contact, "/")
+        }
+        return RemoteFileImpl(contact, parent.path, other)
     }
 
     override suspend fun delete(recursively: Boolean): Boolean {
@@ -127,6 +181,8 @@ internal class RemoteFileImpl(
     override suspend fun open(): FileDownloadSessionImpl {
         TODO("Not yet implemented")
     }
+
+    override fun toString(): String = path
 }
 
 internal class FileDownloadSessionImpl : FileDownloadSession, CoroutineScope {

+ 1 - 1
mirai-core/src/commonMain/kotlin/utils/io/serialization/utils.kt

@@ -216,7 +216,7 @@ internal inline fun <T : ProtoBuf> ByteReadPacket.readOidbSsoPkg(
 ): OidbBodyOrFailure<T> {
     val oidb = readBytes(length).loadAs(OidbSso.OIDBSSOPkg.serializer())
     return if (oidb.result == 0) {
-        OidbBodyOrFailure.success(readBytes(length).loadOidb(serializer))
+        OidbBodyOrFailure.success(oidb.bodybuffer.loadAs(serializer))
     } else {
         OidbBodyOrFailure.failure(oidb)
     }