Repository: AlistGo/alist (language: go)

Issue:
请求添加微云驱动
### Please make sure of the following things

- [X] I have read the [documentation](https://alist.nn.ci).
- [X] I'm sure there are no duplicate issues or discussions.
- [X] I'm sure this feature is not implemented.
- [X] I'm sure it's a reasonable and popular requirement.

### Description of the feature / 需求描述

腾讯微云网盘不限速，下载和在线看视频速度稳定，请求加入alist。

微云网址：https://www.weiyun.com

### Suggested solution / 实现思路

不懂程序，不知道思路。请求作者大大考虑一下这个驱动。

### Additional context / 附件

_No response_


Discussion / hints:
### See
1. 50% #864

<!-- Created by actions-cool/issues-similarity-analysis. Do not remove. -->
微云大公司产品，稳定靠谱。请求作者大大增加此驱动。感谢
22年年初的时候就有大佬研究过哩~不的行
微云[api文档](https://open.weiyun.com/)不完整，只能实现上传、下载、重命名，连文件夹都创不了。付：（从wps抓的）
client_id：iciY14xT8lJF0SrlfvIhuTD36VeOkm9R
client_secret：26gJq9sDVHHxVUrAJy0COQ196v0EVXaW
重命名api：POST:https://user.weiyun.com/twoa/v1/files/{rs_id}/rename?new_filename=
查看文件详细信息：GET:https://user.weiyun.com/twoa/v1/files/{rs_id}

腾讯这个尿性，没有二维码，还让你扫码登录，真是大无语，很多东西庞大无条理；之前我就对接过，当时也是没办法解决登录问题，我是复制的cookie，但是基本一天就失效了：
![image](https://github.com/alist-org/alist/assets/31364579/af4ac423-3f32-4184-995a-1fac8b682ca0)

顺着楼上大佬提供的信息摸到了"`管事儿`"的人问了下具体情况，
开放`平台API`已不使用/不维护，大概率是鸽啦~（之前提到的大佬就是我说的楼上大佬 :joy_cat:  ）
![image](https://github.com/alist-org/alist/assets/56105412/6e98345b-aef8-4e2e-b8c6-5dbaf3ff73d8)

天翼云盘也是没有open api的吧，都是抓的客户端或者网页端的接口，然后解密他们的签名算法，自己做对接的。
微云真心好用，不和谐文件，在线观看和下载视频很稳定。关键是不和谐文件，这个就完爆国内的其他网盘了
> 微云[api文档](https://open.weiyun.com/)不完整，只能实现上传、下载、重命名，连文件夹都创不了。付：（从wps抓的） client_id：iciY14xT8lJF0SrlfvIhuTD36VeOkm9R client_secret：26gJq9sDVHHxVUrAJy0COQ196v0EVXaW 重命名api：POST:[https://user.weiyun.com/twoa/v1/files/{rs_id}/rename?new_filename=](https://user.weiyun.com/twoa/v1/files/%7Brs_id%7D/rename?new_filename=) 查看文件详细信息：GET:[https://user.weiyun.com/twoa/v1/files/{rs_id}](https://user.weiyun.com/twoa/v1/files/%7Brs_id%7D)


微云驱动，只需要能下载和在线观看视频就可以了。上传和文件操作不是必要功能，可以不做的。真心期望作者大大能添加微云这个驱动！！跪求。
主要问题是微云网页端如果用QQ登录的话会存在cookie过期的情况，并且会很快。目前已知就算保活也活不过48h。微信登录那就没这个问题，用的是token。
本来想抓客户端的api的，但发现用的不是http而是tcp，不好搞。。。
> 主要问题是微云网页端如果用QQ登录的话会存在cookie过期的情况，并且会很快。目前已知就算保活也活不过48h。微信登录那就没这个问题，用的是token。 本来想抓客户端的api的，但发现用的不是http而是tcp，不好搞。。。

cookie的保活我有点思路，可以加好友详细聊聊吗？我去你博客评论区发了我的联系方式给你，望回复，感谢。

Fix patch:
diff --git a/drivers/all.go b/drivers/all.go
index 1f7c808b9a4..6dfd8b182c4 100644
--- a/drivers/all.go
+++ b/drivers/all.go
@@ -44,6 +44,7 @@ import (
 	_ "github.com/alist-org/alist/v3/drivers/uss"
 	_ "github.com/alist-org/alist/v3/drivers/virtual"
 	_ "github.com/alist-org/alist/v3/drivers/webdav"
+	_ "github.com/alist-org/alist/v3/drivers/weiyun"
 	_ "github.com/alist-org/alist/v3/drivers/wopan"
 	_ "github.com/alist-org/alist/v3/drivers/yandex_disk"
 )
diff --git a/drivers/weiyun/driver.go b/drivers/weiyun/driver.go
new file mode 100644
index 00000000000..84088566e2c
--- /dev/null
+++ b/drivers/weiyun/driver.go
@@ -0,0 +1,381 @@
+package weiyun
+
+import (
+	"context"
+	"io"
+	"math"
+	"net/http"
+	"os"
+	"sync"
+	"time"
+
+	"github.com/alist-org/alist/v3/drivers/base"
+	"github.com/alist-org/alist/v3/internal/driver"
+	"github.com/alist-org/alist/v3/internal/errs"
+	"github.com/alist-org/alist/v3/internal/model"
+	"github.com/alist-org/alist/v3/internal/op"
+	"github.com/alist-org/alist/v3/pkg/cron"
+	"github.com/alist-org/alist/v3/pkg/utils"
+	weiyunsdkgo "github.com/foxxorcat/weiyun-sdk-go"
+)
+
+type WeiYun struct {
+	model.Storage
+	Addition
+
+	client     *weiyunsdkgo.WeiYunClient
+	cron       *cron.Cron
+	rootFolder *Folder
+}
+
+func (d *WeiYun) Config() driver.Config {
+	return config
+}
+
+func (d *WeiYun) GetAddition() driver.Additional {
+	return &d.Addition
+}
+
+func (d *WeiYun) Init(ctx context.Context) error {
+	d.client = weiyunsdkgo.NewWeiYunClientWithRestyClient(base.RestyClient)
+	err := d.client.SetCookiesStr(d.Cookies).RefreshCtoken()
+	if err != nil {
+		return err
+	}
+
+	// Cookie过期回调
+	d.client.SetOnCookieExpired(func(err error) {
+		d.Status = err.Error()
+		op.MustSaveDriverStorage(d)
+	})
+
+	// cookie更新回调
+	d.client.SetOnCookieUpload(func(c []*http.Cookie) {
+		d.Cookies = weiyunsdkgo.CookieToString(weiyunsdkgo.ClearCookie(c))
+		op.MustSaveDriverStorage(d)
+	})
+
+	// qqCookie保活
+	if d.client.LoginType() == 1 {
+		d.cron = cron.NewCron(time.Minute * 5)
+		d.cron.Do(func() {
+			d.client.KeepAlive()
+		})
+	}
+
+	// 获取默认根目录dirKey
+	if d.RootFolderID == "" {
+		userInfo, err := d.client.DiskUserInfoGet()
+		if err != nil {
+			return err
+		}
+		d.RootFolderID = userInfo.MainDirKey
+	}
+
+	// 处理目录ID，找到PdirKey
+	folders, err := d.client.LibDirPathGet(d.RootFolderID)
+	if err != nil {
+		return err
+	}
+	folder := folders[len(folders)-1]
+	d.rootFolder = &Folder{
+		PFolder: &Folder{
+			Folder: weiyunsdkgo.Folder{
+				DirKey: folder.PdirKey,
+			},
+		},
+		Folder: folder.Folder,
+	}
+	return nil
+}
+
+func (d *WeiYun) Drop(ctx context.Context) error {
+	d.client = nil
+	if d.cron != nil {
+		d.cron.Stop()
+		d.cron = nil
+	}
+	return nil
+}
+
+func (d *WeiYun) GetRoot(ctx context.Context) (model.Obj, error) {
+	return d.rootFolder, nil
+}
+
+func (d *WeiYun) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
+	if folder, ok := dir.(*Folder); ok {
+		var files []model.Obj
+		for {
+			data, err := d.client.DiskDirFileList(folder.GetID(), weiyunsdkgo.WarpParamOption(
+				weiyunsdkgo.QueryFileOptionOffest(int64(len(files))),
+				weiyunsdkgo.QueryFileOptionGetType(weiyunsdkgo.FileAndDir),
+				weiyunsdkgo.QueryFileOptionSort(func() weiyunsdkgo.OrderBy {
+					switch d.OrderBy {
+					case "name":
+						return weiyunsdkgo.FileName
+					case "size":
+						return weiyunsdkgo.FileSize
+					case "updated_at":
+						return weiyunsdkgo.FileMtime
+					default:
+						return weiyunsdkgo.FileName
+					}
+				}(), d.OrderDirection == "desc"),
+			))
+			if err != nil {
+				return nil, err
+			}
+
+			if files == nil {
+				files = make([]model.Obj, 0, data.TotalDirCount+data.TotalFileCount)
+			}
+
+			for _, dir := range data.DirList {
+				files = append(files, &Folder{
+					PFolder: folder,
+					Folder:  dir,
+				})
+			}
+
+			for _, file := range data.FileList {
+				files = append(files, &File{
+					PFolder: folder,
+					File:    file,
+				})
+			}
+
+			if data.FinishFlag || len(data.DirList)+len(data.FileList) == 0 {
+				return files, nil
+			}
+		}
+	}
+	return nil, errs.NotSupport
+}
+
+func (d *WeiYun) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
+	if file, ok := file.(*File); ok {
+		data, err := d.client.DiskFileDownload(weiyunsdkgo.FileParam{PdirKey: file.GetPKey(), FileID: file.GetID()})
+		if err != nil {
+			return nil, err
+		}
+		return &model.Link{
+			URL: data.DownloadUrl,
+			Header: http.Header{
+				"Cookie": []string{data.CookieName + "=" + data.CookieValue},
+			},
+		}, nil
+	}
+	return nil, errs.NotSupport
+}
+
+func (d *WeiYun) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
+	if folder, ok := parentDir.(*Folder); ok {
+		newFolder, err := d.client.DiskDirCreate(weiyunsdkgo.FolderParam{
+			PPdirKey: folder.GetPKey(),
+			PdirKey:  folder.DirKey,
+			DirName:  dirName,
+		})
+		if err != nil {
+			return nil, err
+		}
+		return &Folder{
+			PFolder: folder,
+			Folder:  *newFolder,
+		}, nil
+	}
+	return nil, errs.NotSupport
+}
+
+func (d *WeiYun) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
+	if dstDir, ok := dstDir.(*Folder); ok {
+		dstParam := weiyunsdkgo.FolderParam{
+			PdirKey: dstDir.GetPKey(),
+			DirKey:  dstDir.GetID(),
+			DirName: dstDir.GetName(),
+		}
+		switch srcObj := srcObj.(type) {
+		case *File:
+			err := d.client.DiskFileMove(weiyunsdkgo.FileParam{
+				PPdirKey: srcObj.PFolder.GetPKey(),
+				PdirKey:  srcObj.GetPKey(),
+				FileID:   srcObj.GetID(),
+				FileName: srcObj.GetName(),
+			}, dstParam)
+			if err != nil {
+				return nil, err
+			}
+
+			return &File{
+				PFolder: dstDir,
+				File:    srcObj.File,
+			}, nil
+		case *Folder:
+			err := d.client.DiskDirMove(weiyunsdkgo.FolderParam{
+				PPdirKey: srcObj.PFolder.GetPKey(),
+				PdirKey:  srcObj.GetPKey(),
+				DirKey:   srcObj.GetID(),
+				DirName:  srcObj.GetName(),
+			}, dstParam)
+			if err != nil {
+				return nil, err
+			}
+
+			return &Folder{
+				PFolder: dstDir,
+				Folder:  srcObj.Folder,
+			}, nil
+		}
+	}
+	return nil, errs.NotSupport
+}
+
+func (d *WeiYun) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
+	switch srcObj := srcObj.(type) {
+	case *File:
+		err := d.client.DiskFileRename(weiyunsdkgo.FileParam{
+			PPdirKey: srcObj.PFolder.GetPKey(),
+			PdirKey:  srcObj.GetPKey(),
+			FileID:   srcObj.GetID(),
+			FileName: srcObj.GetName(),
+		}, newName)
+		if err != nil {
+			return nil, err
+		}
+		newFile := srcObj.File
+		newFile.FileName = newName
+		newFile.FileCtime = weiyunsdkgo.TimeStamp(time.Now())
+		return &File{
+			PFolder: srcObj.PFolder,
+			File:    newFile,
+		}, nil
+	case *Folder:
+		err := d.client.DiskDirAttrModify(weiyunsdkgo.FolderParam{
+			PPdirKey: srcObj.PFolder.GetPKey(),
+			PdirKey:  srcObj.GetPKey(),
+			DirKey:   srcObj.GetID(),
+			DirName:  srcObj.GetName(),
+		}, newName)
+		if err != nil {
+			return nil, err
+		}
+
+		newFolder := srcObj.Folder
+		newFolder.DirName = newName
+		newFolder.DirCtime = weiyunsdkgo.TimeStamp(time.Now())
+		return &Folder{
+			PFolder: srcObj.PFolder,
+			Folder:  newFolder,
+		}, nil
+	}
+	return nil, errs.NotSupport
+}
+
+func (d *WeiYun) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
+	// TODO copy obj, optional
+	return errs.NotImplement
+}
+
+func (d *WeiYun) Remove(ctx context.Context, obj model.Obj) error {
+	switch obj := obj.(type) {
+	case *File:
+		return d.client.DiskFileDelete(weiyunsdkgo.FileParam{
+			PPdirKey: obj.PFolder.GetPKey(),
+			PdirKey:  obj.GetPKey(),
+			FileID:   obj.GetID(),
+			FileName: obj.GetName(),
+		})
+	case *Folder:
+		return d.client.DiskDirDelete(weiyunsdkgo.FolderParam{
+			PPdirKey: obj.PFolder.GetPKey(),
+			PdirKey:  obj.GetPKey(),
+			DirKey:   obj.GetID(),
+			DirName:  obj.GetName(),
+		})
+	}
+	// TODO remove obj, optional
+	return errs.NotSupport
+}
+
+func (d *WeiYun) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
+	if folder, ok := dstDir.(*Folder); ok {
+		file, err := utils.CreateTempFile(stream)
+		if err != nil {
+			return nil, err
+		}
+		defer func() {
+			_ = file.Close()
+			_ = os.Remove(file.Name())
+		}()
+
+		// step 1.
+		preData, err := d.client.PreUpload(ctx, weiyunsdkgo.UpdloadFileParam{
+			PdirKey: folder.GetPKey(),
+			DirKey:  folder.DirKey,
+
+			FileName: stream.GetName(),
+			FileSize: stream.GetSize(),
+			File:     file,
+
+			ChannelCount:    4,
+			FileExistOption: 1,
+		})
+		if err != nil {
+			return nil, err
+		}
+
+		// fast upload
+		if !preData.FileExist {
+			// step 2.
+			upCtx, cancel := context.WithCancelCause(ctx)
+			var wg sync.WaitGroup
+			for _, channel := range preData.ChannelList {
+				wg.Add(1)
+				go func(channel weiyunsdkgo.UploadChannelData) {
+					defer wg.Done()
+					if utils.IsCanceled(upCtx) {
+						return
+					}
+					for {
+						channel.Len = int(math.Min(float64(stream.GetSize()-channel.Offset), float64(channel.Len)))
+						upData, err := d.client.UploadFile(upCtx, channel, preData.UploadAuthData,
+							io.NewSectionReader(file, channel.Offset, int64(channel.Len)))
+						if err != nil {
+							cancel(err)
+							return
+						}
+						// 上传完成
+						if upData.UploadState != 1 {
+							return
+						}
+						channel = upData.Channel
+					}
+				}(channel)
+			}
+			wg.Wait()
+			if utils.IsCanceled(upCtx) {
+				return nil, context.Cause(upCtx)
+			}
+		}
+
+		return &File{
+			PFolder: folder,
+			File:    preData.File,
+		}, nil
+	}
+	return nil, errs.NotSupport
+}
+
+// func (d *WeiYun) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
+// 	return nil, errs.NotSupport
+// }
+
+var _ driver.Driver = (*WeiYun)(nil)
+var _ driver.GetRooter = (*WeiYun)(nil)
+var _ driver.MkdirResult = (*WeiYun)(nil)
+
+// var _ driver.CopyResult = (*WeiYun)(nil)
+var _ driver.MoveResult = (*WeiYun)(nil)
+var _ driver.Remove = (*WeiYun)(nil)
+
+var _ driver.PutResult = (*WeiYun)(nil)
+var _ driver.RenameResult = (*WeiYun)(nil)
diff --git a/drivers/weiyun/meta.go b/drivers/weiyun/meta.go
new file mode 100644
index 00000000000..c9bbbf7b84f
--- /dev/null
+++ b/drivers/weiyun/meta.go
@@ -0,0 +1,28 @@
+package weiyun
+
+import (
+	"github.com/alist-org/alist/v3/internal/driver"
+	"github.com/alist-org/alist/v3/internal/op"
+)
+
+type Addition struct {
+	RootFolderID   string `json:"root_folder_id"`
+	Cookies        string `json:"cookies" required:"true"`
+	OrderBy        string `json:"order_by" type:"select" options:"name,size,updated_at" default:"name"`
+	OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
+}
+
+var config = driver.Config{
+	Name:              "WeiYun",
+	LocalSort:         false,
+	OnlyProxy:         true,
+	CheckStatus:       true,
+	Alert:             "",
+	NoOverwriteUpload: false,
+}
+
+func init() {
+	op.RegisterDriver(func() driver.Driver {
+		return &WeiYun{}
+	})
+}
diff --git a/drivers/weiyun/types.go b/drivers/weiyun/types.go
new file mode 100644
index 00000000000..355583275ca
--- /dev/null
+++ b/drivers/weiyun/types.go
@@ -0,0 +1,39 @@
+package weiyun
+
+import (
+	"time"
+
+	weiyunsdkgo "github.com/foxxorcat/weiyun-sdk-go"
+)
+
+type File struct {
+	PFolder *Folder
+	weiyunsdkgo.File
+}
+
+func (f *File) GetID() string      { return f.FileID }
+func (f *File) GetSize() int64     { return f.FileSize }
+func (f *File) GetName() string    { return f.FileName }
+func (f *File) ModTime() time.Time { return time.Time(f.FileMtime) }
+func (f *File) IsDir() bool        { return false }
+func (f *File) GetPath() string    { return "" }
+
+func (f *File) GetPKey() string {
+	return f.PFolder.DirKey
+}
+
+type Folder struct {
+	PFolder *Folder
+	weiyunsdkgo.Folder
+}
+
+func (f *Folder) GetID() string      { return f.DirKey }
+func (f *Folder) GetSize() int64     { return 0 }
+func (f *Folder) GetName() string    { return f.DirName }
+func (f *Folder) ModTime() time.Time { return time.Time(f.DirMtime) }
+func (f *Folder) IsDir() bool        { return true }
+func (f *Folder) GetPath() string    { return "" }
+
+func (f *Folder) GetPKey() string {
+	return f.PFolder.DirKey
+}
diff --git a/go.mod b/go.mod
index 14a874033f3..318b1275ed7 100644
--- a/go.mod
+++ b/go.mod
@@ -17,6 +17,7 @@ require (
 	github.com/disintegration/imaging v1.6.2
 	github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564
 	github.com/foxxorcat/mopan-sdk-go v0.1.1
+	github.com/foxxorcat/weiyun-sdk-go v0.1.1
 	github.com/gin-contrib/cors v1.4.0
 	github.com/gin-gonic/gin v1.9.1
 	github.com/go-resty/resty/v2 v2.7.0
diff --git a/go.sum b/go.sum
index a8f987bd48c..28bf38e19de 100644
--- a/go.sum
+++ b/go.sum
@@ -195,6 +195,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/foxxorcat/mopan-sdk-go v0.1.1 h1:JYMeCu4PFpqgHapvOz4jPMT7CxR6Yebu3aWkgGMDeIU=
 github.com/foxxorcat/mopan-sdk-go v0.1.1/go.mod h1:LpBPmwezjQNyhaNo3HGzgFtQbhvxmF5ZybSVuKi7OVA=
+github.com/foxxorcat/weiyun-sdk-go v0.1.1 h1:m4qcJk0adr+bpM4es2zCqP3jhMEwEPyTMGICsamygEQ=
+github.com/foxxorcat/weiyun-sdk-go v0.1.1/go.mod h1:AKsLFuWhWlClpGrg1zxTdMejugZEZtmhIuElAk3W83s=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
 github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=


Test patch: