mirror of https://github.com/Xhofe/alist
				
				
				
			feat(misskey): add misskey driver (#7864)
							parent
							
								
									bdd9774aa7
								
							
						
					
					
						commit
						fd51f34efa
					
				| 
						 | 
				
			
			@ -37,6 +37,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/misskey"
 | 
			
		||||
	_ "github.com/alist-org/alist/v3/drivers/mopan"
 | 
			
		||||
	_ "github.com/alist-org/alist/v3/drivers/netease_music"
 | 
			
		||||
	_ "github.com/alist-org/alist/v3/drivers/onedrive"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,74 @@
 | 
			
		|||
package misskey
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/alist-org/alist/v3/internal/driver"
 | 
			
		||||
	"github.com/alist-org/alist/v3/internal/errs"
 | 
			
		||||
	"github.com/alist-org/alist/v3/internal/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Misskey struct {
 | 
			
		||||
	model.Storage
 | 
			
		||||
	Addition
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) Config() driver.Config {
 | 
			
		||||
	return config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) GetAddition() driver.Additional {
 | 
			
		||||
	return &d.Addition
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) Init(ctx context.Context) error {
 | 
			
		||||
	d.Endpoint = strings.TrimSuffix(d.Endpoint, "/")
 | 
			
		||||
	if d.Endpoint == "" || d.AccessToken == "" {
 | 
			
		||||
		return errs.EmptyToken
 | 
			
		||||
	} else {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) Drop(ctx context.Context) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
 | 
			
		||||
	return d.list(dir)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
 | 
			
		||||
	return d.link(file)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
 | 
			
		||||
	return d.makeDir(parentDir, dirName)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
 | 
			
		||||
	return d.move(srcObj, dstDir)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
 | 
			
		||||
	return d.rename(srcObj, newName)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
 | 
			
		||||
	return d.copy(srcObj, dstDir)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) Remove(ctx context.Context, obj model.Obj) error {
 | 
			
		||||
	return d.remove(obj)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
 | 
			
		||||
	return d.put(dstDir, stream, up)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
 | 
			
		||||
//	return nil, errs.NotSupport
 | 
			
		||||
//}
 | 
			
		||||
 | 
			
		||||
var _ driver.Driver = (*Misskey)(nil)
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
package misskey
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/alist-org/alist/v3/internal/driver"
 | 
			
		||||
	"github.com/alist-org/alist/v3/internal/op"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Addition struct {
 | 
			
		||||
	// Usually one of two
 | 
			
		||||
	driver.RootPath
 | 
			
		||||
	// define other
 | 
			
		||||
	// Field string `json:"field" type:"select" required:"true" options:"a,b,c" default:"a"`
 | 
			
		||||
	Endpoint    string `json:"endpoint" required:"true" default:"https://misskey.io"`
 | 
			
		||||
	AccessToken string `json:"access_token" required:"true"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var config = driver.Config{
 | 
			
		||||
	Name:              "Misskey",
 | 
			
		||||
	LocalSort:         false,
 | 
			
		||||
	OnlyLocal:         false,
 | 
			
		||||
	OnlyProxy:         false,
 | 
			
		||||
	NoCache:           false,
 | 
			
		||||
	NoUpload:          false,
 | 
			
		||||
	NeedMs:            false,
 | 
			
		||||
	DefaultRoot:       "/",
 | 
			
		||||
	CheckStatus:       false,
 | 
			
		||||
	Alert:             "",
 | 
			
		||||
	NoOverwriteUpload: false,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	op.RegisterDriver(func() driver.Driver {
 | 
			
		||||
		return &Misskey{}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
package misskey
 | 
			
		||||
 | 
			
		||||
type Resp struct {
 | 
			
		||||
	Code int
 | 
			
		||||
	Raw  []byte
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Properties struct {
 | 
			
		||||
	Width  int `json:"width"`
 | 
			
		||||
	Height int `json:"height"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MFile struct {
 | 
			
		||||
	ID           string     `json:"id"`
 | 
			
		||||
	CreatedAt    string     `json:"createdAt"`
 | 
			
		||||
	Name         string     `json:"name"`
 | 
			
		||||
	Type         string     `json:"type"`
 | 
			
		||||
	MD5          string     `json:"md5"`
 | 
			
		||||
	Size         int64      `json:"size"`
 | 
			
		||||
	IsSensitive  bool       `json:"isSensitive"`
 | 
			
		||||
	Blurhash     string     `json:"blurhash"`
 | 
			
		||||
	Properties   Properties `json:"properties"`
 | 
			
		||||
	URL          string     `json:"url"`
 | 
			
		||||
	ThumbnailURL string     `json:"thumbnailUrl"`
 | 
			
		||||
	Comment      *string    `json:"comment"`
 | 
			
		||||
	FolderID     *string    `json:"folderId"`
 | 
			
		||||
	Folder       MFolder    `json:"folder"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MFolder struct {
 | 
			
		||||
	ID        string  `json:"id"`
 | 
			
		||||
	CreatedAt string  `json:"createdAt"`
 | 
			
		||||
	Name      string  `json:"name"`
 | 
			
		||||
	ParentID  *string `json:"parentId"`
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,256 @@
 | 
			
		|||
package misskey
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-resty/resty/v2"
 | 
			
		||||
 | 
			
		||||
	"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/pkg/utils"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Base layer methods
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) request(path, method string, callback base.ReqCallback, resp interface{}) error {
 | 
			
		||||
	url := d.Endpoint + "/api/drive" + path
 | 
			
		||||
	req := base.RestyClient.R()
 | 
			
		||||
 | 
			
		||||
	req.SetAuthToken(d.AccessToken).SetHeader("Content-Type", "application/json")
 | 
			
		||||
 | 
			
		||||
	if callback != nil {
 | 
			
		||||
		callback(req)
 | 
			
		||||
	} else {
 | 
			
		||||
		req.SetBody("{}")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req.SetResult(resp)
 | 
			
		||||
 | 
			
		||||
	// 启用调试模式
 | 
			
		||||
	req.EnableTrace()
 | 
			
		||||
 | 
			
		||||
	response, err := req.Execute(method, url)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if !response.IsSuccess() {
 | 
			
		||||
		return errors.New(response.String())
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) getThumb(ctx context.Context, obj model.Obj) (io.Reader, error) {
 | 
			
		||||
	// TODO return the thumb of obj, optional
 | 
			
		||||
	return nil, errs.NotImplement
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func setBody(body interface{}) base.ReqCallback {
 | 
			
		||||
	return func(req *resty.Request) {
 | 
			
		||||
		req.SetBody(body)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func handleFolderId(dir model.Obj) interface{} {
 | 
			
		||||
	if dir.GetID() == "" {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	return dir.GetID()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// API layer methods
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) getFiles(dir model.Obj) ([]model.Obj, error) {
 | 
			
		||||
	var files []MFile
 | 
			
		||||
	var body map[string]string
 | 
			
		||||
	if dir.GetPath() != "/" {
 | 
			
		||||
		body = map[string]string{"folderId": dir.GetID()}
 | 
			
		||||
	} else {
 | 
			
		||||
		body = map[string]string{}
 | 
			
		||||
	}
 | 
			
		||||
	err := d.request("/files", "POST", setBody(body), &files)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return []model.Obj{}, err
 | 
			
		||||
	}
 | 
			
		||||
	return utils.SliceConvert(files, func(src MFile) (model.Obj, error) {
 | 
			
		||||
		return mFile2Object(src), nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) getFolders(dir model.Obj) ([]model.Obj, error) {
 | 
			
		||||
	var folders []MFolder
 | 
			
		||||
	var body map[string]string
 | 
			
		||||
	if dir.GetPath() != "/" {
 | 
			
		||||
		body = map[string]string{"folderId": dir.GetID()}
 | 
			
		||||
	} else {
 | 
			
		||||
		body = map[string]string{}
 | 
			
		||||
	}
 | 
			
		||||
	err := d.request("/folders", "POST", setBody(body), &folders)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return []model.Obj{}, err
 | 
			
		||||
	}
 | 
			
		||||
	return utils.SliceConvert(folders, func(src MFolder) (model.Obj, error) {
 | 
			
		||||
		return mFolder2Object(src), nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) list(dir model.Obj) ([]model.Obj, error) {
 | 
			
		||||
	files, _ := d.getFiles(dir)
 | 
			
		||||
	folders, _ := d.getFolders(dir)
 | 
			
		||||
	return append(files, folders...), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) link(file model.Obj) (*model.Link, error) {
 | 
			
		||||
	var mFile MFile
 | 
			
		||||
	err := d.request("/files/show", "POST", setBody(map[string]string{"fileId": file.GetID()}), &mFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return &model.Link{
 | 
			
		||||
		URL: mFile.URL,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) makeDir(parentDir model.Obj, dirName string) (model.Obj, error) {
 | 
			
		||||
	var folder MFolder
 | 
			
		||||
	err := d.request("/folders/create", "POST", setBody(map[string]interface{}{"parentId": handleFolderId(parentDir), "name": dirName}), &folder)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return mFolder2Object(folder), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) move(srcObj, dstDir model.Obj) (model.Obj, error) {
 | 
			
		||||
	if srcObj.IsDir() {
 | 
			
		||||
		var folder MFolder
 | 
			
		||||
		err := d.request("/folders/update", "POST", setBody(map[string]interface{}{"folderId": srcObj.GetID(), "parentId": handleFolderId(dstDir)}), &folder)
 | 
			
		||||
		return mFolder2Object(folder), err
 | 
			
		||||
	} else {
 | 
			
		||||
		var file MFile
 | 
			
		||||
		err := d.request("/files/update", "POST", setBody(map[string]interface{}{"fileId": srcObj.GetID(), "folderId": handleFolderId(dstDir)}), &file)
 | 
			
		||||
		return mFile2Object(file), err
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) rename(srcObj model.Obj, newName string) (model.Obj, error) {
 | 
			
		||||
	if srcObj.IsDir() {
 | 
			
		||||
		var folder MFolder
 | 
			
		||||
		err := d.request("/folders/update", "POST", setBody(map[string]string{"folderId": srcObj.GetID(), "name": newName}), &folder)
 | 
			
		||||
		return mFolder2Object(folder), err
 | 
			
		||||
	} else {
 | 
			
		||||
		var file MFile
 | 
			
		||||
		err := d.request("/files/update", "POST", setBody(map[string]string{"fileId": srcObj.GetID(), "name": newName}), &file)
 | 
			
		||||
		return mFile2Object(file), err
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) copy(srcObj, dstDir model.Obj) (model.Obj, error) {
 | 
			
		||||
	if srcObj.IsDir() {
 | 
			
		||||
		folder, err := d.makeDir(dstDir, srcObj.GetName())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		list, err := d.list(srcObj)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		for _, obj := range list {
 | 
			
		||||
			_, err := d.copy(obj, folder)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return folder, nil
 | 
			
		||||
	} else {
 | 
			
		||||
		var file MFile
 | 
			
		||||
		url, err := d.link(srcObj)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		err = d.request("/files/upload-from-url", "POST", setBody(map[string]interface{}{"url": url.URL, "folderId": handleFolderId(dstDir)}), &file)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		return mFile2Object(file), nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) remove(obj model.Obj) error {
 | 
			
		||||
	if obj.IsDir() {
 | 
			
		||||
		err := d.request("/folders/delete", "POST", setBody(map[string]string{"folderId": obj.GetID()}), nil)
 | 
			
		||||
		return err
 | 
			
		||||
	} else {
 | 
			
		||||
		err := d.request("/files/delete", "POST", setBody(map[string]string{"fileId": obj.GetID()}), nil)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Misskey) put(dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
 | 
			
		||||
	var file MFile
 | 
			
		||||
 | 
			
		||||
	fileContent, err := io.ReadAll(stream)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req := base.RestyClient.R().
 | 
			
		||||
		SetFileReader("file", stream.GetName(), io.NopCloser(bytes.NewReader(fileContent))).
 | 
			
		||||
		SetFormData(map[string]string{
 | 
			
		||||
			"folderId":    handleFolderId(dstDir).(string),
 | 
			
		||||
			"name":        stream.GetName(),
 | 
			
		||||
			"comment":     "",
 | 
			
		||||
			"isSensitive": "false",
 | 
			
		||||
			"force":       "false",
 | 
			
		||||
		}).
 | 
			
		||||
		SetResult(&file).SetAuthToken(d.AccessToken)
 | 
			
		||||
 | 
			
		||||
	resp, err := req.Post(d.Endpoint + "/api/drive/files/create")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if !resp.IsSuccess() {
 | 
			
		||||
		return nil, errors.New(resp.String())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return mFile2Object(file), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func mFile2Object(file MFile) *model.ObjThumbURL {
 | 
			
		||||
	ctime, err := time.Parse(time.RFC3339, file.CreatedAt)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctime = time.Time{}
 | 
			
		||||
	}
 | 
			
		||||
	return &model.ObjThumbURL{
 | 
			
		||||
		Object: model.Object{
 | 
			
		||||
			ID:       file.ID,
 | 
			
		||||
			Name:     file.Name,
 | 
			
		||||
			Ctime:    ctime,
 | 
			
		||||
			IsFolder: false,
 | 
			
		||||
			Size:     file.Size,
 | 
			
		||||
		},
 | 
			
		||||
		Thumbnail: model.Thumbnail{
 | 
			
		||||
			Thumbnail: file.ThumbnailURL,
 | 
			
		||||
		},
 | 
			
		||||
		Url: model.Url{
 | 
			
		||||
			Url: file.URL,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func mFolder2Object(folder MFolder) *model.Object {
 | 
			
		||||
	ctime, err := time.Parse(time.RFC3339, folder.CreatedAt)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctime = time.Time{}
 | 
			
		||||
	}
 | 
			
		||||
	return &model.Object{
 | 
			
		||||
		ID:       folder.ID,
 | 
			
		||||
		Name:     folder.Name,
 | 
			
		||||
		Ctime:    ctime,
 | 
			
		||||
		IsFolder: true,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue