diff --git a/drivers/all.go b/drivers/all.go index 314cf9cb..c468412a 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -25,6 +25,7 @@ import ( _ "github.com/alist-org/alist/v3/drivers/local" _ "github.com/alist-org/alist/v3/drivers/mediatrack" _ "github.com/alist-org/alist/v3/drivers/mega" + _ "github.com/alist-org/alist/v3/drivers/mopan" _ "github.com/alist-org/alist/v3/drivers/onedrive" _ "github.com/alist-org/alist/v3/drivers/onedrive_app" _ "github.com/alist-org/alist/v3/drivers/pikpak" diff --git a/drivers/mopan/driver.go b/drivers/mopan/driver.go new file mode 100644 index 00000000..dfe8399c --- /dev/null +++ b/drivers/mopan/driver.go @@ -0,0 +1,295 @@ +package mopan + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "os" + "time" + + "github.com/alist-org/alist/v3/drivers/base" + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/model" + "github.com/alist-org/alist/v3/internal/op" + "github.com/alist-org/alist/v3/pkg/utils" + "github.com/avast/retry-go" + "github.com/foxxorcat/mopan-sdk-go" +) + +type MoPan struct { + model.Storage + Addition + client *mopan.MoClient + + userID string +} + +func (d *MoPan) Config() driver.Config { + return config +} + +func (d *MoPan) GetAddition() driver.Additional { + return &d.Addition +} + +func (d *MoPan) Init(ctx context.Context) error { + login := func() error { + data, err := d.client.Login(d.Phone, d.Password) + if err != nil { + return err + } + d.client.SetAuthorization(data.Token) + + info, err := d.client.GetUserInfo() + if err != nil { + return err + } + d.userID = info.UserID + return nil + } + d.client = mopan.NewMoClient(). + SetRestyClient(base.RestyClient). + SetOnAuthorizationExpired(func(_ error) error { + err := login() + if err != nil { + d.Status = err.Error() + op.MustSaveDriverStorage(d) + } + return err + }).SetDeviceInfo(d.DeviceInfo) + d.DeviceInfo = d.client.GetDeviceInfo() + return login() +} + +func (d *MoPan) Drop(ctx context.Context) error { + d.client = nil + d.userID = "" + return nil +} + +func (d *MoPan) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { + var files []model.Obj + for page := 1; ; page++ { + data, err := d.client.QueryFiles(dir.GetID(), page, mopan.WarpParamOption( + func(j mopan.Json) { + j["orderBy"] = d.OrderBy + j["descending"] = d.OrderDirection == "desc" + }, + mopan.ParamOptionShareFile(d.CloudID), + )) + if err != nil { + return nil, err + } + + if len(data.FileListAO.FileList)+len(data.FileListAO.FolderList) == 0 { + break + } + + files = append(files, utils.MustSliceConvert(data.FileListAO.FolderList, folderToObj)...) + files = append(files, utils.MustSliceConvert(data.FileListAO.FileList, fileToObj)...) + } + return files, nil +} + +func (d *MoPan) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + data, err := d.client.GetFileDownloadUrl(file.GetID(), mopan.WarpParamOption(mopan.ParamOptionShareFile(d.CloudID))) + if err != nil { + return nil, err + } + + return &model.Link{ + URL: data.DownloadUrl, + }, nil +} + +func (d *MoPan) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) { + f, err := d.client.CreateFolder(dirName, parentDir.GetID(), mopan.WarpParamOption( + mopan.ParamOptionShareFile(d.CloudID), + )) + if err != nil { + return nil, err + } + return folderToObj(*f), nil +} + +func (d *MoPan) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { + return d.newTask(srcObj, dstDir, mopan.TASK_MOVE) +} + +func (d *MoPan) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) { + if srcObj.IsDir() { + _, err := d.client.RenameFolder(srcObj.GetID(), newName, mopan.WarpParamOption( + mopan.ParamOptionShareFile(d.CloudID), + )) + if err != nil { + return nil, err + } + } else { + _, err := d.client.RenameFile(srcObj.GetID(), newName, mopan.WarpParamOption( + mopan.ParamOptionShareFile(d.CloudID), + )) + if err != nil { + return nil, err + } + } + return CloneObj(srcObj, srcObj.GetID(), newName), nil +} + +func (d *MoPan) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { + return d.newTask(srcObj, dstDir, mopan.TASK_COPY) +} + +func (d *MoPan) newTask(srcObj, dstDir model.Obj, taskType mopan.TaskType) (model.Obj, error) { + param := mopan.TaskParam{ + UserOrCloudID: d.userID, + Source: 1, + TaskType: taskType, + TargetSource: 1, + TargetUserOrCloudID: d.userID, + TargetType: 1, + TargetFolderID: dstDir.GetID(), + TaskStatusDetailDTOList: []mopan.TaskFileParam{ + { + FileID: srcObj.GetID(), + IsFolder: srcObj.IsDir(), + FileName: srcObj.GetName(), + }, + }, + } + if d.CloudID != "" { + param.UserOrCloudID = d.CloudID + param.Source = 2 + param.TargetSource = 2 + param.TargetUserOrCloudID = d.CloudID + } + + task, err := d.client.AddBatchTask(param) + if err != nil { + return nil, err + } + + for count := 0; count < 5; count++ { + stat, err := d.client.CheckBatchTask(mopan.TaskCheckParam{ + TaskId: task.TaskIDList[0], + TaskType: task.TaskType, + TargetType: 1, + TargetFolderID: task.TargetFolderID, + TargetSource: param.TargetSource, + TargetUserOrCloudID: param.TargetUserOrCloudID, + }) + if err != nil { + return nil, err + } + + switch stat.TaskStatus { + case 2: + if err := d.client.CancelBatchTask(stat.TaskID, task.TaskType); err != nil { + return nil, err + } + return nil, errors.New("file name conflict") + case 4: + if task.TaskType == mopan.TASK_MOVE { + return CloneObj(srcObj, srcObj.GetID(), srcObj.GetName()), nil + } + return CloneObj(srcObj, stat.SuccessedFileIDList[0], srcObj.GetName()), nil + } + time.Sleep(time.Second) + } + return nil, nil +} + +func (d *MoPan) Remove(ctx context.Context, obj model.Obj) error { + _, err := d.client.DeleteToRecycle([]mopan.TaskFileParam{ + { + FileID: obj.GetID(), + IsFolder: obj.IsDir(), + FileName: obj.GetName(), + }, + }, mopan.WarpParamOption(mopan.ParamOptionShareFile(d.CloudID))) + return err +} + +func (d *MoPan) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { + file, err := utils.CreateTempFile(stream) + if err != nil { + return nil, err + } + defer func() { + _ = file.Close() + _ = os.Remove(file.Name()) + }() + + initUpdload, err := d.client.InitMultiUpload(ctx, mopan.UpdloadFileParam{ + ParentFolderId: dstDir.GetID(), + FileName: stream.GetName(), + FileSize: stream.GetSize(), + File: file, + }, mopan.WarpParamOption( + mopan.ParamOptionShareFile(d.CloudID), + )) + if err != nil { + return nil, err + } + + if !initUpdload.FileDataExists { + parts, err := d.client.GetAllMultiUploadUrls(initUpdload.UploadFileID, initUpdload.PartInfo) + if err != nil { + return nil, err + } + d.client.CloudDiskStartBusiness() + for i, part := range parts { + if utils.IsCanceled(ctx) { + return nil, ctx.Err() + } + + err := retry.Do(func() error { + if _, err := file.Seek(int64(part.PartNumber-1)*int64(initUpdload.PartSize), io.SeekStart); err != nil { + return retry.Unrecoverable(err) + } + + req, err := part.NewRequest(ctx, io.LimitReader(file, int64(initUpdload.PartSize))) + if err != nil { + return err + } + + resp, err := base.HttpClient.Do(req) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("upload err,code=%d", resp.StatusCode) + } + return nil + }, + retry.Context(ctx), + retry.Attempts(3), + retry.Delay(time.Second), + retry.MaxDelay(5*time.Second)) + if err != nil { + return nil, err + } + up(100 * (i + 1) / len(parts)) + } + } + uFile, err := d.client.CommitMultiUploadFile(initUpdload.UploadFileID, nil) + if err != nil { + return nil, err + } + return &model.Object{ + ID: uFile.UserFileID, + Name: uFile.FileName, + Size: int64(uFile.FileSize), + Modified: time.Time(uFile.CreateDate), + }, nil +} + +var _ driver.Driver = (*MoPan)(nil) +var _ driver.MkdirResult = (*MoPan)(nil) +var _ driver.MoveResult = (*MoPan)(nil) +var _ driver.RenameResult = (*MoPan)(nil) +var _ driver.Remove = (*MoPan)(nil) +var _ driver.CopyResult = (*MoPan)(nil) +var _ driver.PutResult = (*MoPan)(nil) diff --git a/drivers/mopan/meta.go b/drivers/mopan/meta.go new file mode 100644 index 00000000..2b64955b --- /dev/null +++ b/drivers/mopan/meta.go @@ -0,0 +1,37 @@ +package mopan + +import ( + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/op" +) + +type Addition struct { + Phone string `json:"phone" required:"true"` + Password string `json:"password" required:"true"` + + RootFolderID string `json:"root_folder_id" default:"-11" required:"true" help:"be careful when using the -11 value, some operations may cause system errors"` + + CloudID string `json:"cloud_id"` + + OrderBy string `json:"order_by" type:"select" options:"filename,filesize,lastOpTime" default:"filename"` + OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"` + + DeviceInfo string `json:"device_info"` +} + +func (a *Addition) GetRootId() string { + return a.RootFolderID +} + +var config = driver.Config{ + Name: "MoPan", + // DefaultRoot: "root, / or other", + CheckStatus: true, + Alert: "warning|This network disk may store your password in clear text. Please set your password carefully", +} + +func init() { + op.RegisterDriver(func() driver.Driver { + return &MoPan{} + }) +} diff --git a/drivers/mopan/types.go b/drivers/mopan/types.go new file mode 100644 index 00000000..54b02f9a --- /dev/null +++ b/drivers/mopan/types.go @@ -0,0 +1 @@ +package mopan diff --git a/drivers/mopan/util.go b/drivers/mopan/util.go new file mode 100644 index 00000000..f35a226a --- /dev/null +++ b/drivers/mopan/util.go @@ -0,0 +1,58 @@ +package mopan + +import ( + "time" + + "github.com/alist-org/alist/v3/internal/model" + "github.com/foxxorcat/mopan-sdk-go" +) + +func fileToObj(f mopan.File) model.Obj { + return &model.ObjThumb{ + Object: model.Object{ + ID: string(f.ID), + Name: f.Name, + Size: f.Size, + Modified: time.Time(f.LastOpTime), + }, + Thumbnail: model.Thumbnail{ + Thumbnail: f.Icon.SmallURL, + }, + } +} + +func folderToObj(f mopan.Folder) model.Obj { + return &model.Object{ + ID: string(f.ID), + Name: f.Name, + Modified: time.Time(f.LastOpTime), + IsFolder: true, + } +} + +func CloneObj(o model.Obj, newID, newName string) model.Obj { + if o.IsDir() { + return &model.Object{ + ID: newID, + Name: newName, + IsFolder: true, + Modified: o.ModTime(), + } + } + + thumb := "" + if o, ok := o.(model.Thumb); ok { + thumb = o.Thumb() + } + return &model.ObjThumb{ + Object: model.Object{ + ID: newID, + Name: newName, + Size: o.GetSize(), + Modified: o.ModTime(), + }, + Thumbnail: model.Thumbnail{ + Thumbnail: thumb, + }, + } +} diff --git a/go.mod b/go.mod index 3b4d47c4..779abf18 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/deckarep/golang-set/v2 v2.3.0 github.com/disintegration/imaging v1.6.2 github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 + github.com/foxxorcat/mopan-sdk-go v0.1.0 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 c6a74caa..5dd44fe6 100644 --- a/go.sum +++ b/go.sum @@ -86,6 +86,8 @@ github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1 github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 h1:I6KUy4CI6hHjqnyJLNCEi7YHVMkwwtfSr2k9splgdSM= github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564/go.mod h1:yekO+3ZShy19S+bsmnERmznGy9Rfg6dWWWpiGJjNAz8= +github.com/foxxorcat/mopan-sdk-go v0.1.0 h1:U/E/uNK4N7xNbcHXdw+DG56LWw2W6Xpjj+yoH8EmTj0= +github.com/foxxorcat/mopan-sdk-go v0.1.0/go.mod h1:LpBPmwezjQNyhaNo3HGzgFtQbhvxmF5ZybSVuKi7OVA= 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= @@ -208,6 +210,7 @@ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APP github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=