Redesign group files (#1589)

* Prototype new `RemoteFiles` design

* add `@JavaFriendlyAPI`

* remove `quietly`

* move `moveTo` to `AbsoluteFile`

* Add java friendly apis

* Remove `condoneMissing`

* Change `renameTo`

* Extract interface declarations

* update docs

* Add `AbsoluteFileFolder.exists`

* Add common ProgressionCallback

* Implement `RemoteFiles` and relevant `Absolute*`

* Implement `refresh` and `refreshed`

* Update docs

* Forbid blank paths

* Update docs

* Deprecate `RemoteFile` and implement `FileMessage.toAbsoluteFile`

* Change corresponding properties on operations

* Deprecate more old declarations

* Update docs

* Add check for permission

* Allow relative paths and fix upload

* fix absolutePath

* doc update

* api dump

* `Result<R>.onSuccessCatching`

* return null when file not exists

* Fix file uploading

* Fix folder.absolutePath

* add `resolveFileById`

* Implement toString

* Add `nameWithoutExtension` and `extension`

* Add `deep` to resolveFileById

* Implement permission check

* Remove notes

* Fix `resolveFileById`

* Fix `extension`

* add docs

* Improve docs

Co-authored-by: Karlatemp <karlatemp@vip.qq.com>
This commit is contained in:
Him188 2021-10-30 16:05:57 +01:00 committed by GitHub
parent 767475f9ab
commit 9e151e7026
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1984 additions and 4 deletions

View File

@ -328,6 +328,7 @@ public final class net/mamoe/mirai/contact/ExceptionsKt {
}
public abstract interface class net/mamoe/mirai/contact/FileSupported : net/mamoe/mirai/contact/Contact {
public abstract fun getFiles ()Lnet/mamoe/mirai/contact/file/RemoteFiles;
public abstract fun getFilesRoot ()Lnet/mamoe/mirai/utils/RemoteFile;
}
@ -749,6 +750,112 @@ public final class net/mamoe/mirai/contact/announcement/OnlineAnnouncementKt {
public static final fun getBot (Lnet/mamoe/mirai/contact/announcement/OnlineAnnouncement;)Lnet/mamoe/mirai/Bot;
}
public abstract interface class net/mamoe/mirai/contact/file/AbsoluteFile : net/mamoe/mirai/contact/file/AbsoluteFileFolder {
public abstract fun getExpiryTime ()J
public abstract fun getMd5 ()[B
public abstract fun getSha1 ()[B
public abstract fun getSize ()J
public fun getUrl ()Ljava/lang/String;
public abstract fun getUrl (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun moveTo (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;)Z
public abstract fun moveTo (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun refreshed ()Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public abstract fun refreshed (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun toMessage ()Lnet/mamoe/mirai/message/data/FileMessage;
}
public abstract interface class net/mamoe/mirai/contact/file/AbsoluteFileFolder {
public static final field Companion Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder$Companion;
public fun delete ()Z
public abstract fun delete (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun exists ()Z
public abstract fun exists (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getAbsolutePath ()Ljava/lang/String;
public abstract fun getContact ()Lnet/mamoe/mirai/contact/FileSupported;
public static fun getExtension (Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder;)Ljava/lang/String;
public abstract fun getId ()Ljava/lang/String;
public abstract fun getLastModifiedTime ()J
public abstract fun getName ()Ljava/lang/String;
public static fun getNameWithoutExtension (Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder;)Ljava/lang/String;
public abstract fun getParent ()Lnet/mamoe/mirai/contact/file/AbsoluteFolder;
public abstract fun getUploadTime ()J
public abstract fun getUploaderId ()J
public abstract fun isFile ()Z
public abstract fun isFolder ()Z
public fun refresh ()Z
public abstract fun refresh (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun refreshed ()Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder;
public abstract fun refreshed (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun renameTo (Ljava/lang/String;)Z
public abstract fun renameTo (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun toString ()Ljava/lang/String;
}
public final class net/mamoe/mirai/contact/file/AbsoluteFileFolder$Companion {
public final fun getExtension (Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder;)Ljava/lang/String;
public final fun getNameWithoutExtension (Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder;)Ljava/lang/String;
}
public abstract interface class net/mamoe/mirai/contact/file/AbsoluteFolder : net/mamoe/mirai/contact/file/AbsoluteFileFolder {
public static final field Companion Lnet/mamoe/mirai/contact/file/AbsoluteFolder$Companion;
public static final field ROOT_FOLDER_ID Ljava/lang/String;
public fun children ()Lkotlinx/coroutines/flow/Flow;
public abstract fun children (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun childrenStream ()Ljava/util/stream/Stream;
public abstract fun childrenStream (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun createFolder (Ljava/lang/String;)Lnet/mamoe/mirai/contact/file/AbsoluteFolder;
public abstract fun createFolder (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun files ()Lkotlinx/coroutines/flow/Flow;
public abstract fun files (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun filesStream ()Ljava/util/stream/Stream;
public abstract fun filesStream (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun folders ()Lkotlinx/coroutines/flow/Flow;
public abstract fun folders (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun foldersStream ()Ljava/util/stream/Stream;
public abstract fun foldersStream (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getContentsCount ()I
public fun isEmpty ()Z
public fun refreshed ()Lnet/mamoe/mirai/contact/file/AbsoluteFolder;
public abstract fun refreshed (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun resolveAll (Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow;
public abstract fun resolveAll (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun resolveAllStream (Ljava/lang/String;)Ljava/util/stream/Stream;
public abstract fun resolveAllStream (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun resolveFileById (Ljava/lang/String;)Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public fun resolveFileById (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun resolveFileById (Ljava/lang/String;Z)Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public abstract fun resolveFileById (Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun resolveFileById$default (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;Ljava/lang/String;ZILjava/lang/Object;)Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public static synthetic fun resolveFileById$default (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;Ljava/lang/String;ZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public fun resolveFiles (Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow;
public abstract fun resolveFiles (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun resolveFilesStream (Ljava/lang/String;)Ljava/util/stream/Stream;
public abstract fun resolveFilesStream (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun resolveFolder (Ljava/lang/String;)Lnet/mamoe/mirai/contact/file/AbsoluteFolder;
public abstract fun resolveFolder (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;)Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public abstract fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun uploadNewFile$default (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;ILjava/lang/Object;)Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public static synthetic fun uploadNewFile$default (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
}
public final class net/mamoe/mirai/contact/file/AbsoluteFolder$Companion {
public static final field ROOT_FOLDER_ID Ljava/lang/String;
}
public abstract interface class net/mamoe/mirai/contact/file/RemoteFiles {
public abstract fun getContact ()Lnet/mamoe/mirai/contact/FileSupported;
public abstract fun getRoot ()Lnet/mamoe/mirai/contact/file/AbsoluteFolder;
public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;)Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun uploadNewFile$default (Lnet/mamoe/mirai/contact/file/RemoteFiles;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;ILjava/lang/Object;)Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public static synthetic fun uploadNewFile$default (Lnet/mamoe/mirai/contact/file/RemoteFiles;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
}
public abstract interface class net/mamoe/mirai/contact/roaming/RoamingMessage {
public fun getBot ()Lnet/mamoe/mirai/Bot;
public abstract fun getContact ()Lnet/mamoe/mirai/contact/Contact;
@ -4034,6 +4141,8 @@ public abstract interface class net/mamoe/mirai/message/data/FileMessage : net/m
public synthetic fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey;
public abstract fun getName ()Ljava/lang/String;
public abstract fun getSize ()J
public fun toAbsoluteFile (Lnet/mamoe/mirai/contact/FileSupported;)Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public abstract fun toAbsoluteFile (Lnet/mamoe/mirai/contact/FileSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun toRemoteFile (Lnet/mamoe/mirai/contact/FileSupported;)Lnet/mamoe/mirai/utils/RemoteFile;
public fun toRemoteFile (Lnet/mamoe/mirai/contact/FileSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
@ -5977,6 +6086,21 @@ public final class net/mamoe/mirai/utils/OverFileSizeMaxException : java/lang/Il
public fun <init> ()V
}
public abstract interface class net/mamoe/mirai/utils/ProgressionCallback {
public static final field Companion Lnet/mamoe/mirai/utils/ProgressionCallback$Companion;
public static fun asProgressionCallback (Lkotlinx/coroutines/channels/SendChannel;Z)Lnet/mamoe/mirai/utils/ProgressionCallback;
public fun onBegin (Ljava/lang/Object;Lnet/mamoe/mirai/utils/ExternalResource;)V
public fun onFailure (Ljava/lang/Object;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/Throwable;)V
public fun onFinished (Ljava/lang/Object;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/Object;)V
public fun onProgression (Ljava/lang/Object;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/Object;)V
public fun onSuccess (Ljava/lang/Object;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/Object;)V
}
public final class net/mamoe/mirai/utils/ProgressionCallback$Companion {
public final fun asProgressionCallback (Lkotlinx/coroutines/channels/SendChannel;Z)Lnet/mamoe/mirai/utils/ProgressionCallback;
public static synthetic fun asProgressionCallback$default (Lnet/mamoe/mirai/utils/ProgressionCallback$Companion;Lkotlinx/coroutines/channels/SendChannel;ZILjava/lang/Object;)Lnet/mamoe/mirai/utils/ProgressionCallback;
}
public abstract interface class net/mamoe/mirai/utils/RemoteFile {
public static final field Companion Lnet/mamoe/mirai/utils/RemoteFile$Companion;
public static final field ROOT_PATH Ljava/lang/String;

View File

@ -328,6 +328,7 @@ public final class net/mamoe/mirai/contact/ExceptionsKt {
}
public abstract interface class net/mamoe/mirai/contact/FileSupported : net/mamoe/mirai/contact/Contact {
public abstract fun getFiles ()Lnet/mamoe/mirai/contact/file/RemoteFiles;
public abstract fun getFilesRoot ()Lnet/mamoe/mirai/utils/RemoteFile;
}
@ -749,6 +750,112 @@ public final class net/mamoe/mirai/contact/announcement/OnlineAnnouncementKt {
public static final fun getBot (Lnet/mamoe/mirai/contact/announcement/OnlineAnnouncement;)Lnet/mamoe/mirai/Bot;
}
public abstract interface class net/mamoe/mirai/contact/file/AbsoluteFile : net/mamoe/mirai/contact/file/AbsoluteFileFolder {
public abstract fun getExpiryTime ()J
public abstract fun getMd5 ()[B
public abstract fun getSha1 ()[B
public abstract fun getSize ()J
public fun getUrl ()Ljava/lang/String;
public abstract fun getUrl (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun moveTo (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;)Z
public abstract fun moveTo (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun refreshed ()Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public abstract fun refreshed (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun toMessage ()Lnet/mamoe/mirai/message/data/FileMessage;
}
public abstract interface class net/mamoe/mirai/contact/file/AbsoluteFileFolder {
public static final field Companion Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder$Companion;
public fun delete ()Z
public abstract fun delete (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun exists ()Z
public abstract fun exists (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getAbsolutePath ()Ljava/lang/String;
public abstract fun getContact ()Lnet/mamoe/mirai/contact/FileSupported;
public static fun getExtension (Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder;)Ljava/lang/String;
public abstract fun getId ()Ljava/lang/String;
public abstract fun getLastModifiedTime ()J
public abstract fun getName ()Ljava/lang/String;
public static fun getNameWithoutExtension (Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder;)Ljava/lang/String;
public abstract fun getParent ()Lnet/mamoe/mirai/contact/file/AbsoluteFolder;
public abstract fun getUploadTime ()J
public abstract fun getUploaderId ()J
public abstract fun isFile ()Z
public abstract fun isFolder ()Z
public fun refresh ()Z
public abstract fun refresh (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun refreshed ()Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder;
public abstract fun refreshed (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun renameTo (Ljava/lang/String;)Z
public abstract fun renameTo (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun toString ()Ljava/lang/String;
}
public final class net/mamoe/mirai/contact/file/AbsoluteFileFolder$Companion {
public final fun getExtension (Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder;)Ljava/lang/String;
public final fun getNameWithoutExtension (Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder;)Ljava/lang/String;
}
public abstract interface class net/mamoe/mirai/contact/file/AbsoluteFolder : net/mamoe/mirai/contact/file/AbsoluteFileFolder {
public static final field Companion Lnet/mamoe/mirai/contact/file/AbsoluteFolder$Companion;
public static final field ROOT_FOLDER_ID Ljava/lang/String;
public fun children ()Lkotlinx/coroutines/flow/Flow;
public abstract fun children (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun childrenStream ()Ljava/util/stream/Stream;
public abstract fun childrenStream (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun createFolder (Ljava/lang/String;)Lnet/mamoe/mirai/contact/file/AbsoluteFolder;
public abstract fun createFolder (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun files ()Lkotlinx/coroutines/flow/Flow;
public abstract fun files (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun filesStream ()Ljava/util/stream/Stream;
public abstract fun filesStream (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun folders ()Lkotlinx/coroutines/flow/Flow;
public abstract fun folders (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun foldersStream ()Ljava/util/stream/Stream;
public abstract fun foldersStream (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getContentsCount ()I
public fun isEmpty ()Z
public fun refreshed ()Lnet/mamoe/mirai/contact/file/AbsoluteFolder;
public abstract fun refreshed (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun resolveAll (Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow;
public abstract fun resolveAll (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun resolveAllStream (Ljava/lang/String;)Ljava/util/stream/Stream;
public abstract fun resolveAllStream (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun resolveFileById (Ljava/lang/String;)Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public fun resolveFileById (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun resolveFileById (Ljava/lang/String;Z)Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public abstract fun resolveFileById (Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun resolveFileById$default (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;Ljava/lang/String;ZILjava/lang/Object;)Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public static synthetic fun resolveFileById$default (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;Ljava/lang/String;ZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public fun resolveFiles (Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow;
public abstract fun resolveFiles (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun resolveFilesStream (Ljava/lang/String;)Ljava/util/stream/Stream;
public abstract fun resolveFilesStream (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun resolveFolder (Ljava/lang/String;)Lnet/mamoe/mirai/contact/file/AbsoluteFolder;
public abstract fun resolveFolder (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;)Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public abstract fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun uploadNewFile$default (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;ILjava/lang/Object;)Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public static synthetic fun uploadNewFile$default (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
}
public final class net/mamoe/mirai/contact/file/AbsoluteFolder$Companion {
public static final field ROOT_FOLDER_ID Ljava/lang/String;
}
public abstract interface class net/mamoe/mirai/contact/file/RemoteFiles {
public abstract fun getContact ()Lnet/mamoe/mirai/contact/FileSupported;
public abstract fun getRoot ()Lnet/mamoe/mirai/contact/file/AbsoluteFolder;
public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;)Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun uploadNewFile$default (Lnet/mamoe/mirai/contact/file/RemoteFiles;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;ILjava/lang/Object;)Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public static synthetic fun uploadNewFile$default (Lnet/mamoe/mirai/contact/file/RemoteFiles;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
}
public abstract interface class net/mamoe/mirai/contact/roaming/RoamingMessage {
public fun getBot ()Lnet/mamoe/mirai/Bot;
public abstract fun getContact ()Lnet/mamoe/mirai/contact/Contact;
@ -4034,6 +4141,8 @@ public abstract interface class net/mamoe/mirai/message/data/FileMessage : net/m
public synthetic fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey;
public abstract fun getName ()Ljava/lang/String;
public abstract fun getSize ()J
public fun toAbsoluteFile (Lnet/mamoe/mirai/contact/FileSupported;)Lnet/mamoe/mirai/contact/file/AbsoluteFile;
public abstract fun toAbsoluteFile (Lnet/mamoe/mirai/contact/FileSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun toRemoteFile (Lnet/mamoe/mirai/contact/FileSupported;)Lnet/mamoe/mirai/utils/RemoteFile;
public fun toRemoteFile (Lnet/mamoe/mirai/contact/FileSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
@ -5977,6 +6086,21 @@ public final class net/mamoe/mirai/utils/OverFileSizeMaxException : java/lang/Il
public fun <init> ()V
}
public abstract interface class net/mamoe/mirai/utils/ProgressionCallback {
public static final field Companion Lnet/mamoe/mirai/utils/ProgressionCallback$Companion;
public static fun asProgressionCallback (Lkotlinx/coroutines/channels/SendChannel;Z)Lnet/mamoe/mirai/utils/ProgressionCallback;
public fun onBegin (Ljava/lang/Object;Lnet/mamoe/mirai/utils/ExternalResource;)V
public fun onFailure (Ljava/lang/Object;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/Throwable;)V
public fun onFinished (Ljava/lang/Object;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/Object;)V
public fun onProgression (Ljava/lang/Object;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/Object;)V
public fun onSuccess (Ljava/lang/Object;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/Object;)V
}
public final class net/mamoe/mirai/utils/ProgressionCallback$Companion {
public final fun asProgressionCallback (Lkotlinx/coroutines/channels/SendChannel;Z)Lnet/mamoe/mirai/utils/ProgressionCallback;
public static synthetic fun asProgressionCallback$default (Lnet/mamoe/mirai/utils/ProgressionCallback$Companion;Lkotlinx/coroutines/channels/SendChannel;ZILjava/lang/Object;)Lnet/mamoe/mirai/utils/ProgressionCallback;
}
public abstract interface class net/mamoe/mirai/utils/RemoteFile {
public static final field Companion Lnet/mamoe/mirai/utils/RemoteFile$Companion;
public static final field ROOT_PATH Ljava/lang/String;

View File

@ -10,15 +10,18 @@
package net.mamoe.mirai.contact
import net.mamoe.mirai.contact.file.RemoteFiles
import net.mamoe.mirai.utils.NotStableForInheritance
import net.mamoe.mirai.utils.RemoteFile
/**
* 支持文件操作的 [Contact]. 目前仅 [Group].
*
* 获取文件操作相关示例: [RemoteFile]
* 获取文件操作相关示例: [RemoteFiles]
*
* @since 2.5
*
* @see RemoteFiles
*/
@NotStableForInheritance
public interface FileSupported : Contact {
@ -27,5 +30,14 @@ public interface FileSupported : Contact {
*
* @since 2.5
*/
@Suppress("DEPRECATION")
@Deprecated("Please use files instead.", replaceWith = ReplaceWith("files.root")) // deprecated since 2.8.0-RC
public val filesRoot: RemoteFile
/**
* 获取远程文件列表 (管理器).
*
* @since 2.8
*/
public val files: RemoteFiles
}

View File

@ -0,0 +1,83 @@
/*
* Copyright 2019-2021 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:JvmBlockingBridge
@file:Suppress("OVERLOADS_INTERFACE")
package net.mamoe.mirai.contact.file
import kotlinx.io.errors.IOException
import net.mamoe.kjbb.JvmBlockingBridge
import net.mamoe.mirai.contact.PermissionDeniedException
import net.mamoe.mirai.message.data.FileMessage
import net.mamoe.mirai.utils.NotStableForInheritance
/**
* 绝对文件标识. 精确表示一个远程文件. 不会受同名文件或目录的影响.
*
* @since 2.8
* @see RemoteFiles
* @see AbsoluteFolder
* @see AbsoluteFileFolder
*/
@NotStableForInheritance
public interface AbsoluteFile : AbsoluteFileFolder {
/**
* 文件到期时间戳, 单位秒.
*/
public val expiryTime: Long
/**
* 文件大小 (占用空间), 单位 byte.
*/
public val size: Long
/**
* 文件内容 SHA-1.
*/
public val sha1: ByteArray
/**
* 文件内容 MD5.
*/
public val md5: ByteArray
/**
* 移动远程文件到 [folder] 目录下. 成功时返回 `true`, 当远程文件不存在时返回 `false`.
*
* 注意该操作有可能产生同名文件或目录 ( [folder] 中已经存在一个名称为 [name] 的文件或目录时).
*
* @throws IOException 当发生网络错误时可能抛出
* @throws IllegalStateException 当发生已知的协议错误时抛出
* @throws PermissionDeniedException 当无管理员权限时抛出 (若群仅允许管理员上传)
*/
public suspend fun moveTo(folder: AbsoluteFolder): Boolean
/**
* 获得下载链接 URL 字符串. 当远程文件不存在时返回 `null`.
*/
public suspend fun getUrl(): String?
/**
* 得到表示远程文件的可以发送的 [FileMessage].
*
* [上传文件][RemoteFiles.uploadNewFile] 时就已经发送了文件消息. [toMessage] 可供之后再次发送使用.
*/
public fun toMessage(): FileMessage
/**
* 返回更新了文件或目录信息 ([lastModifiedTime] ) , 指向相同文件的 [AbsoluteFileFolder].
* 不会更新当前 [AbsoluteFileFolder] 对象.
*
* 当远程文件或目录不存在时返回 `null`.
*
* 该函数会遍历上级目录的所有文件并匹配当前文件, 因此可能会非常慢, 请不要频繁使用.
*/
override suspend fun refreshed(): AbsoluteFile?
}

View File

@ -0,0 +1,183 @@
/*
* Copyright 2019-2021 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:JvmBlockingBridge
@file:Suppress("OVERLOADS_INTERFACE")
package net.mamoe.mirai.contact.file
import kotlinx.io.errors.IOException
import net.mamoe.kjbb.JvmBlockingBridge
import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.contact.PermissionDeniedException
import net.mamoe.mirai.utils.NotStableForInheritance
import java.io.File
/**
* 绝对文件或目录标识. 精确表示一个远程文件. 不会受同名文件或目录的影响.
*
* @since 2.8
* @see RemoteFiles
* @see AbsoluteFile
* @see AbsoluteFolder
*/
@NotStableForInheritance
public sealed interface AbsoluteFileFolder {
/**
* 该对象所属 [FileSupported]
*/
public val contact: FileSupported
/**
* 上级 [AbsoluteFileFolder].
*
* - 当该 [AbsoluteFileFolder] 表示一个目录中的文件时返回文件所属目录的 [AbsoluteFolder].
* - 当该 [AbsoluteFileFolder] 表示子目录时返回父目录的 [AbsoluteFolder].
*
* 特别地,
* - 当该 [AbsoluteFileFolder] 表示根目录下的一个文件时返回根目录的 [AbsoluteFolder].
* - 当该 [AbsoluteFileFolder] 表示根目录时返回 `null` (表示无上级).
*
* 也就是说, [AbsoluteFileFolder.parent] `null`, 那么该 [AbsoluteFileFolder] 就表示根目录.
*/
public val parent: AbsoluteFolder?
/**
* 文件或目录的 ID, `fileId` `folderId`. 该属性由服务器维护, 通常唯一且持久.
*/
public val id: String
/**
* 文件名或目录名.
*
* 注意, 当远程文件或目录被 (其他人) 改名时, [name] 不会变动.
* 只有在调用 [renameTo] [refresh] 时才会更新.
*
* 不会包含 `:*?"<>|/\` 任一字符.
*/
public val name: String
/**
* 绝对路径, `/foo/bar.txt`.
*
* 注意, 当远程文件或目录被 (其他人) 移动到其他位置或其父目录名称改名时, [absolutePath] 不会变动.
* 只有在调用 [renameTo] [refresh] 等时才会更新.
*/
public val absolutePath: String
/**
* 表示远程文件时返回 `true`.
*/
public val isFile: Boolean
/**
* 表示远程目录时返回 `true`.
*/
public val isFolder: Boolean
/**
* 远程文件或目录的创建时间, 时间戳秒.
*/
public val uploadTime: Long
/**
* 远程文件或目录的最后修改时间戳, 单位秒.
*
* 注意, 当远程文件或目录被 (其他人) 改动时, [lastModifiedTime] 不会变动.
* 只有在调用 [renameTo] [refresh] 等时才会更新.
*/
public val lastModifiedTime: Long
/**
* 上传者 ID.
*/
public val uploaderId: Long
/**
* 查询该远程文件或目录是否还存在于服务器.
*
* 只会精确地按 [id] 检查, 而不会考虑同名文件或目录. 当文件或目录存在时返回 `true`.
*
* 该操作不会更新 [absolutePath] 等属性.
*/
public suspend fun exists(): Boolean
/**
* 重命名远程文件或目录, **并且**修改当前(`this`) [AbsoluteFileFolder] [name].
* 成功时返回 `true`, 当远程文件或目录不存在时返回 `false`.
*
* 注意该操作有可能产生同名文件或目录 (当服务器已经存在一个名称为 [newName] 的文件或目录时).
*
* @throws IOException 当发生网络错误时可能抛出
* @throws IllegalStateException 当发生已知的协议错误时抛出
* @throws PermissionDeniedException 当无管理员权限时抛出 (若群仅允许管理员上传)
*/
public suspend fun renameTo(newName: String): Boolean
/**
* 删除远程文件或目录. 只会根据 [id] 精确地删除一个文件或目录, 不会删除其他同名文件或目录.
* 成功时返回 `true`, 当远程文件或目录不存在时返回 `false`.
*
* 若目录非空, 则会删除目录中的所有文件. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时抛出异常.
*
* @throws IOException 当发生网络错误时可能抛出
* @throws IllegalStateException 当发生已知的协议错误时抛出
* @throws PermissionDeniedException 当无管理员权限时抛出
*/
public suspend fun delete(): Boolean
/**
* 更新当前 [AbsoluteFileFolder] 对象的文件或目录信息 ([lastModifiedTime], [absolutePath] ).
* 成功时返回 `true`, 当远程文件或目录不存在时返回 `false`.
*/
public suspend fun refresh(): Boolean
/**
* 返回更新了文件或目录信息 ([lastModifiedTime] ) , 指向相同文件的 [AbsoluteFileFolder].
* 不会更新当前 [AbsoluteFileFolder] 对象.
*
* 当远程文件或目录不存在时返回 `null`.
*
* 该函数会遍历上级目录的所有文件并匹配当前文件, 因此可能会非常慢, 请不要频繁使用.
*/
public suspend fun refreshed(): AbsoluteFileFolder?
public override fun toString(): String
public companion object {
/**
* 返回去掉文件后缀的文件名. `foo.txt` 返回 `foo`.
*
* 注意, 当远程文件或目录被 (其他人) 改名时, [nameWithoutExtension] 不会变动.
* 只有在调用 [renameTo] [refresh] 时才会更新.
*
* 不会包含 `:*?"<>|/\` 任一字符.
*
* @see File.nameWithoutExtension
*/
@get:JvmStatic
public val AbsoluteFileFolder.nameWithoutExtension: String
get() = name.substringBeforeLast('.')
/**
* 返回文件的后缀名. `foo.txt` 返回 `txt`.
*
* 注意, 当远程文件或目录被 (其他人) 改名时, [extension] 不会变动.
* 只有在调用 [renameTo] [refresh] 时才会更新.
*
* 不会包含 `:*?"<>|/\` 任一字符.
*
* @see File.extension
*/
@get:JvmStatic
public val AbsoluteFileFolder.extension: String
get() = name.substringAfterLast('.', "")
}
}

View File

@ -0,0 +1,196 @@
/*
* Copyright 2019-2021 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:JvmBlockingBridge
@file:Suppress("OVERLOADS_INTERFACE")
package net.mamoe.mirai.contact.file
import kotlinx.coroutines.flow.Flow
import net.mamoe.kjbb.JvmBlockingBridge
import net.mamoe.mirai.contact.PermissionDeniedException
import net.mamoe.mirai.utils.ExternalResource
import net.mamoe.mirai.utils.JavaFriendlyAPI
import net.mamoe.mirai.utils.NotStableForInheritance
import net.mamoe.mirai.utils.ProgressionCallback
import java.util.stream.Stream
/**
* 绝对目录标识. 精确表示一个远程目录. 不会受同名文件或目录的影响.
*
* @since 2.8
* @see RemoteFiles
* @see AbsoluteFile
* @see AbsoluteFileFolder
*/
@NotStableForInheritance
public interface AbsoluteFolder : AbsoluteFileFolder {
/**
* 当前快照中文件数量, 当有文件更新时(上传/删除文件) 该属性不会更新.
*
* 只可能通过 [refresh] 手动刷新
*
* 特别的, 若该目录表示根目录, [contentsCount] 返回 `0`. (无法快速获取)
*/
public val contentsCount: Int
/**
* 当该目录为空时返回 `true`.
*/
public fun isEmpty(): Boolean = contentsCount == 0
/**
* 返回更新了文件或目录信息 ([lastModifiedTime] ) , 指向相同文件的 [AbsoluteFileFolder].
* 不会更新当前 [AbsoluteFileFolder] 对象.
*
* 当远程文件或目录不存在时返回 `null`.
*
* 该函数会遍历上级目录的所有文件并匹配当前文件, 因此可能会非常慢, 请不要频繁使用.
*/
override suspend fun refreshed(): AbsoluteFolder?
///////////////////////////////////////////////////////////////////////////
// list children
///////////////////////////////////////////////////////////////////////////
/**
* 获取该目录下所有子目录列表.
*/
public suspend fun folders(): Flow<AbsoluteFolder>
/**
* 获取该目录下所有子目录列表.
*
* 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [folders], 因此不建议在 Kotlin 使用. Kotlin 请使用 [folders].
*/
@JavaFriendlyAPI
public suspend fun foldersStream(): Stream<AbsoluteFolder>
/**
* 获取该目录下所有文件列表.
*/
public suspend fun files(): Flow<AbsoluteFile>
/**
* 获取该目录下所有文件列表.
*
* 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [files], 因此不建议在 Kotlin 使用. Kotlin 请使用 [files].
*/
@JavaFriendlyAPI
public suspend fun filesStream(): Stream<AbsoluteFile>
/**
* 获取该目录下所有文件和子目录列表.
*/
public suspend fun children(): Flow<AbsoluteFileFolder>
/**
* 获取该目录下所有文件和子目录列表.
*
* 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [children], 因此不建议在 Kotlin 使用. Kotlin 请使用 [children].
*/
@JavaFriendlyAPI
public suspend fun childrenStream(): Stream<AbsoluteFileFolder>
///////////////////////////////////////////////////////////////////////////
// resolve and upload
///////////////////////////////////////////////////////////////////////////
/**
* 创建一个名称为 [name] 的子目录. 返回成功创建的或已有的子目录. 当目标目录已经存在时则直接返回该目录.
*
* @throws IllegalArgumentException [name] 为空或包含非法字符 (`:*?"<>|`) 时抛出
* @throws PermissionDeniedException 当权限不足时抛出
*/
public suspend fun createFolder(name: String): AbsoluteFolder
/**
* 获取一个已存在的名称为 [name] 的子目录. 当该名称的子目录不存在时返回 `null`.
*
* @throws IllegalArgumentException [name] 为空或包含非法字符 (`:*?"<>|`) 时抛出
*/
public suspend fun resolveFolder(name: String): AbsoluteFolder?
/**
* 精确获取 [AbsoluteFile.id] [id] 的文件. 在目标文件不存在时返回 `null`. [deep] `true` 时还会深入子目录查找.
*/
@JvmOverloads
public suspend fun resolveFileById(
id: String,
deep: Boolean = false
): AbsoluteFile?
/**
* 根据路径获取指向的所有路径为 [path] 的文件列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件.
*/
public suspend fun resolveFiles(
path: String
): Flow<AbsoluteFile>
/**
* 根据路径获取指向的所有路径为 [path] 的文件列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件.
*
* 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [resolveFiles], 因此不建议在 Kotlin 使用. Kotlin 请使用 [resolveFiles].
*/
@JavaFriendlyAPI
public suspend fun resolveFilesStream(
path: String
): Stream<AbsoluteFile>
/**
* 根据路径获取指向的所有路径为 [path] 的文件和目录列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件和目录.
*/
public suspend fun resolveAll(
path: String
): Flow<AbsoluteFileFolder>
/**
* 根据路径获取指向的所有路径为 [path] 的文件和目录列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件和目录.
*
* 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [resolveAll], 因此不建议在 Kotlin 使用. Kotlin 请使用 [resolveAll].
*/
@JavaFriendlyAPI
public suspend fun resolveAllStream(
path: String
): Stream<AbsoluteFileFolder>
/**
* 上传一个文件到该目录, 返回上传成功的文件标识.
*
* 会在必要时尝试创建远程目录.
*
* ### [filepath]
*
* - 可以是 `foo.txt` 表示该目录下的文件 "foo.txt"
* - 也可以是 `sub/foo.txt` 表示该目录的子目录 "sub" 下的文件 "foo.txt".
* - 或是绝对路径 `/sub/foo.txt` 表示根目录的 "sub" 目录下的文件 "foo.txt"
*
* @param filepath 目标文件名
* @param content 文件内容
* @param callback 下载进度回调, 传递的 `progression` 是已下载字节数.
*
* @throws PermissionDeniedException 当无管理员权限时抛出 (若群仅允许管理员上传)
*/
@JvmOverloads
public suspend fun uploadNewFile(
filepath: String,
content: ExternalResource,
callback: ProgressionCallback<AbsoluteFile, Long>? = null,
): AbsoluteFile
public companion object {
/**
* 根目录 folder ID.
* @see id
*/
public const val ROOT_FOLDER_ID: String = "/"
}
}

View File

@ -0,0 +1,141 @@
/*
* Copyright 2019-2021 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:JvmBlockingBridge
@file:Suppress("OVERLOADS_INTERFACE")
package net.mamoe.mirai.contact.file
import kotlinx.coroutines.flow.Flow
import net.mamoe.kjbb.JvmBlockingBridge
import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.contact.PermissionDeniedException
import net.mamoe.mirai.utils.ExternalResource
import net.mamoe.mirai.utils.NotStableForInheritance
import net.mamoe.mirai.utils.ProgressionCallback
import java.io.File
import java.util.stream.Stream
/**
* 表示远程文件列表 (管理器).
*
* [RemoteFiles] 包含一些协议接口,
*
* # 文件和目录操作
*
* 文件和目录的父类型是 [AbsoluteFileFolder].
*
* - [AbsoluteFile] 表示一个文件
* - [AbsoluteFolder] 表示一个目录
*
* 每个文件或目录都拥有一个唯一 ID: [AbsoluteFileFolder.id]. ID 由服务器提供, 在重命名或移动时不会变化.
*
* 文件名可以通过 [AbsoluteFileFolder.name] 获得, 但注意文件名和其他属性都会随重命名或移动等操作更新.
*
* 除根目录 [root] , 每个文件或目录都拥有父目录 [AbsoluteFileFolder.parent].
*
* # 根目录
*
* 除了 [RemoteFiles] 中定义的捷径外, 一切文件目录操作都以获取根目录开始. 可通过 [RemoteFiles.root] 获取表示根目录的 [AbsoluteFolder].
*
* # 绝对路径与相对路径
*
* mirai 文件系统的绝对路径与相对路径与 Java [File] 实现的相同.
*
* `/` 起始的路径表示绝对路径, 基于根目录 [root] 处理. 其他路径均表示相对路径.
*
* 可由 [AbsoluteFileFolder.absolutePath] 获取其绝对路径. 值得注意的是, 所有文件与目录对象都表示绝对路径下的目标, 因此它们都总是精确地表示一个目标, 而不受环境影响.
*
* 除重命名外, 所有文件和目录操作都默认同时支持上述两种路径.
*
* # 操作 [AbsoluteFileFolder]
*
* ## 重命名, 移动
*
* [AbsoluteFileFolder.renameTo], [AbsoluteFile.moveTo] 提供重命名和移动功能. 注意目录不支持移动.
*
* ## 获取目录中的子目录和文件列表
*
* 一个目录 ([AbsoluteFolder]) 可以包含多个子文件, 根目录还可以包含多个子目录 (详见下文 '目录结构限制').
*
* 使用 [AbsoluteFolder.children] 可以获得其内子目录和文件列表 [Flow]. [AbsoluteFolder.childrenStream] 提供适合 Java [Stream] 实现.
* 使用 [AbsoluteFolder.folders] [AbsoluteFolder.files] 可以特定地只获取子目录或文件列表. 这些函数也有其 `*Stream` 实现.
*
* 若要根据确定的文件或目录名称获取其 [AbsoluteFileFolder] 实例, 可使用 [AbsoluteFolder.resolveFiles] [AbsoluteFolder.resolveFiles].
* 注意 [AbsoluteFolder.resolveFiles] 返回 [Flow] ( Stream 版返回 [Stream]), 因为服务器允许多个文件有相同名称. (详见下文 '允许重名').
*
* 若已知文件 [AbsoluteFile.id], 可通过 [AbsoluteFolder.resolveFileById] 获得该文件.
*
* ## 上传新文件
* 可使用 [AbsoluteFolder.uploadNewFile] 上传新文件. 也可以通过 [RemoteFiles.uploadNewFile] 直接上传而跳过获取目录的步骤 (因为目录不允许同名).
*
* ## 覆盖一个旧文件
* 服务器不允许覆盖文件. 只能通过 [AbsoluteFile.delete] 删除文件后再上传新文件. 注意新旧文件的 [AbsoluteFile.id] 会不同.
*
* # 操作权限
* 操作一个目录时总是需要管理员权限. 若群设置 "允许任何人上传文件", 则上传文件和操作自己上传的文件时都不需要特殊权限. 注意, 操作他人的文件时总是需要管理员权限.
*
* # 服务器限制
*
* ## 目录结构限制
*
* mirai 2.8.0 发布时, 服务器仅允许两层目录结构. 也就是说只允许根目录存在子目录, 子目录不能包含另一个子目录.
*
* 为了考虑将来服务器可能升级, mirai 没有做实现上的限制. mirai 所有操作都支持多层目录, 但进行这样的操作时将会得到服务器错误, 方法会抛出 [IllegalStateException].
*
* ## 允许重名
*
* 服务器允许同名目录和文件存在. 如下同名的三个文件与一个目录是允许的, 但它们的 [AbsoluteFileFolder.id] 都互不相同:
* ```
* foo
* |- test (目录)
* |- test (文件)
* |- test (文件)
* |- test (文件)
* ```
* 注意, 目录不允许同名.
*
* [AbsoluteFileFolder] 依据 [AbsoluteFileFolder.id] 定位文件, 而不是通过文件名. 因此 [AbsoluteFileFolder] 总是精确地代表一个文件或目录.
*
* @since 2.8
* @see FileSupported
*/
@NotStableForInheritance
public interface RemoteFiles {
/**
* 获取表示根目录的 [AbsoluteFolder]
*/
public val root: AbsoluteFolder
/**
* 该对象所属 [FileSupported]
*/
public val contact: FileSupported
/**
* 上传一个文件到指定精确路径. 返回指代该远程文件的 [AbsoluteFile].
*
* 会在必要时尝试创建远程目录.
*
* 也可以使用 [AbsoluteFolder.uploadNewFile].
*
* @param filepath 文件路径, **包含目标文件名**. `/foo/bar.txt`. 若是相对目录则基于 [根目录][root] 处理.
* @param content 文件内容
* @param callback 下载进度回调, 传递的 `progression` 是已下载字节数.
*
* @throws PermissionDeniedException 当无管理员权限时抛出 (若群仅允许管理员上传)
*/
@JvmOverloads
public suspend fun uploadNewFile(
filepath: String,
content: ExternalResource,
callback: ProgressionCallback<AbsoluteFile, Long>? = null,
): AbsoluteFile = root.uploadNewFile(filepath, content, callback)
}

View File

@ -19,6 +19,7 @@ import kotlinx.serialization.Serializable
import net.mamoe.kjbb.JvmBlockingBridge
import net.mamoe.mirai.Mirai
import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.contact.file.AbsoluteFile
import net.mamoe.mirai.event.events.MessageEvent
import net.mamoe.mirai.message.code.CodableMessage
import net.mamoe.mirai.message.code.internal.appendStringAsMiraiCode
@ -40,6 +41,7 @@ import net.mamoe.mirai.utils.*
@Serializable(FileMessage.Serializer::class)
@SerialName(FileMessage.SERIAL_NAME)
@NotStableForInheritance
@JvmBlockingBridge
public interface FileMessage : MessageContent, ConstrainSingle, CodableMessage {
/**
* 服务器需要的某种 ID.
@ -74,11 +76,20 @@ public interface FileMessage : MessageContent, ConstrainSingle, CodableMessage {
/**
* 获取一个对应的 [RemoteFile]. 当目标群或好友不存在这个文件时返回 `null`.
*/
@JvmBlockingBridge
@Suppress("DEPRECATION")
@Deprecated("Please use toAbsoluteFile", ReplaceWith("this.toAbsoluteFile(contact)")) // deprecated since 2.8.0-RC
public suspend fun toRemoteFile(contact: FileSupported): RemoteFile? {
@Suppress("DEPRECATION")
return contact.filesRoot.resolveById(id)
}
/**
* 获取一个对应的 [RemoteFile]. 当目标群或好友不存在这个文件时返回 `null`.
*
* @since 2.8
*/
public suspend fun toAbsoluteFile(contact: FileSupported): AbsoluteFile?
override val key: Key get() = Key
/**

View File

@ -437,6 +437,11 @@ public interface ExternalResource : Closeable {
* @see RemoteFile.path
* @see RemoteFile.uploadAndSend
*/
@Suppress("DEPRECATION")
@Deprecated(
"Deprecated. Please use AbsoluteFolder.uploadNewFile",
ReplaceWith("contact.files.uploadNewFile(path, this, callback)")
) // deprecated since 2.8.0-RC
@JvmStatic
@JvmBlockingBridge
@JvmOverloads
@ -456,6 +461,11 @@ public interface ExternalResource : Closeable {
* @see RemoteFile.path
* @see RemoteFile.uploadAndSend
*/
@Suppress("DEPRECATION")
@Deprecated(
"Deprecated. Please use AbsoluteFolder.uploadNewFile",
ReplaceWith("contact.files.uploadNewFile(path, this, callback)")
) // deprecated since 2.8.0-RC
@JvmStatic
@JvmBlockingBridge
@JvmName("sendAsFile")

View File

@ -0,0 +1,103 @@
/*
* Copyright 2019-2021 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
*/
package net.mamoe.mirai.utils
import kotlinx.coroutines.channels.SendChannel
import net.mamoe.mirai.contact.file.AbsoluteFile
import net.mamoe.mirai.utils.ProgressionCallback.Companion.asProgressionCallback
/**
* 操作进度回调, 可供前端使用, 以提供进度显示.
*
* @param S subject, 操作对象, [AbsoluteFile]
* @param P progression, 用于提示进度. 如当下载文件时为已下载文件大小字节数 [Long].
*
* @see asProgressionCallback
*
* @since 2.8
*/
public interface ProgressionCallback<in S, in P> {
/**
* 当操作开始时调用
*/
public fun onBegin(subject: S, resource: ExternalResource) {}
/**
* 每当有进度更新时调用. 此方法可能会同时被多个线程调用.
*/
public fun onProgression(subject: S, resource: ExternalResource, progression: P) {}
/**
* 当操作成功时调用.
*
* 在默认实现下只会由 [onFinished] 调用
*/
public fun onSuccess(subject: S, resource: ExternalResource, progression: P) {}
/**
* 当操作以异常失败时调用.
*
* 在默认实现下只会由 [onFinished] 调用
*/
public fun onFailure(subject: S, resource: ExternalResource, exception: Throwable) {}
/**
* 当操作完成时调用.
*/
public fun onFinished(subject: S, resource: ExternalResource, result: Result<P>) {
result.fold(
onSuccess = { onSuccess(subject, resource, it) },
onFailure = { onFailure(subject, resource, it) },
)
}
public companion object {
/**
* 将一个 [SendChannel] 作为 [ProgressionCallback] 使用.
*
* ## 下载文件的使用示例
*
* 每当有进度更新, 已下载的字节数都会被[发送][SendChannel.offer] [SendChannel] .
* 进度的发送会通过 [offer][SendChannel.offer], 而不是通过 [send][SendChannel.send]. 意味着 [SendChannel] 通常要实现缓存.
*
* [closeOnFinish] `true`, 当下载完成 (无论是失败还是成功) 时会 [关闭][SendChannel.close] [SendChannel].
*
* 使用示例:
* ```
* val progress = Channel<Long>(Channel.BUFFERED)
*
* launch {
* // 每 3 秒发送一次操作进度百分比
* progress.receiveAsFlow().sample(Duration.seconds(3)).collect { bytes ->
* group.sendMessage("File upload: ${(bytes.toDouble() / resource.size * 100).toInt() / 100}%.") // 保留 2 位小数
* }
* }
*
* group.files.uploadNewFile("/foo.txt", resource, callback = progress.asProgressionCallback(true))
* group.sendMessage("File uploaded successfully.")
* ```
*
* 直接使用 [ProgressionCallback] 也可以实现示例这样的功能, [asProgressionCallback] 是为了简化操作.
*/
@JvmStatic
public fun <S, P> SendChannel<P>.asProgressionCallback(closeOnFinish: Boolean = true): ProgressionCallback<S, P> {
return object : ProgressionCallback<S, P> {
override fun onProgression(subject: S, resource: ExternalResource, progression: P) {
trySend(progression)
}
override fun onFinished(subject: S, resource: ExternalResource, result: Result<P>) {
if (closeOnFinish) this@asProgressionCallback.close(result.exceptionOrNull())
}
}
}
}
}

View File

@ -7,7 +7,7 @@
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
@file:Suppress("unused")
@file:Suppress("unused", "DEPRECATION")
@file:JvmBlockingBridge
package net.mamoe.mirai.utils
@ -97,6 +97,7 @@ import java.io.File
* @see FileSupported
* @since 2.5
*/
@Deprecated("Please use RemoteFiles and AbsoluteFileFolder form fileSupported.files") // deprecated since 2.8.0-RC
@NotStableForInheritance
public interface RemoteFile {
/**
@ -348,6 +349,10 @@ public interface RemoteFile {
* 上传进度回调, 可供前端使用, 以提供进度显示.
* @see asProgressionCallback
*/
@Deprecated(
"Deprecated without replacement. Please use AbsoluteFolder.uploadNewFile",
ReplaceWith("contact.files.uploadNewFile(path, this, callback)")
) // deprecated since 2.8.0-RC
public interface ProgressionCallback {
/**
* 当上传开始时调用
@ -610,6 +615,11 @@ public interface RemoteFile {
*/
@JvmStatic
@JvmOverloads
@Deprecated(
"Deprecated. Please use AbsoluteFolder.uploadNewFile or RemoteFiles.uploadNewFile",
ReplaceWith("this.files.uploadNewFile(path, resource, callback)"),
level = DeprecationLevel.WARNING
) // deprecated since 2.8.0-RC
public suspend fun <C : FileSupported> C.sendFile(
path: String,
resource: ExternalResource,
@ -624,6 +634,11 @@ public interface RemoteFile {
*/
@JvmStatic
@JvmOverloads
@Deprecated(
"Deprecated. Please use AbsoluteFolder.uploadNewFile or RemoteFiles.uploadNewFile",
ReplaceWith("file.toExternalResource().use { this.files.uploadNewFile(path, it, callback) }"),
level = DeprecationLevel.WARNING
) // deprecated since 2.8.0-RC
public suspend fun <C : FileSupported> C.sendFile(
path: String,
file: File,

View File

@ -150,4 +150,13 @@ public inline fun <R> Result<R>.mapFailure(
block: (Throwable) -> Throwable,
): Result<R> = onFailure {
return Result.failure(block(it))
}
}
public inline fun <R> Result<R>.onSuccessCatching(block: () -> Unit): Result<R> {
if (isSuccess) {
runCatching(block).onFailure {
return@onSuccessCatching Result.failure(it)
}
}
return this
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2019-2021 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
*/
package net.mamoe.mirai.internal.contact
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.internal.asQQAndroidBot
internal interface ContactAware {
val contact: Contact
val bot get() = contact.bot.asQQAndroidBot()
val client get() = bot.client
}

View File

@ -16,12 +16,14 @@ import net.mamoe.mirai.LowLevelApi
import net.mamoe.mirai.Mirai
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.contact.announcement.Announcements
import net.mamoe.mirai.contact.file.RemoteFiles
import net.mamoe.mirai.data.GroupInfo
import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.contact.announcement.AnnouncementsImpl
import net.mamoe.mirai.internal.contact.file.RemoteFilesImpl
import net.mamoe.mirai.internal.contact.info.MemberInfoImpl
import net.mamoe.mirai.internal.message.*
import net.mamoe.mirai.internal.network.components.BdhSession
@ -106,7 +108,10 @@ internal class GroupImpl constructor(
override lateinit var owner: NormalMemberImpl
override lateinit var botAsMember: NormalMemberImpl
@Suppress("DEPRECATION")
@Deprecated("Please use files instead.", replaceWith = ReplaceWith("files.root"))
override val filesRoot: RemoteFile by lazy { RemoteFileImpl(this, "/") }
override val files: RemoteFiles by lazy { RemoteFilesImpl(this) }
override val announcements: Announcements by lazy {

View File

@ -0,0 +1,172 @@
/*
* Copyright 2019-2021 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
*/
package net.mamoe.mirai.internal.contact.file
import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.contact.file.AbsoluteFile
import net.mamoe.mirai.contact.file.AbsoluteFolder
import net.mamoe.mirai.internal.message.FileMessageImpl
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.message.data.FileMessage
import net.mamoe.mirai.utils.toUHexString
internal class AbsoluteFileImpl(
contact: FileSupported,
parent: AbsoluteFolder?,
id: String,
name: String,
uploadTime: Long,
lastModifiedTime: Long,
uploaderId: Long,
override var expiryTime: Long,
override val size: Long, // when file is changed, its id will also be changed, so no need to be var
override val sha1: ByteArray,
override val md5: ByteArray,
busId: Int,
) : AbsoluteFile, AbstractAbsoluteFileFolder(
contact, parent, id, name, uploadTime, uploaderId, lastModifiedTime,
busId
) {
override fun checkPermission(operationHint: String) {
// TODO: 30/10/2021 checkPermission: 群可以设置允许任何人上传而目前没有检测这个属性, 因此不能实现权限判定
// if (uploaderId == bot.id) return
// if (contact is GroupImpl && !contact.botPermission.isOperator()) throwPermissionDeniedException(operationHint)
// return
}
override val isFile: Boolean get() = true
override val isFolder: Boolean get() = false
override val absolutePath: String
get() {
val parent = parent
return when {
parent == null || parent.name == "/" -> "/$name"
else -> "${parent.absolutePath}/$name"
}
}
override suspend fun exists(): Boolean {
return FileManagement.GetFileInfo(
client,
groupCode = contact.id,
busId = busId,
fileId = id
).sendAndExpect(bot)
.toResult("AbsoluteFileImpl.exists", checkResp = false)
.getOrThrow()
.fileInfo != null
}
override suspend fun moveTo(folder: AbsoluteFolder): Boolean {
if (folder.contact != this.contact) {
error("Cross-group file operation is not yet supported.")
}
if (folder.absolutePath == this.parentOrRoot.absolutePath) return true
checkPermission("moveTo")
val result = FileManagement.MoveFile(client, contact.id, busId, id, parent.idOrRoot, folder.idOrRoot)
.sendAndExpect(bot).toResult("AbsoluteFileImpl.moveTo", checkResp = false)
.getOrThrow()
return when (result.int32RetCode) {
-36 -> throwPermissionDeniedException("moveTo")
0 -> {
parent = folder
true
}
else -> {
false
}
}
// } else {
// return FileManagement.RenameFolder(client, contact.id, id, name).sendAndExpect(bot)
// .toResult("RemoteFile.moveTo", checkResp = false).getOrThrow().int32RetCode == 0
// }
}
override suspend fun getUrl(): String? {
// Known error
// java.lang.IllegalStateException: Failed AbsoluteFileImpl.getUrl, result=-303, msg=param error: bus_id
// java.lang.IllegalStateException: Failed AbsoluteFileImpl.getUrl, result=-103, msg=GetFileAttrAction file not exist
val resp = FileManagement.RequestDownload(
client,
groupCode = contact.id,
busId = busId,
fileId = id
).sendAndExpect(bot)
.toResult("AbsoluteFileImpl.getUrl")
.getOrElse { return null }
return "http://${resp.downloadIp}/ftn_handler/${resp.downloadUrl.toUHexString("")}/?fname=" +
id.toByteArray().toUHexString("")
}
override fun toMessage(): FileMessage {
return FileMessageImpl(id, busId, name, size)
}
override suspend fun refresh(): Boolean {
val new = refreshed() ?: return false
this.parent = new.parent
this.expiryTime = new.expiryTime
this.name = new.name
this.lastModifiedTime = new.lastModifiedTime
return true
}
override fun toString(): String = "AbsoluteFile(name=$name, absolutePath=$absolutePath, id=$id)"
override suspend fun refreshed(): AbsoluteFile? {
val result = FileManagement.GetFileInfo(client, contact.id, id, busId)
.sendAndExpect(bot)
.toResult("AbsoluteFile.refreshed")
.getOrNull()?.fileInfo
?: return null
return if (result.parentFolderId == this.parentOrRoot.id) {
this.parentOrRoot.impl().createChildFile(result)
} else {
null
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as AbsoluteFileImpl
if (expiryTime != other.expiryTime) return false
if (size != other.size) return false
if (!sha1.contentEquals(other.sha1)) return false
if (!md5.contentEquals(other.md5)) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + expiryTime.hashCode()
result = 31 * result + size.hashCode()
result = 31 * result + sha1.contentHashCode()
result = 31 * result + md5.contentHashCode()
return result
}
}

View File

@ -0,0 +1,465 @@
/*
* Copyright 2019-2021 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
*/
package net.mamoe.mirai.internal.contact.file
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.contact.file.AbsoluteFile
import net.mamoe.mirai.contact.file.AbsoluteFileFolder
import net.mamoe.mirai.contact.file.AbsoluteFolder
import net.mamoe.mirai.contact.isOperator
import net.mamoe.mirai.internal.contact.GroupImpl
import net.mamoe.mirai.internal.contact.file.RemoteFilesImpl.Companion.findFileByPath
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.components.ClockHolder.Companion.clock
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.network.protocol.packet.sendAndExpect
import net.mamoe.mirai.internal.utils.FileSystem
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
import net.mamoe.mirai.utils.*
import java.util.stream.Stream
import kotlin.streams.asStream
internal fun Oidb0x6d8.GetFileListRspBody.Item.resolved(parent: AbsoluteFolderImpl): AbsoluteFileFolder? {
val item = this
return when {
item.fileInfo != null -> {
parent.createChildFile(item.fileInfo)
}
item.folderInfo != null -> {
parent.createChildFolder(item.folderInfo)
}
else -> null
}
}
internal fun AbsoluteFolderImpl.createChildFolder(
folderInfo: GroupFileCommon.FolderInfo
): AbsoluteFolderImpl = AbsoluteFolderImpl(
contact = contact,
parent = this,
id = folderInfo.folderId,
name = folderInfo.folderName,
uploadTime = folderInfo.createTime.toLongUnsigned(),
uploaderId = folderInfo.createUin,
lastModifiedTime = folderInfo.modifyTime.toLongUnsigned(),
contentsCount = folderInfo.totalFileCount
)
internal fun AbsoluteFolderImpl.createChildFile(
info: GroupFileCommon.FileInfo
): AbsoluteFileImpl = AbsoluteFileImpl(
contact = contact,
parent = this,
id = info.fileId,
name = info.fileName,
uploadTime = info.uploadTime.toLongUnsigned(),
lastModifiedTime = info.modifyTime.toLongUnsigned(),
uploaderId = info.uploaderUin,
expiryTime = info.deadTime.toLongUnsigned(),
size = info.fileSize,
sha1 = info.sha,
md5 = info.md5,
busId = info.busId
)
internal class AbsoluteFolderImpl(
contact: FileSupported, parent: AbsoluteFolder?, id: String, name: String,
uploadTime: Long, uploaderId: Long, lastModifiedTime: Long,
override var contentsCount: Int,
) : AbstractAbsoluteFileFolder(
contact,
parent, id, name, uploadTime, uploaderId, lastModifiedTime, 0
), AbsoluteFolder {
override fun checkPermission(operationHint: String) {
// 目录权限不受 '允许任何人上传' 设置的影响
if (contact is GroupImpl && !contact.botPermission.isOperator()) throwPermissionDeniedException(operationHint)
return
}
override val isFile: Boolean get() = false
override val isFolder: Boolean get() = true
override val absolutePath: String
get() {
val parent = parent
return when {
parent == null || this.id == "/" -> "/"
parent.parent == null || parent.id == "/" -> "/$name"
else -> "${parent.absolutePath}/$name"
}
}
companion object {
suspend fun getItemsFlow(
client: QQAndroidClient,
contact: FileSupported,
folderId: String
): Flow<Oidb0x6d8.GetFileListRspBody.Item> {
return flow {
var index = 0
while (true) {
val list = FileManagement.GetFileList(
client,
groupCode = contact.id,
folderId = folderId,
startIndex = index
).sendAndExpect(client.bot).toResult("AbsoluteFolderImpl.getFilesFlow").getOrThrow()
index += list.itemList.size
if (list.int32RetCode != 0) return@flow
if (list.itemList.isEmpty()) return@flow
emitAll(list.itemList.asFlow())
}
}
}
suspend fun uploadNewFileImpl(
folder: AbsoluteFolderImpl,
filepath: String,
content: ExternalResource,
callback: ProgressionCallback<AbsoluteFile, Long>?
): AbsoluteFile {
if (filepath.isBlank()) throw IllegalArgumentException("filename cannot be blank.")
// TODO: 12/10/2021 checkPermission for AbsoluteFolderImpl.upload
content.withAutoClose {
val resp = FileManagement.RequestUpload(
folder.client,
groupCode = folder.contact.id,
folderId = folder.id,
resource = content,
filename = filepath
).sendAndExpect(folder.bot).toResult("AbsoluteFolderImpl.upload").getOrThrow()
when (resp.int32RetCode) {
-36 -> folder.throwPermissionDeniedException("uploadNewFile")
}
val file = AbsoluteFileImpl(
contact = folder.contact,
parent = folder,
id = resp.fileId,
name = filepath,
uploadTime = folder.bot.clock.server.currentTimeSeconds(),
lastModifiedTime = folder.bot.clock.server.currentTimeSeconds(),
expiryTime = 0,
uploaderId = folder.bot.id,
size = content.size,
sha1 = content.sha1,
md5 = content.md5,
busId = resp.busId
)
if (resp.boolFileExist) {
// resp.boolFileExist:
// 服务器是否存在相同的内容, 只是用来判断可不可以跳过上传
// 当为 true 时跳过上传, 但仍然需要完成 `sendMessage(FileMessage)` 才是正常逻辑
callback?.onBegin(file, content)
val result = kotlin.runCatching {
folder.contact.sendMessage(file.toMessage())
}.map { content.size }
callback?.onFinished(file, content, result)
return file
}
val ext = GroupFileUploadExt(
u1 = 100,
u2 = 1,
entry = GroupFileUploadEntry(
business = ExcitingBusiInfo(
busId = resp.busId,
senderUin = folder.bot.id,
receiverUin = folder.contact.id, // TODO: 2021/3/1 code or uin?
groupCode = folder.contact.id,
),
fileEntry = ExcitingFileEntry(
fileSize = content.size,
md5 = content.md5,
sha1 = content.sha1,
fileId = resp.fileId.toByteArray(),
uploadKey = resp.checkKey,
),
clientInfo = ExcitingClientInfo(
clientType = 2,
appId = folder.client.protocol.id.toString(),
terminalType = 2,
clientVer = "9e9c09dc",
unknown = 4,
),
fileNameInfo = ExcitingFileNameInfo(filepath),
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(file, content)
kotlin.runCatching {
Highway.uploadResourceBdh(
bot = folder.bot,
resource = content,
kind = ResourceKind.GROUP_FILE,
commandId = 71,
extendInfo = ext,
dataFlag = 0,
callback = if (callback == null) null else fun(it: Long) {
callback.onProgression(file, content, it)
}
)
}.let { result0 ->
val result = result0.onSuccessCatching {
folder.contact.sendMessage(file.toMessage())
}
callback?.onFinished(file, content, result.map { content.size })
}
return file
}
}
}
suspend fun getItemsFlow(): Flow<Oidb0x6d8.GetFileListRspBody.Item> = Companion.getItemsFlow(client, contact, id)
@JavaFriendlyAPI
private suspend fun getItemsSequence(): Sequence<Oidb0x6d8.GetFileListRspBody.Item> {
return sequence {
var index = 0
while (true) {
val list = runBlocking {
FileManagement.GetFileList(
client,
groupCode = contact.id,
folderId = id,
startIndex = index
).sendAndExpect(bot)
}.toResult("AbsoluteFolderImpl.getFilesFlow").getOrThrow()
index += list.itemList.size
if (list.int32RetCode != 0) return@sequence
if (list.itemList.isEmpty()) return@sequence
yieldAll(list.itemList)
}
}
}
private fun Oidb0x6d8.GetFileListRspBody.Item.resolve(): AbsoluteFileFolder? = resolved(this@AbsoluteFolderImpl)
override suspend fun folders(): Flow<AbsoluteFolder> {
return getItemsFlow().filter { it.folderInfo != null }.map { it.resolve() as AbsoluteFolder }
}
@JavaFriendlyAPI
override suspend fun foldersStream(): Stream<AbsoluteFolder> {
return getItemsSequence().filter { it.folderInfo != null }.map { it.resolve() as AbsoluteFolder }.asStream()
}
override suspend fun files(): Flow<AbsoluteFile> {
return getItemsFlow().filter { it.fileInfo != null }.map { it.resolve() as AbsoluteFile }
}
@JavaFriendlyAPI
override suspend fun filesStream(): Stream<AbsoluteFile> {
return getItemsSequence().filter { it.fileInfo != null }.map { it.resolve() as AbsoluteFile }.asStream()
}
override suspend fun children(): Flow<AbsoluteFileFolder> {
return getItemsFlow().mapNotNull { it.resolve() }
}
@JavaFriendlyAPI
override suspend fun childrenStream(): Stream<AbsoluteFileFolder> {
return getItemsSequence().mapNotNull { it.resolve() }.asStream()
}
override suspend fun createFolder(name: String): AbsoluteFolder {
if (name.isBlank()) throw IllegalArgumentException("folder name cannot be blank.")
checkPermission("createFolder")
FileSystem.checkLegitimacy(name)
// server only support nesting depth level of 1 so we don't need to check the name
val result = FileManagement.CreateFolder(client, contact.id, this.id, name)
.sendAndExpect(bot).toResult("AbsoluteFolderImpl.mkdir", checkResp = false)
.getOrThrow() // throw protocol errors
/*
2021-10-30 13:06:33 D/soutv: unnamed = CreateFolderRspBody#-941698272 {
folderInfo=FolderInfo#1879610684 {
createTime=0x617D3548(1635595592)
createUin=xxx
folderId=/49a18e46-cf24-4362-b0d0-13235c0e7862
folderName=myFolder
modifyTime=0x617D3548(1635595592)
modifyUin=xxx
parentFolderId=/
usedSpace=0x0000000000000000(0)
}
retMsg=ok
}
*/
/*
2021-10-30 13:03:44 D/soutv: unnamed = CreateFolderRspBody#-941698272 {
clientWording=只允许群主和管理员操作
int32RetCode=0xFFFFFFDC(-36)
retMsg=not group admin
}
*/
/*
2021-10-30 13:10:32 D/soutv: unnamed = CreateFolderRspBody#-941698272 {
clientWording=同名文件夹已存在
int32RetCode=0xFFFFFEC7(-313)
retMsg=folder name has exist
}
*/
return when (result.int32RetCode) {
-36 -> throwPermissionDeniedException("createFolder")
-313 -> this.resolveFolder(name) // already exists
0 -> {
if (result.folderInfo != null) {
this.createChildFolder(result.folderInfo)
} else {
this.resolveFolder(name)
}
}
else -> {
// unexpected errors
error("Failed to create folder '$name': ${result.int32RetCode} ${result.clientWording}.")
}
} ?: error("Failed to create folder '$name': server returned success but failed to find folder.")
}
override suspend fun resolveFolder(name: String): AbsoluteFolder? {
if (name.isBlank()) throw IllegalArgumentException("folder name cannot be blank.")
if (!FileSystem.isLegal(name)) return null
return getItemsFlow().firstOrNull { it.folderInfo?.folderName == name }?.resolve() as AbsoluteFolder?
}
override suspend fun resolveFileById(id: String, deep: Boolean): AbsoluteFile? {
if (id == "/" || id.isEmpty()) throw IllegalArgumentException("Illegal file id: $id")
getItemsFlow().filter { it.fileInfo?.fileId == id }.map { it.resolve() as AbsoluteFile }.firstOrNull()
?.let { return it }
if (!deep) return null
return folders().map { it.resolveFileById(id, deep) }.firstOrNull()
}
override suspend fun resolveFiles(path: String): Flow<AbsoluteFile> {
if (path.isBlank()) throw IllegalArgumentException("path cannot be blank.")
if (!FileSystem.isLegal(path)) return emptyFlow()
if (!path.contains('/')) {
return getItemsFlow().filter { it.fileInfo?.fileName == path }.map { it.resolve() as AbsoluteFile }
}
return resolveFolder(path.substringBefore('/'))?.resolveFiles(path.substringAfter('/')) ?: emptyFlow()
}
@OptIn(JavaFriendlyAPI::class)
override suspend fun resolveFilesStream(path: String): Stream<AbsoluteFile> {
if (path.isBlank()) throw IllegalArgumentException("path cannot be blank.")
if (!FileSystem.isLegal(path)) return Stream.empty()
if (!path.contains('/')) {
return getItemsSequence().filter { it.fileInfo?.fileName == path }.map { it.resolve() as AbsoluteFile }
.asStream()
}
return resolveFolder(path.substringBefore('/'))?.resolveFilesStream(path.substringAfter('/')) ?: Stream.empty()
}
override suspend fun resolveAll(path: String): Flow<AbsoluteFileFolder> {
if (path.isBlank()) throw IllegalArgumentException("path cannot be blank.")
if (!FileSystem.isLegal(path)) return emptyFlow()
if (!path.contains('/')) {
return getItemsFlow().mapNotNull { it.resolve() }
}
return resolveFolder(path.substringBefore('/'))?.resolveAll(path.substringAfter('/')) ?: emptyFlow()
}
@JavaFriendlyAPI
override suspend fun resolveAllStream(path: String): Stream<AbsoluteFileFolder> {
if (path.isBlank()) throw IllegalArgumentException("path cannot be blank.")
if (!FileSystem.isLegal(path)) return Stream.empty()
if (!path.contains('/')) {
return getItemsSequence().mapNotNull { it.resolve() }.asStream()
}
return resolveFolder(path.substringBefore('/'))?.resolveAllStream(path.substringAfter('/')) ?: Stream.empty()
}
override suspend fun uploadNewFile(
filepath: String,
content: ExternalResource,
callback: ProgressionCallback<AbsoluteFile, Long>?
): AbsoluteFile {
val (actualFolder, actualFilename) = findFileByPath(filepath)
return uploadNewFileImpl(actualFolder.impl(), actualFilename, content, callback)
}
override suspend fun exists(): Boolean {
return parentOrFail().folders().firstOrNull { it.id == this.id } != null
}
override suspend fun refresh(): Boolean {
val new = refreshed() ?: return false
this.name = new.name
this.lastModifiedTime = new.lastModifiedTime
this.contentsCount = new.contentsCount
return true
}
override fun toString(): String = "AbsoluteFolder(name=$name, absolutePath=$absolutePath, id=$id)"
override suspend fun refreshed(): AbsoluteFolder? = parentOrRoot.folders().firstOrNull { it.id == this.id }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as AbsoluteFolderImpl
if (contentsCount != other.contentsCount) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + contentsCount.hashCode()
return result
}
}

View File

@ -0,0 +1,147 @@
/*
* Copyright 2019-2021 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("MemberVisibilityCanBePrivate")
package net.mamoe.mirai.internal.contact.file
import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.contact.PermissionDeniedException
import net.mamoe.mirai.contact.file.AbsoluteFile
import net.mamoe.mirai.contact.file.AbsoluteFileFolder
import net.mamoe.mirai.contact.file.AbsoluteFolder
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.internal.utils.FileSystem
import net.mamoe.mirai.utils.cast
internal fun AbstractAbsoluteFileFolder.api(): AbsoluteFileFolder = this.cast()
internal fun AbsoluteFileFolder.impl(): AbstractAbsoluteFileFolder = this.cast()
internal fun AbsoluteFile.impl(): AbsoluteFileImpl = this.cast()
internal fun AbsoluteFolder.impl(): AbsoluteFolderImpl = this.cast()
internal val AbsoluteFolder?.idOrRoot get() = this?.id ?: AbsoluteFolder.ROOT_FOLDER_ID
internal val AbstractAbsoluteFileFolder.parentOrRoot get() = parent ?: contact.files.root
/**
* @see AbsoluteFileFolder
*/
internal abstract class AbstractAbsoluteFileFolder(
// overriding AbsFileFolder
val contact: FileSupported,
var parent: AbsoluteFolder?,
val id: String, // uuid-like
var name: String,
val uploadTime: Long,
val uploaderId: Long,
var lastModifiedTime: Long,
// end
val busId: Int, // protocol internal
) {
protected inline val bot get() = contact.bot.asQQAndroidBot()
protected inline val client get() = bot.client
protected abstract fun checkPermission(operationHint: String)
fun throwPermissionDeniedException(operationHint: String): Nothing {
throw PermissionDeniedException("Permission denied: '$operationHint' on file '${this.api().absolutePath}' requires an operator permission.")
}
protected fun parentOrFail() = parent ?: error("Cannot rename the root folder.")
///////////////////////////////////////////////////////////////////////////
// overriding AbsFileFolder
///////////////////////////////////////////////////////////////////////////
protected abstract val isFile: Boolean
protected abstract val isFolder: Boolean
suspend fun renameTo(newName: String): Boolean {
FileSystem.checkLegitimacy(newName)
parentOrFail()
checkPermission("renameTo")
val result = if (isFile) {
FileManagement.RenameFile(client, contact.id, busId, id, parent.idOrRoot, newName)
} else {
FileManagement.RenameFolder(client, contact.id, id, newName)
}.sendAndExpect(bot)
result.toResult("AbstractAbsoluteFileFolder.renameTo") {
when (it) {
0 -> {
name = newName
return true
}
1 -> return false
else -> false
}
}.getOrThrow()
error("unreachable")
}
suspend fun delete(): Boolean {
checkPermission("delete")
val result = if (isFile) {
FileManagement.DeleteFile(client, contact.id, busId, id, parent.idOrRoot).sendAndExpect(bot)
} else {
// natively 'recursive'
FileManagement.DeleteFolder(client, contact.id, id).sendAndExpect(bot)
}.toResult("AbstractAbsoluteFileFolder.delete", checkResp = false).getOrThrow()
return when (result.int32RetCode) {
-36 -> throwPermissionDeniedException("delete")
0 -> true
else -> {
// files not exists or other errors.
false
}
}
}
@Suppress("DuplicatedCode")
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AbstractAbsoluteFileFolder
if (contact != other.contact) return false
if (parent != other.parent) return false
if (id != other.id) return false
if (name != other.name) return false
if (uploadTime != other.uploadTime) return false
if (uploaderId != other.uploaderId) return false
if (lastModifiedTime != other.lastModifiedTime) return false
if (busId != other.busId) return false
if (isFile != other.isFile) return false
if (isFolder != other.isFolder) return false
return true
}
override fun hashCode(): Int {
var result = contact.hashCode()
result = 31 * result + (parent?.hashCode() ?: 0)
result = 31 * result + id.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + uploadTime.hashCode()
result = 31 * result + uploaderId.hashCode()
result = 31 * result + lastModifiedTime.hashCode()
result = 31 * result + busId
result = 31 * result + isFile.hashCode()
result = 31 * result + isFolder.hashCode()
return result
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2019-2021 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
*/
package net.mamoe.mirai.internal.contact.file
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.protocol.data.proto.Oidb0x6d6
import net.mamoe.mirai.internal.network.protocol.packet.chat.CommonOidbResponse
import net.mamoe.mirai.internal.utils.FileSystem
/**
* Abstract protocol bridge for file management.
*/
internal interface FileProtocol {
val fs: FileSystem get() = FileSystem
fun renameFile(
client: QQAndroidClient,
groupCode: Long,
busId: Int,
fileId: String,
parentFolderId: String,
newName: String,
): CommonOidbResponse<Oidb0x6d6.RenameFileRspBody>
}

View File

@ -0,0 +1,48 @@
/*
* Copyright 2019-2021 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
*/
package net.mamoe.mirai.internal.contact.file
import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.contact.file.AbsoluteFolder
import net.mamoe.mirai.contact.file.RemoteFiles
import net.mamoe.mirai.internal.contact.ContactAware
import net.mamoe.mirai.internal.utils.FileSystem
internal class RemoteFilesImpl(
override val contact: FileSupported,
override val root: AbsoluteFolder = AbsoluteFolderImpl(
contact,
null,
AbsoluteFolder.ROOT_FOLDER_ID,
"/",
0,
0,
0,
0
),
) : RemoteFiles, ContactAware {
companion object {
suspend fun AbsoluteFolder.findFileByPath(path: String): Pair<AbsoluteFolder, String> {
if (path.isBlank()) throw IllegalArgumentException("absolutePath cannot be blank.")
val normalized = FileSystem.normalize(path)
// if (!normalized.contains('/')) {
// throw IllegalArgumentException("Invalid absolutePath: '$path'. If you wanted to upload file to root directory, please add a leading '/'.")
// }
val folder = when (normalized.count { it == '/' }) {
0, 1 -> this
else -> this.createFolder(normalized.substringBeforeLast("/"))
}
val filename = normalized.substringAfterLast('/')
return folder to filename
}
}
}

View File

@ -10,9 +10,26 @@
package net.mamoe.mirai.internal.message
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.contact.file.AbsoluteFile
import net.mamoe.mirai.contact.file.AbsoluteFolder
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.asQQAndroidBot
import net.mamoe.mirai.internal.contact.file.AbsoluteFolderImpl
import net.mamoe.mirai.internal.contact.file.createChildFile
import net.mamoe.mirai.internal.contact.file.impl
import net.mamoe.mirai.internal.contact.file.resolved
import net.mamoe.mirai.internal.network.protocol.data.proto.Oidb0x6d8.GetFileListRspBody
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.message.data.FileMessage
import net.mamoe.mirai.utils.cast
import kotlin.contracts.contract
internal fun FileMessage.checkIsImpl(): FileMessageImpl {
@ -31,5 +48,41 @@ internal data class FileMessageImpl(
override val internalId: Int
get() = busId
override suspend fun toAbsoluteFile(contact: FileSupported): AbsoluteFile? {
val result = FileManagement.GetFileInfo(contact.bot.asQQAndroidBot().client, contact.id, id, busId)
.sendAndExpect(contact.bot.asQQAndroidBot())
.toResult("FileMessage.toAbsoluteFile").getOrThrow()
if (result.fileInfo == null) return null
// Get its parent AbsoluteFolder
// This is necessary for properties like creationTime.
// Maybe we can optimize it in the future (i.e. make it lazy?)
val root = contact.files.root.impl()
val folder = if (result.fileInfo.parentFolderId == AbsoluteFolder.ROOT_FOLDER_ID) {
root
} else {
val folders = ArrayList<GetFileListRspBody.Item>()
root.impl().getItemsFlow()
.filter { it.folderInfo != null }
.onEach { folders.add(it) }
.firstOrNull { it.folderInfo?.folderId == result.fileInfo.parentFolderId }
?.resolved(root) as AbsoluteFolderImpl?
?: kotlin.run {
for (folder in folders) {
AbsoluteFolderImpl.getItemsFlow(
(contact.bot as QQAndroidBot).client,
contact,
folder.folderInfo!!.folderId
).firstOrNull { it.folderInfo?.folderId == result.fileInfo.parentFolderId }
?.resolved(root)?.cast<AbsoluteFolderImpl?>()?.let { return@run it }
}
root
}
}
return folder.createChildFile(result.fileInfo)
}
override fun toString(): String = "[mirai:file:$name,$id,$size,$busId]"
}

View File

@ -73,6 +73,28 @@ internal inline fun <T> CommonOidbResponse<T>.toResult(actionName: String, check
}
}
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE")
@kotlin.internal.InlineOnly
internal inline fun <T> CommonOidbResponse<T>.toResult(
actionName: String,
checkResp: CheckableStruct.(Int) -> Boolean
): Result<T> {
return if (this is CommonOidbResponse.Failure) {
Result.failure(this.createException(actionName))
} else {
this as CommonOidbResponse.Success<T>
val result = this.resp
if (result is CheckableStruct) {
if (!checkResp(
result,
result.int32RetCode
)
) return Result.failure(IllegalStateException("Failed $actionName, result=${result.int32RetCode}, msg=${result.retMsg}"))
}
Result.success(this.resp)
}
}
/**
* @param respMapper may throw any exception, which will be wrapped to CommonOidbResponse.Failure
*/

View File

@ -7,6 +7,8 @@
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
@file:Suppress("DEPRECATION", "OverridingDeprecatedMember")
package net.mamoe.mirai.internal.utils
import kotlinx.coroutines.flow.*
@ -46,6 +48,10 @@ internal object FileSystem {
}
}
fun isLegal(path: String): Boolean {
return path.firstOrNull { it in """:*?"<>|""" } == null
}
fun normalize(path: String): String {
checkLegitimacy(path)
return path.replace('\\', '/')