mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2025-09-19 12:16:24 +08:00
feat(drivers): add Degoo driver (#1097)
* Create driver.go Signed-off-by: CaspianGUAN <app@caspian.im> * Create util.go Signed-off-by: CaspianGUAN <app@caspian.im> * Create types.go Signed-off-by: CaspianGUAN <app@caspian.im> * Create meta.go Signed-off-by: CaspianGUAN <app@caspian.im> * Update drivers/degoo/driver.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: CaspianGUAN <app@caspian.im> * Update drivers/degoo/driver.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: CaspianGUAN <app@caspian.im> * Update driver.go Signed-off-by: CaspianGUAN <app@caspian.im> * Update meta.go Signed-off-by: CaspianGUAN <app@caspian.im> * Update types.go Signed-off-by: CaspianGUAN <app@caspian.im> * Update util.go Signed-off-by: CaspianGUAN <app@caspian.im> * Update driver.go Signed-off-by: CaspianGUAN <app@caspian.im> * Update util.go Signed-off-by: CaspianGUAN <app@caspian.im> * Update drivers/degoo/util.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: CaspianGUAN <app@caspian.im> * Update util.go Signed-off-by: CaspianGUAN <app@caspian.im> * refactor(degoo): add Degoo driver integration and update API handling * fix(degoo): apply suggestions --------- Signed-off-by: CaspianGUAN <app@caspian.im> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: MadDogOwner <xiaoran@xrgzs.top>
This commit is contained in:
@ -23,6 +23,7 @@ import (
|
|||||||
_ "github.com/OpenListTeam/OpenList/v4/drivers/cloudreve"
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/cloudreve"
|
||||||
_ "github.com/OpenListTeam/OpenList/v4/drivers/cloudreve_v4"
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/cloudreve_v4"
|
||||||
_ "github.com/OpenListTeam/OpenList/v4/drivers/crypt"
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/crypt"
|
||||||
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/degoo"
|
||||||
_ "github.com/OpenListTeam/OpenList/v4/drivers/doubao"
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/doubao"
|
||||||
_ "github.com/OpenListTeam/OpenList/v4/drivers/doubao_share"
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/doubao_share"
|
||||||
_ "github.com/OpenListTeam/OpenList/v4/drivers/dropbox"
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/dropbox"
|
||||||
|
205
drivers/degoo/driver.go
Normal file
205
drivers/degoo/driver.go
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
package degoo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/errs"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Degoo struct {
|
||||||
|
model.Storage
|
||||||
|
Addition
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Degoo) Config() driver.Config {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Degoo) GetAddition() driver.Additional {
|
||||||
|
return &d.Addition
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Degoo) Init(ctx context.Context) error {
|
||||||
|
|
||||||
|
d.client = base.HttpClient
|
||||||
|
|
||||||
|
if d.Token == "" {
|
||||||
|
err := d.login(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.getDevices(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Degoo) Drop(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Degoo) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
|
items, err := d.getAllFileChildren5(ctx, dir.GetID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return utils.MustSliceConvert(items, func(s DegooFileItem) model.Obj {
|
||||||
|
isFolder := s.Category == 2 || s.Category == 1 || s.Category == 10
|
||||||
|
|
||||||
|
createTime, modTime, _ := humanReadableTimes(s.CreationTime, s.LastModificationTime, s.LastUploadTime)
|
||||||
|
|
||||||
|
size, err := strconv.ParseInt(s.Size, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
size = 0 // Default to 0 if size parsing fails
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.Object{
|
||||||
|
ID: s.ID,
|
||||||
|
Path: s.FilePath,
|
||||||
|
Name: s.Name,
|
||||||
|
Size: size,
|
||||||
|
Modified: modTime,
|
||||||
|
Ctime: createTime,
|
||||||
|
IsFolder: isFolder,
|
||||||
|
}
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Degoo) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
|
item, err := d.getOverlay4(ctx, file.GetID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.Link{URL: item.URL}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Degoo) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||||
|
// This is done by calling the setUploadFile3 API with a special checksum and size.
|
||||||
|
const query = `mutation SetUploadFile3($Token: String!, $FileInfos: [FileInfoUpload3]!) { setUploadFile3(Token: $Token, FileInfos: $FileInfos) }`
|
||||||
|
|
||||||
|
variables := map[string]interface{}{
|
||||||
|
"Token": d.Token,
|
||||||
|
"FileInfos": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"Checksum": folderChecksum,
|
||||||
|
"Name": dirName,
|
||||||
|
"CreationTime": time.Now().UnixMilli(),
|
||||||
|
"ParentID": parentDir.GetID(),
|
||||||
|
"Size": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := d.apiCall(ctx, "SetUploadFile3", query, variables)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Degoo) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||||
|
const query = `mutation SetMoveFile($Token: String!, $Copy: Boolean, $NewParentID: String!, $FileIDs: [String]!) { setMoveFile(Token: $Token, Copy: $Copy, NewParentID: $NewParentID, FileIDs: $FileIDs) }`
|
||||||
|
|
||||||
|
variables := map[string]interface{}{
|
||||||
|
"Token": d.Token,
|
||||||
|
"Copy": false,
|
||||||
|
"NewParentID": dstDir.GetID(),
|
||||||
|
"FileIDs": []string{srcObj.GetID()},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := d.apiCall(ctx, "SetMoveFile", query, variables)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return srcObj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Degoo) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||||
|
const query = `mutation SetRenameFile($Token: String!, $FileRenames: [FileRenameInfo]!) { setRenameFile(Token: $Token, FileRenames: $FileRenames) }`
|
||||||
|
|
||||||
|
variables := map[string]interface{}{
|
||||||
|
"Token": d.Token,
|
||||||
|
"FileRenames": []DegooFileRenameInfo{
|
||||||
|
{
|
||||||
|
ID: srcObj.GetID(),
|
||||||
|
NewName: newName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := d.apiCall(ctx, "SetRenameFile", query, variables)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Degoo) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||||
|
// Copy is not implemented, Degoo API does not support direct copy.
|
||||||
|
return nil, errs.NotImplement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Degoo) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
|
// Remove deletes a file or folder (moves to trash).
|
||||||
|
const query = `mutation SetDeleteFile5($Token: String!, $IsInRecycleBin: Boolean!, $IDs: [IDType]!) { setDeleteFile5(Token: $Token, IsInRecycleBin: $IsInRecycleBin, IDs: $IDs) }`
|
||||||
|
|
||||||
|
variables := map[string]interface{}{
|
||||||
|
"Token": d.Token,
|
||||||
|
"IsInRecycleBin": false,
|
||||||
|
"IDs": []map[string]string{{"FileID": obj.GetID()}},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := d.apiCall(ctx, "SetDeleteFile5", query, variables)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Degoo) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
|
||||||
|
tmpF, err := file.CacheFullAndWriter(&up, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
parentID := dstDir.GetID()
|
||||||
|
|
||||||
|
// Calculate the checksum for the file.
|
||||||
|
checksum, err := d.checkSum(tmpF)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Get upload authorization via getBucketWriteAuth4.
|
||||||
|
auths, err := d.getBucketWriteAuth4(ctx, file, parentID, checksum)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Upload file.
|
||||||
|
// support rapid upload
|
||||||
|
if auths.GetBucketWriteAuth4[0].Error != "Already exist!" {
|
||||||
|
err = d.uploadS3(ctx, auths, tmpF, file, checksum)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Register metadata with setUploadFile3.
|
||||||
|
data, err := d.SetUploadFile3(ctx, file, parentID, checksum)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !data.SetUploadFile3 {
|
||||||
|
return fmt.Errorf("setUploadFile3 failed: %v", data)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
26
drivers/degoo/meta.go
Normal file
26
drivers/degoo/meta.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package degoo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Addition struct {
|
||||||
|
driver.RootID
|
||||||
|
Username string `json:"username" required:"true" help:"Your Degoo account email"`
|
||||||
|
Password string `json:"password" required:"true" help:"Your Degoo account password"`
|
||||||
|
Token string `json:"token" help:"Access token for Degoo API, will be obtained automatically if not provided"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = driver.Config{
|
||||||
|
Name: "Degoo",
|
||||||
|
LocalSort: true,
|
||||||
|
DefaultRoot: "0",
|
||||||
|
NoOverwriteUpload: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
op.RegisterDriver(func() driver.Driver {
|
||||||
|
return &Degoo{}
|
||||||
|
})
|
||||||
|
}
|
110
drivers/degoo/types.go
Normal file
110
drivers/degoo/types.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package degoo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DegooLoginRequest represents the login request body.
|
||||||
|
type DegooLoginRequest struct {
|
||||||
|
GenerateToken bool `json:"GenerateToken"`
|
||||||
|
Username string `json:"Username"`
|
||||||
|
Password string `json:"Password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DegooLoginResponse represents a successful login response.
|
||||||
|
type DegooLoginResponse struct {
|
||||||
|
Token string `json:"Token"`
|
||||||
|
RefreshToken string `json:"RefreshToken"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DegooAccessTokenRequest represents the token refresh request body.
|
||||||
|
type DegooAccessTokenRequest struct {
|
||||||
|
RefreshToken string `json:"RefreshToken"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DegooAccessTokenResponse represents the token refresh response.
|
||||||
|
type DegooAccessTokenResponse struct {
|
||||||
|
AccessToken string `json:"AccessToken"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DegooFileItem represents a Degoo file or folder.
|
||||||
|
type DegooFileItem struct {
|
||||||
|
ID string `json:"ID"`
|
||||||
|
ParentID string `json:"ParentID"`
|
||||||
|
Name string `json:"Name"`
|
||||||
|
Category int `json:"Category"`
|
||||||
|
Size string `json:"Size"`
|
||||||
|
URL string `json:"URL"`
|
||||||
|
CreationTime string `json:"CreationTime"`
|
||||||
|
LastModificationTime string `json:"LastModificationTime"`
|
||||||
|
LastUploadTime string `json:"LastUploadTime"`
|
||||||
|
MetadataID string `json:"MetadataID"`
|
||||||
|
DeviceID int64 `json:"DeviceID"`
|
||||||
|
FilePath string `json:"FilePath"`
|
||||||
|
IsInRecycleBin bool `json:"IsInRecycleBin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DegooErrors struct {
|
||||||
|
Path []string `json:"path"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
ErrorType string `json:"errorType"`
|
||||||
|
ErrorInfo interface{} `json:"errorInfo"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DegooGraphqlResponse is the common structure for GraphQL API responses.
|
||||||
|
type DegooGraphqlResponse struct {
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
Errors []DegooErrors `json:"errors,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DegooGetChildren5Data is the data field for getFileChildren5.
|
||||||
|
type DegooGetChildren5Data struct {
|
||||||
|
GetFileChildren5 struct {
|
||||||
|
Items []DegooFileItem `json:"Items"`
|
||||||
|
NextToken string `json:"NextToken"`
|
||||||
|
} `json:"getFileChildren5"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DegooGetOverlay4Data is the data field for getOverlay4.
|
||||||
|
type DegooGetOverlay4Data struct {
|
||||||
|
GetOverlay4 DegooFileItem `json:"getOverlay4"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DegooFileRenameInfo represents a file rename operation.
|
||||||
|
type DegooFileRenameInfo struct {
|
||||||
|
ID string `json:"ID"`
|
||||||
|
NewName string `json:"NewName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DegooFileIDs represents a list of file IDs for move operations.
|
||||||
|
type DegooFileIDs struct {
|
||||||
|
FileIDs []string `json:"FileIDs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DegooGetBucketWriteAuth4Data is the data field for GetBucketWriteAuth4.
|
||||||
|
type DegooGetBucketWriteAuth4Data struct {
|
||||||
|
GetBucketWriteAuth4 []struct {
|
||||||
|
AuthData struct {
|
||||||
|
PolicyBase64 string `json:"PolicyBase64"`
|
||||||
|
Signature string `json:"Signature"`
|
||||||
|
BaseURL string `json:"BaseURL"`
|
||||||
|
KeyPrefix string `json:"KeyPrefix"`
|
||||||
|
AccessKey struct {
|
||||||
|
Key string `json:"Key"`
|
||||||
|
Value string `json:"Value"`
|
||||||
|
} `json:"AccessKey"`
|
||||||
|
ACL string `json:"ACL"`
|
||||||
|
AdditionalBody []struct {
|
||||||
|
Key string `json:"Key"`
|
||||||
|
Value string `json:"Value"`
|
||||||
|
} `json:"AdditionalBody"`
|
||||||
|
} `json:"AuthData"`
|
||||||
|
Error interface{} `json:"Error"`
|
||||||
|
} `json:"getBucketWriteAuth4"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DegooSetUploadFile3Data is the data field for SetUploadFile3.
|
||||||
|
type DegooSetUploadFile3Data struct {
|
||||||
|
SetUploadFile3 bool `json:"setUploadFile3"`
|
||||||
|
}
|
198
drivers/degoo/upload.go
Normal file
198
drivers/degoo/upload.go
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
package degoo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Degoo) getBucketWriteAuth4(ctx context.Context, file model.FileStreamer, parentID string, checksum string) (*DegooGetBucketWriteAuth4Data, error) {
|
||||||
|
const query = `query GetBucketWriteAuth4(
|
||||||
|
$Token: String!
|
||||||
|
$ParentID: String!
|
||||||
|
$StorageUploadInfos: [StorageUploadInfo2]
|
||||||
|
) {
|
||||||
|
getBucketWriteAuth4(
|
||||||
|
Token: $Token
|
||||||
|
ParentID: $ParentID
|
||||||
|
StorageUploadInfos: $StorageUploadInfos
|
||||||
|
) {
|
||||||
|
AuthData {
|
||||||
|
PolicyBase64
|
||||||
|
Signature
|
||||||
|
BaseURL
|
||||||
|
KeyPrefix
|
||||||
|
AccessKey {
|
||||||
|
Key
|
||||||
|
Value
|
||||||
|
}
|
||||||
|
ACL
|
||||||
|
AdditionalBody {
|
||||||
|
Key
|
||||||
|
Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
variables := map[string]interface{}{
|
||||||
|
"Token": d.Token,
|
||||||
|
"ParentID": parentID,
|
||||||
|
"StorageUploadInfos": []map[string]string{{
|
||||||
|
"FileName": file.GetName(),
|
||||||
|
"Checksum": checksum,
|
||||||
|
"Size": strconv.FormatInt(file.GetSize(), 10),
|
||||||
|
}}}
|
||||||
|
|
||||||
|
data, err := d.apiCall(ctx, "GetBucketWriteAuth4", query, variables)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp DegooGetBucketWriteAuth4Data
|
||||||
|
err = json.Unmarshal(data, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkSum calculates the SHA1-based checksum for Degoo upload API.
|
||||||
|
func (d *Degoo) checkSum(file io.Reader) (string, error) {
|
||||||
|
seed := []byte{13, 7, 2, 2, 15, 40, 75, 117, 13, 10, 19, 16, 29, 23, 3, 36}
|
||||||
|
hasher := sha1.New()
|
||||||
|
hasher.Write(seed)
|
||||||
|
|
||||||
|
if _, err := utils.CopyWithBuffer(hasher, file); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
cs := hasher.Sum(nil)
|
||||||
|
|
||||||
|
csBytes := []byte{10, byte(len(cs))}
|
||||||
|
csBytes = append(csBytes, cs...)
|
||||||
|
csBytes = append(csBytes, 16, 0)
|
||||||
|
|
||||||
|
return strings.ReplaceAll(base64.StdEncoding.EncodeToString(csBytes), "/", "_"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Degoo) uploadS3(ctx context.Context, auths *DegooGetBucketWriteAuth4Data, tmpF model.File, file model.FileStreamer, checksum string) error {
|
||||||
|
a := auths.GetBucketWriteAuth4[0].AuthData
|
||||||
|
|
||||||
|
_, err := tmpF.Seek(0, io.SeekStart)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := utils.Ext(file.GetName())
|
||||||
|
key := fmt.Sprintf("%s%s/%s.%s", a.KeyPrefix, ext, checksum, ext)
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := multipart.NewWriter(&b)
|
||||||
|
err = w.WriteField("key", key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = w.WriteField("acl", a.ACL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = w.WriteField("policy", a.PolicyBase64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = w.WriteField("signature", a.Signature)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = w.WriteField(a.AccessKey.Key, a.AccessKey.Value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, additional := range a.AdditionalBody {
|
||||||
|
err = w.WriteField(additional.Key, additional.Value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = w.WriteField("Content-Type", "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.CreateFormFile("file", key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
headSize := b.Len()
|
||||||
|
err = w.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
head := bytes.NewReader(b.Bytes()[:headSize])
|
||||||
|
tail := bytes.NewReader(b.Bytes()[headSize:])
|
||||||
|
|
||||||
|
rateLimitedRd := driver.NewLimitedUploadStream(ctx, io.MultiReader(head, tmpF, tail))
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, a.BaseURL, rateLimitedRd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Add("ngsw-bypass", "1")
|
||||||
|
req.Header.Add("Content-Type", w.FormDataContentType())
|
||||||
|
|
||||||
|
res, err := d.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusNoContent {
|
||||||
|
return fmt.Errorf("upload failed with status code %d", res.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ driver.Driver = (*Degoo)(nil)
|
||||||
|
|
||||||
|
func (d *Degoo) SetUploadFile3(ctx context.Context, file model.FileStreamer, parentID string, checksum string) (*DegooSetUploadFile3Data, error) {
|
||||||
|
const query = `mutation SetUploadFile3($Token: String!, $FileInfos: [FileInfoUpload3]!) {
|
||||||
|
setUploadFile3(Token: $Token, FileInfos: $FileInfos)
|
||||||
|
}`
|
||||||
|
|
||||||
|
variables := map[string]interface{}{
|
||||||
|
"Token": d.Token,
|
||||||
|
"FileInfos": []map[string]string{{
|
||||||
|
"Checksum": checksum,
|
||||||
|
"CreationTime": strconv.FormatInt(file.CreateTime().UnixMilli(), 10),
|
||||||
|
"Name": file.GetName(),
|
||||||
|
"ParentID": parentID,
|
||||||
|
"Size": strconv.FormatInt(file.GetSize(), 10),
|
||||||
|
}}}
|
||||||
|
|
||||||
|
data, err := d.apiCall(ctx, "SetUploadFile3", query, variables)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp DegooSetUploadFile3Data
|
||||||
|
err = json.Unmarshal(data, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &resp, nil
|
||||||
|
}
|
245
drivers/degoo/util.go
Normal file
245
drivers/degoo/util.go
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
package degoo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Thanks to https://github.com/bernd-wechner/Degoo for API research.
|
||||||
|
|
||||||
|
const (
|
||||||
|
loginURL = "https://rest-api.degoo.com/login"
|
||||||
|
accessTokenURL = "https://rest-api.degoo.com/access-token/v2"
|
||||||
|
|
||||||
|
// Degoo GraphQL API endpoint.
|
||||||
|
apiURL = "https://production-appsync.degoo.com/graphql"
|
||||||
|
// Fixed API key.
|
||||||
|
apiKey = "da2-vs6twz5vnjdavpqndtbzg3prra"
|
||||||
|
// Checksum for new folder.
|
||||||
|
folderChecksum = "CgAQAg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// login performs the login process and retrieves the access token.
|
||||||
|
func (d *Degoo) login(ctx context.Context) error {
|
||||||
|
creds := DegooLoginRequest{
|
||||||
|
GenerateToken: true,
|
||||||
|
Username: d.Addition.Username,
|
||||||
|
Password: d.Addition.Password,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonCreds, err := json.Marshal(creds)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to serialize login credentials: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", loginURL, bytes.NewBuffer(jsonCreds))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create login request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("User-Agent", base.UserAgent)
|
||||||
|
req.Header.Set("Origin", "https://app.degoo.com")
|
||||||
|
|
||||||
|
resp, err := d.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("login request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("login failed: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
var loginResp DegooLoginResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&loginResp); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse login response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if loginResp.RefreshToken != "" {
|
||||||
|
tokenReq := DegooAccessTokenRequest{RefreshToken: loginResp.RefreshToken}
|
||||||
|
jsonTokenReq, err := json.Marshal(tokenReq)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to serialize access token request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenReqHTTP, err := http.NewRequestWithContext(ctx, "POST", accessTokenURL, bytes.NewBuffer(jsonTokenReq))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create access token request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenReqHTTP.Header.Set("User-Agent", base.UserAgent)
|
||||||
|
|
||||||
|
tokenResp, err := d.client.Do(tokenReqHTTP)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get access token: %w", err)
|
||||||
|
}
|
||||||
|
defer tokenResp.Body.Close()
|
||||||
|
|
||||||
|
var accessTokenResp DegooAccessTokenResponse
|
||||||
|
if err := json.NewDecoder(tokenResp.Body).Decode(&accessTokenResp); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse access token response: %w", err)
|
||||||
|
}
|
||||||
|
d.Token = accessTokenResp.AccessToken
|
||||||
|
} else if loginResp.Token != "" {
|
||||||
|
d.Token = loginResp.Token
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("login failed, no valid token returned")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiCall performs a Degoo GraphQL API request.
|
||||||
|
func (d *Degoo) apiCall(ctx context.Context, operationName, query string, variables map[string]interface{}) (json.RawMessage, error) {
|
||||||
|
reqBody := map[string]interface{}{
|
||||||
|
"operationName": operationName,
|
||||||
|
"query": query,
|
||||||
|
"variables": variables,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBody, err := json.Marshal(reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to serialize request body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(jsonBody))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("x-api-key", apiKey)
|
||||||
|
req.Header.Set("User-Agent", base.UserAgent)
|
||||||
|
|
||||||
|
if d.Token != "" {
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.Token))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := d.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("API request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("API response error: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
var degooResp DegooGraphqlResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(°ooResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode API response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(degooResp.Errors) > 0 {
|
||||||
|
if degooResp.Errors[0].ErrorType == "Unauthorized" {
|
||||||
|
err = d.login(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unauthorized access, login failed: %w", err)
|
||||||
|
}
|
||||||
|
// Retry the API call after re-login
|
||||||
|
return d.apiCall(ctx, operationName, query, variables)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("degoo API returned an error: %v", degooResp.Errors[0].Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return degooResp.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// humanReadableTimes converts Degoo timestamps to Go time.Time.
|
||||||
|
func humanReadableTimes(creation, modification, upload string) (cTime, mTime, uTime time.Time) {
|
||||||
|
cTime, _ = time.Parse(time.RFC3339, creation)
|
||||||
|
if modification != "" {
|
||||||
|
modMillis, _ := strconv.ParseInt(modification, 10, 64)
|
||||||
|
mTime = time.Unix(0, modMillis*int64(time.Millisecond))
|
||||||
|
}
|
||||||
|
if upload != "" {
|
||||||
|
upMillis, _ := strconv.ParseInt(upload, 10, 64)
|
||||||
|
uTime = time.Unix(0, upMillis*int64(time.Millisecond))
|
||||||
|
}
|
||||||
|
return cTime, mTime, uTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDevices fetches and caches top-level devices and folders.
|
||||||
|
func (d *Degoo) getDevices(ctx context.Context) error {
|
||||||
|
const query = `query GetFileChildren5($Token: String! $ParentID: String $AllParentIDs: [String] $Limit: Int! $Order: Int! $NextToken: String ) { getFileChildren5(Token: $Token ParentID: $ParentID AllParentIDs: $AllParentIDs Limit: $Limit Order: $Order NextToken: $NextToken) { Items { ParentID } NextToken } }`
|
||||||
|
variables := map[string]interface{}{
|
||||||
|
"Token": d.Token,
|
||||||
|
"ParentID": "0",
|
||||||
|
"Limit": 10,
|
||||||
|
"Order": 3,
|
||||||
|
}
|
||||||
|
data, err := d.apiCall(ctx, "GetFileChildren5", query, variables)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var resp DegooGetChildren5Data
|
||||||
|
if err := json.Unmarshal(data, &resp); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse device list: %w", err)
|
||||||
|
}
|
||||||
|
if d.RootFolderID == "0" {
|
||||||
|
if len(resp.GetFileChildren5.Items) > 0 {
|
||||||
|
d.RootFolderID = resp.GetFileChildren5.Items[0].ParentID
|
||||||
|
}
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAllFileChildren5 fetches all children of a directory with pagination.
|
||||||
|
func (d *Degoo) getAllFileChildren5(ctx context.Context, parentID string) ([]DegooFileItem, error) {
|
||||||
|
const query = `query GetFileChildren5($Token: String! $ParentID: String $AllParentIDs: [String] $Limit: Int! $Order: Int! $NextToken: String ) { getFileChildren5(Token: $Token ParentID: $ParentID AllParentIDs: $AllParentIDs Limit: $Limit Order: $Order NextToken: $NextToken) { Items { ID ParentID Name Category Size CreationTime LastModificationTime LastUploadTime FilePath IsInRecycleBin DeviceID MetadataID } NextToken } }`
|
||||||
|
var allItems []DegooFileItem
|
||||||
|
nextToken := ""
|
||||||
|
for {
|
||||||
|
variables := map[string]interface{}{
|
||||||
|
"Token": d.Token,
|
||||||
|
"ParentID": parentID,
|
||||||
|
"Limit": 1000,
|
||||||
|
"Order": 3,
|
||||||
|
}
|
||||||
|
if nextToken != "" {
|
||||||
|
variables["NextToken"] = nextToken
|
||||||
|
}
|
||||||
|
data, err := d.apiCall(ctx, "GetFileChildren5", query, variables)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var resp DegooGetChildren5Data
|
||||||
|
if err := json.Unmarshal(data, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
allItems = append(allItems, resp.GetFileChildren5.Items...)
|
||||||
|
if resp.GetFileChildren5.NextToken == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
nextToken = resp.GetFileChildren5.NextToken
|
||||||
|
}
|
||||||
|
return allItems, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOverlay4 fetches metadata for a single item by ID.
|
||||||
|
func (d *Degoo) getOverlay4(ctx context.Context, id string) (DegooFileItem, error) {
|
||||||
|
const query = `query GetOverlay4($Token: String!, $ID: IDType!) { getOverlay4(Token: $Token, ID: $ID) { ID ParentID Name Category Size CreationTime LastModificationTime LastUploadTime URL FilePath IsInRecycleBin DeviceID MetadataID } }`
|
||||||
|
variables := map[string]interface{}{
|
||||||
|
"Token": d.Token,
|
||||||
|
"ID": map[string]string{
|
||||||
|
"FileID": id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data, err := d.apiCall(ctx, "GetOverlay4", query, variables)
|
||||||
|
if err != nil {
|
||||||
|
return DegooFileItem{}, err
|
||||||
|
}
|
||||||
|
var resp DegooGetOverlay4Data
|
||||||
|
if err := json.Unmarshal(data, &resp); err != nil {
|
||||||
|
return DegooFileItem{}, fmt.Errorf("failed to parse item metadata: %w", err)
|
||||||
|
}
|
||||||
|
return resp.GetOverlay4, nil
|
||||||
|
}
|
Reference in New Issue
Block a user