mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2025-09-19 12:16:24 +08:00
feat(degoo): token improvement (#1149)
* Update driver.go Signed-off-by: Caspian <app@caspian.im> * Update meta.go Signed-off-by: Caspian <app@caspian.im> * Update util.go Signed-off-by: Caspian <app@caspian.im> * Update util.go Signed-off-by: Caspian <app@caspian.im> * Update util.go Signed-off-by: Caspian <app@caspian.im> * Update util.go Signed-off-by: MadDogOwner <xiaoran@xrgzs.top> * make account optional * ensure username and password Signed-off-by: MadDogOwner <xiaoran@xrgzs.top> --------- Signed-off-by: Caspian <app@caspian.im> Signed-off-by: MadDogOwner <xiaoran@xrgzs.top> Co-authored-by: MadDogOwner <xiaoran@xrgzs.top>
This commit is contained in:
@ -32,11 +32,9 @@ func (d *Degoo) Init(ctx context.Context) error {
|
|||||||
|
|
||||||
d.client = base.HttpClient
|
d.client = base.HttpClient
|
||||||
|
|
||||||
if d.Token == "" {
|
// Ensure we have a valid token (will login if needed or refresh if expired)
|
||||||
err := d.login(ctx)
|
if err := d.ensureValidToken(ctx); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("failed to initialize token: %w", err)
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return d.getDevices(ctx)
|
return d.getDevices(ctx)
|
||||||
@ -87,7 +85,7 @@ func (d *Degoo) MakeDir(ctx context.Context, parentDir model.Obj, dirName string
|
|||||||
const query = `mutation SetUploadFile3($Token: String!, $FileInfos: [FileInfoUpload3]!) { setUploadFile3(Token: $Token, FileInfos: $FileInfos) }`
|
const query = `mutation SetUploadFile3($Token: String!, $FileInfos: [FileInfoUpload3]!) { setUploadFile3(Token: $Token, FileInfos: $FileInfos) }`
|
||||||
|
|
||||||
variables := map[string]interface{}{
|
variables := map[string]interface{}{
|
||||||
"Token": d.Token,
|
"Token": d.AccessToken,
|
||||||
"FileInfos": []map[string]interface{}{
|
"FileInfos": []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"Checksum": folderChecksum,
|
"Checksum": folderChecksum,
|
||||||
@ -111,7 +109,7 @@ func (d *Degoo) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj,
|
|||||||
const query = `mutation SetMoveFile($Token: String!, $Copy: Boolean, $NewParentID: String!, $FileIDs: [String]!) { setMoveFile(Token: $Token, Copy: $Copy, NewParentID: $NewParentID, FileIDs: $FileIDs) }`
|
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{}{
|
variables := map[string]interface{}{
|
||||||
"Token": d.Token,
|
"Token": d.AccessToken,
|
||||||
"Copy": false,
|
"Copy": false,
|
||||||
"NewParentID": dstDir.GetID(),
|
"NewParentID": dstDir.GetID(),
|
||||||
"FileIDs": []string{srcObj.GetID()},
|
"FileIDs": []string{srcObj.GetID()},
|
||||||
@ -129,7 +127,7 @@ func (d *Degoo) Rename(ctx context.Context, srcObj model.Obj, newName string) er
|
|||||||
const query = `mutation SetRenameFile($Token: String!, $FileRenames: [FileRenameInfo]!) { setRenameFile(Token: $Token, FileRenames: $FileRenames) }`
|
const query = `mutation SetRenameFile($Token: String!, $FileRenames: [FileRenameInfo]!) { setRenameFile(Token: $Token, FileRenames: $FileRenames) }`
|
||||||
|
|
||||||
variables := map[string]interface{}{
|
variables := map[string]interface{}{
|
||||||
"Token": d.Token,
|
"Token": d.AccessToken,
|
||||||
"FileRenames": []DegooFileRenameInfo{
|
"FileRenames": []DegooFileRenameInfo{
|
||||||
{
|
{
|
||||||
ID: srcObj.GetID(),
|
ID: srcObj.GetID(),
|
||||||
@ -155,7 +153,7 @@ func (d *Degoo) Remove(ctx context.Context, obj model.Obj) error {
|
|||||||
const query = `mutation SetDeleteFile5($Token: String!, $IsInRecycleBin: Boolean!, $IDs: [IDType]!) { setDeleteFile5(Token: $Token, IsInRecycleBin: $IsInRecycleBin, IDs: $IDs) }`
|
const query = `mutation SetDeleteFile5($Token: String!, $IsInRecycleBin: Boolean!, $IDs: [IDType]!) { setDeleteFile5(Token: $Token, IsInRecycleBin: $IsInRecycleBin, IDs: $IDs) }`
|
||||||
|
|
||||||
variables := map[string]interface{}{
|
variables := map[string]interface{}{
|
||||||
"Token": d.Token,
|
"Token": d.AccessToken,
|
||||||
"IsInRecycleBin": false,
|
"IsInRecycleBin": false,
|
||||||
"IDs": []map[string]string{{"FileID": obj.GetID()}},
|
"IDs": []map[string]string{{"FileID": obj.GetID()}},
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,10 @@ import (
|
|||||||
|
|
||||||
type Addition struct {
|
type Addition struct {
|
||||||
driver.RootID
|
driver.RootID
|
||||||
Username string `json:"username" required:"true" help:"Your Degoo account email"`
|
Username string `json:"username" help:"Your Degoo account email"`
|
||||||
Password string `json:"password" required:"true" help:"Your Degoo account password"`
|
Password string `json:"password" help:"Your Degoo account password"`
|
||||||
Token string `json:"token" help:"Access token for Degoo API, will be obtained automatically if not provided"`
|
RefreshToken string `json:"refresh_token" help:"Refresh token for automatic token renewal, obtained automatically"`
|
||||||
|
AccessToken string `json:"access_token" help:"Access token for Degoo API, obtained automatically"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
|
@ -49,7 +49,7 @@ func (d *Degoo) getBucketWriteAuth4(ctx context.Context, file model.FileStreamer
|
|||||||
}`
|
}`
|
||||||
|
|
||||||
variables := map[string]interface{}{
|
variables := map[string]interface{}{
|
||||||
"Token": d.Token,
|
"Token": d.AccessToken,
|
||||||
"ParentID": parentID,
|
"ParentID": parentID,
|
||||||
"StorageUploadInfos": []map[string]string{{
|
"StorageUploadInfos": []map[string]string{{
|
||||||
"FileName": file.GetName(),
|
"FileName": file.GetName(),
|
||||||
@ -174,7 +174,7 @@ func (d *Degoo) SetUploadFile3(ctx context.Context, file model.FileStreamer, par
|
|||||||
}`
|
}`
|
||||||
|
|
||||||
variables := map[string]interface{}{
|
variables := map[string]interface{}{
|
||||||
"Token": d.Token,
|
"Token": d.AccessToken,
|
||||||
"FileInfos": []map[string]string{{
|
"FileInfos": []map[string]string{{
|
||||||
"Checksum": checksum,
|
"Checksum": checksum,
|
||||||
"CreationTime": strconv.FormatInt(file.CreateTime().UnixMilli(), 10),
|
"CreationTime": strconv.FormatInt(file.CreateTime().UnixMilli(), 10),
|
||||||
|
@ -3,10 +3,13 @@ package degoo
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
||||||
@ -16,23 +19,195 @@ import (
|
|||||||
// Thanks to https://github.com/bernd-wechner/Degoo for API research.
|
// Thanks to https://github.com/bernd-wechner/Degoo for API research.
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// API endpoints
|
||||||
loginURL = "https://rest-api.degoo.com/login"
|
loginURL = "https://rest-api.degoo.com/login"
|
||||||
accessTokenURL = "https://rest-api.degoo.com/access-token/v2"
|
accessTokenURL = "https://rest-api.degoo.com/access-token/v2"
|
||||||
|
|
||||||
// Degoo GraphQL API endpoint.
|
|
||||||
apiURL = "https://production-appsync.degoo.com/graphql"
|
apiURL = "https://production-appsync.degoo.com/graphql"
|
||||||
// Fixed API key.
|
|
||||||
|
// API configuration
|
||||||
apiKey = "da2-vs6twz5vnjdavpqndtbzg3prra"
|
apiKey = "da2-vs6twz5vnjdavpqndtbzg3prra"
|
||||||
// Checksum for new folder.
|
|
||||||
folderChecksum = "CgAQAg"
|
folderChecksum = "CgAQAg"
|
||||||
|
|
||||||
|
// Token management
|
||||||
|
tokenRefreshThreshold = 5 * time.Minute
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
minRequestInterval = 1 * time.Second
|
||||||
|
|
||||||
|
// Error messages
|
||||||
|
errRateLimited = "rate limited (429), please try again later"
|
||||||
|
errUnauthorized = "unauthorized access"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Global rate limiting - protects against concurrent API calls
|
||||||
|
lastRequestTime time.Time
|
||||||
|
requestMutex sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// JWT payload structure for token expiration checking
|
||||||
|
type JWTPayload struct {
|
||||||
|
UserID string `json:"userID"`
|
||||||
|
Exp int64 `json:"exp"`
|
||||||
|
Iat int64 `json:"iat"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limiting helper functions
|
||||||
|
|
||||||
|
// applyRateLimit ensures minimum interval between API requests
|
||||||
|
func applyRateLimit() {
|
||||||
|
requestMutex.Lock()
|
||||||
|
defer requestMutex.Unlock()
|
||||||
|
|
||||||
|
if !lastRequestTime.IsZero() {
|
||||||
|
if elapsed := time.Since(lastRequestTime); elapsed < minRequestInterval {
|
||||||
|
time.Sleep(minRequestInterval - elapsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastRequestTime = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP request helper functions
|
||||||
|
|
||||||
|
// createJSONRequest creates a new HTTP request with JSON body
|
||||||
|
func createJSONRequest(ctx context.Context, method, url string, body interface{}) (*http.Request, error) {
|
||||||
|
jsonBody, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(jsonBody))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("User-Agent", base.UserAgent)
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkHTTPResponse checks for common HTTP error conditions
|
||||||
|
func checkHTTPResponse(resp *http.Response, operation string) error {
|
||||||
|
if resp.StatusCode == http.StatusTooManyRequests {
|
||||||
|
return fmt.Errorf("%s %s", operation, errRateLimited)
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("%s failed: %s", operation, resp.Status)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTokenExpired checks if the JWT token is expired or will expire soon
|
||||||
|
func (d *Degoo) isTokenExpired() bool {
|
||||||
|
if d.AccessToken == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := extractJWTPayload(d.AccessToken)
|
||||||
|
if err != nil {
|
||||||
|
return true // Invalid token format
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token expires within the threshold
|
||||||
|
expireTime := time.Unix(payload.Exp, 0)
|
||||||
|
return time.Now().Add(tokenRefreshThreshold).After(expireTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractJWTPayload extracts and parses JWT payload
|
||||||
|
func extractJWTPayload(token string) (*JWTPayload, error) {
|
||||||
|
parts := strings.Split(token, ".")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return nil, fmt.Errorf("invalid JWT format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the payload (second part)
|
||||||
|
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode JWT payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var jwtPayload JWTPayload
|
||||||
|
if err := json.Unmarshal(payload, &jwtPayload); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse JWT payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &jwtPayload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshToken attempts to refresh the access token using the refresh token
|
||||||
|
func (d *Degoo) refreshToken(ctx context.Context) error {
|
||||||
|
if d.RefreshToken == "" {
|
||||||
|
return fmt.Errorf("no refresh token available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create request
|
||||||
|
tokenReq := DegooAccessTokenRequest{RefreshToken: d.RefreshToken}
|
||||||
|
req, err := createJSONRequest(ctx, "POST", accessTokenURL, tokenReq)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create refresh token request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute request
|
||||||
|
resp, err := d.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("refresh token request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Check response
|
||||||
|
if err := checkHTTPResponse(resp, "refresh token"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var accessTokenResp DegooAccessTokenResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&accessTokenResp); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse access token response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if accessTokenResp.AccessToken == "" {
|
||||||
|
return fmt.Errorf("empty access token received")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.AccessToken = accessTokenResp.AccessToken
|
||||||
|
// Save the updated token to storage
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureValidToken ensures we have a valid, non-expired token
|
||||||
|
func (d *Degoo) ensureValidToken(ctx context.Context) error {
|
||||||
|
// Check if token is expired or will expire soon
|
||||||
|
if d.isTokenExpired() {
|
||||||
|
// Try to refresh token first if we have a refresh token
|
||||||
|
if d.RefreshToken != "" {
|
||||||
|
if refreshErr := d.refreshToken(ctx); refreshErr == nil {
|
||||||
|
return nil // Successfully refreshed
|
||||||
|
} else {
|
||||||
|
// If refresh failed, fall back to full login
|
||||||
|
fmt.Printf("Token refresh failed, falling back to full login: %v\n", refreshErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform full login
|
||||||
|
if d.Username != "" && d.Password != "" {
|
||||||
|
return d.login(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// login performs the login process and retrieves the access token.
|
// login performs the login process and retrieves the access token.
|
||||||
func (d *Degoo) login(ctx context.Context) error {
|
func (d *Degoo) login(ctx context.Context) error {
|
||||||
|
if d.Username == "" || d.Password == "" {
|
||||||
|
return fmt.Errorf("username or password not provided")
|
||||||
|
}
|
||||||
|
|
||||||
creds := DegooLoginRequest{
|
creds := DegooLoginRequest{
|
||||||
GenerateToken: true,
|
GenerateToken: true,
|
||||||
Username: d.Addition.Username,
|
Username: d.Username,
|
||||||
Password: d.Addition.Password,
|
Password: d.Password,
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonCreds, err := json.Marshal(creds)
|
jsonCreds, err := json.Marshal(creds)
|
||||||
@ -54,6 +229,11 @@ func (d *Degoo) login(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Handle rate limiting (429 Too Many Requests)
|
||||||
|
if resp.StatusCode == http.StatusTooManyRequests {
|
||||||
|
return fmt.Errorf("login rate limited (429), please try again later")
|
||||||
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return fmt.Errorf("login failed: %s", resp.Status)
|
return fmt.Errorf("login failed: %s", resp.Status)
|
||||||
}
|
}
|
||||||
@ -87,71 +267,108 @@ func (d *Degoo) login(ctx context.Context) error {
|
|||||||
if err := json.NewDecoder(tokenResp.Body).Decode(&accessTokenResp); err != nil {
|
if err := json.NewDecoder(tokenResp.Body).Decode(&accessTokenResp); err != nil {
|
||||||
return fmt.Errorf("failed to parse access token response: %w", err)
|
return fmt.Errorf("failed to parse access token response: %w", err)
|
||||||
}
|
}
|
||||||
d.Token = accessTokenResp.AccessToken
|
d.AccessToken = accessTokenResp.AccessToken
|
||||||
|
d.RefreshToken = loginResp.RefreshToken // Save refresh token
|
||||||
} else if loginResp.Token != "" {
|
} else if loginResp.Token != "" {
|
||||||
d.Token = loginResp.Token
|
d.AccessToken = loginResp.Token
|
||||||
|
d.RefreshToken = "" // Direct token, no refresh token available
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("login failed, no valid token returned")
|
return fmt.Errorf("login failed, no valid token returned")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save the updated tokens to storage
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// apiCall performs a Degoo GraphQL API request.
|
// apiCall performs a Degoo GraphQL API request.
|
||||||
func (d *Degoo) apiCall(ctx context.Context, operationName, query string, variables map[string]interface{}) (json.RawMessage, error) {
|
func (d *Degoo) apiCall(ctx context.Context, operationName, query string, variables map[string]interface{}) (json.RawMessage, error) {
|
||||||
|
// Apply rate limiting
|
||||||
|
applyRateLimit()
|
||||||
|
|
||||||
|
// Ensure we have a valid token before making the API call
|
||||||
|
if err := d.ensureValidToken(ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to ensure valid token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the Token in variables if it exists (after potential refresh)
|
||||||
|
d.updateTokenInVariables(variables)
|
||||||
|
|
||||||
|
return d.executeGraphQLRequest(ctx, operationName, query, variables)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateTokenInVariables updates the Token field in GraphQL variables
|
||||||
|
func (d *Degoo) updateTokenInVariables(variables map[string]interface{}) {
|
||||||
|
if variables != nil {
|
||||||
|
if _, hasToken := variables["Token"]; hasToken {
|
||||||
|
variables["Token"] = d.AccessToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeGraphQLRequest executes a GraphQL request with retry logic
|
||||||
|
func (d *Degoo) executeGraphQLRequest(ctx context.Context, operationName, query string, variables map[string]interface{}) (json.RawMessage, error) {
|
||||||
reqBody := map[string]interface{}{
|
reqBody := map[string]interface{}{
|
||||||
"operationName": operationName,
|
"operationName": operationName,
|
||||||
"query": query,
|
"query": query,
|
||||||
"variables": variables,
|
"variables": variables,
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonBody, err := json.Marshal(reqBody)
|
// Create and configure request
|
||||||
|
req, err := createJSONRequest(ctx, "POST", apiURL, reqBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to serialize request body: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(jsonBody))
|
// Set Degoo-specific headers
|
||||||
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("x-api-key", apiKey)
|
||||||
req.Header.Set("User-Agent", base.UserAgent)
|
if d.AccessToken != "" {
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.AccessToken))
|
||||||
if d.Token != "" {
|
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.Token))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute request
|
||||||
resp, err := d.client.Do(req)
|
resp, err := d.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("API request failed: %w", err)
|
return nil, fmt.Errorf("GraphQL API request failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
// Check for HTTP errors
|
||||||
return nil, fmt.Errorf("API response error: %s", resp.Status)
|
if err := checkHTTPResponse(resp, "GraphQL API"); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse GraphQL response
|
||||||
var degooResp DegooGraphqlResponse
|
var degooResp DegooGraphqlResponse
|
||||||
if err := json.NewDecoder(resp.Body).Decode(°ooResp); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(°ooResp); err != nil {
|
||||||
return nil, fmt.Errorf("failed to decode API response: %w", err)
|
return nil, fmt.Errorf("failed to decode GraphQL response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle GraphQL errors
|
||||||
if len(degooResp.Errors) > 0 {
|
if len(degooResp.Errors) > 0 {
|
||||||
if degooResp.Errors[0].ErrorType == "Unauthorized" {
|
return d.handleGraphQLError(ctx, degooResp.Errors[0], operationName, query, variables)
|
||||||
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
|
return degooResp.Data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleGraphQLError handles GraphQL-level errors with retry logic
|
||||||
|
func (d *Degoo) handleGraphQLError(ctx context.Context, gqlError DegooErrors, operationName, query string, variables map[string]interface{}) (json.RawMessage, error) {
|
||||||
|
if gqlError.ErrorType == "Unauthorized" {
|
||||||
|
// Re-login and retry
|
||||||
|
if err := d.login(ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("%s, login failed: %w", errUnauthorized, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update token in variables and retry
|
||||||
|
d.updateTokenInVariables(variables)
|
||||||
|
return d.apiCall(ctx, operationName, query, variables)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("GraphQL API error: %s", gqlError.Message)
|
||||||
|
}
|
||||||
|
|
||||||
// humanReadableTimes converts Degoo timestamps to Go time.Time.
|
// humanReadableTimes converts Degoo timestamps to Go time.Time.
|
||||||
func humanReadableTimes(creation, modification, upload string) (cTime, mTime, uTime time.Time) {
|
func humanReadableTimes(creation, modification, upload string) (cTime, mTime, uTime time.Time) {
|
||||||
cTime, _ = time.Parse(time.RFC3339, creation)
|
cTime, _ = time.Parse(time.RFC3339, creation)
|
||||||
@ -170,7 +387,7 @@ func humanReadableTimes(creation, modification, upload string) (cTime, mTime, uT
|
|||||||
func (d *Degoo) getDevices(ctx context.Context) error {
|
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 } }`
|
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{}{
|
variables := map[string]interface{}{
|
||||||
"Token": d.Token,
|
"Token": d.AccessToken,
|
||||||
"ParentID": "0",
|
"ParentID": "0",
|
||||||
"Limit": 10,
|
"Limit": 10,
|
||||||
"Order": 3,
|
"Order": 3,
|
||||||
@ -199,7 +416,7 @@ func (d *Degoo) getAllFileChildren5(ctx context.Context, parentID string) ([]Deg
|
|||||||
nextToken := ""
|
nextToken := ""
|
||||||
for {
|
for {
|
||||||
variables := map[string]interface{}{
|
variables := map[string]interface{}{
|
||||||
"Token": d.Token,
|
"Token": d.AccessToken,
|
||||||
"ParentID": parentID,
|
"ParentID": parentID,
|
||||||
"Limit": 1000,
|
"Limit": 1000,
|
||||||
"Order": 3,
|
"Order": 3,
|
||||||
@ -228,7 +445,7 @@ func (d *Degoo) getAllFileChildren5(ctx context.Context, parentID string) ([]Deg
|
|||||||
func (d *Degoo) getOverlay4(ctx context.Context, id string) (DegooFileItem, error) {
|
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 } }`
|
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{}{
|
variables := map[string]interface{}{
|
||||||
"Token": d.Token,
|
"Token": d.AccessToken,
|
||||||
"ID": map[string]string{
|
"ID": map[string]string{
|
||||||
"FileID": id,
|
"FileID": id,
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user