parent
4c18e5acd1
commit
37926e3133
@ -1 +1 @@
|
|||||||
Subproject commit 88e4b7fbf3d5e5806ad0cedce99d845ef21704c7
|
Subproject commit 5e66c6fd9c8b50f31a7a2d1f1e77ef13543ee366
|
@ -0,0 +1,72 @@
|
|||||||
|
package googledrive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||||
|
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
|
||||||
|
"github.com/cloudreve/Cloudreve/v3/pkg/request"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client Google Drive client
|
||||||
|
type Client struct {
|
||||||
|
Endpoints *Endpoints
|
||||||
|
Policy *model.Policy
|
||||||
|
Credential *Credential
|
||||||
|
|
||||||
|
ClientID string
|
||||||
|
ClientSecret string
|
||||||
|
Redirect string
|
||||||
|
|
||||||
|
Request request.Client
|
||||||
|
ClusterController cluster.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
// Endpoints OneDrive客户端相关设置
|
||||||
|
type Endpoints struct {
|
||||||
|
UserConsentEndpoint string // OAuth认证的基URL
|
||||||
|
TokenEndpoint string // OAuth token 基URL
|
||||||
|
EndpointURL string // 接口请求的基URL
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
TokenCachePrefix = "googledrive_"
|
||||||
|
|
||||||
|
oauthEndpoint = "https://oauth2.googleapis.com/token"
|
||||||
|
userConsentBase = "https://accounts.google.com/o/oauth2/auth"
|
||||||
|
v3DriveEndpoint = "https://www.googleapis.com/drive/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Defualt required scopes
|
||||||
|
RequiredScope = []string{
|
||||||
|
"https://www.googleapis.com/auth/drive",
|
||||||
|
"openid",
|
||||||
|
"profile",
|
||||||
|
"https://www.googleapis.com/auth/userinfo.profile",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrInvalidRefreshToken 上传策略无有效的RefreshToken
|
||||||
|
ErrInvalidRefreshToken = errors.New("no valid refresh token in this policy")
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewClient 根据存储策略获取新的client
|
||||||
|
func NewClient(policy *model.Policy) (*Client, error) {
|
||||||
|
client := &Client{
|
||||||
|
Endpoints: &Endpoints{
|
||||||
|
TokenEndpoint: oauthEndpoint,
|
||||||
|
UserConsentEndpoint: userConsentBase,
|
||||||
|
EndpointURL: v3DriveEndpoint,
|
||||||
|
},
|
||||||
|
Credential: &Credential{
|
||||||
|
RefreshToken: policy.AccessKey,
|
||||||
|
},
|
||||||
|
Policy: policy,
|
||||||
|
ClientID: policy.BucketName,
|
||||||
|
ClientSecret: policy.SecretKey,
|
||||||
|
Redirect: policy.OptionsSerialized.OauthRedirect,
|
||||||
|
Request: request.NewClient(),
|
||||||
|
ClusterController: cluster.DefaultController,
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
package googledrive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||||
|
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
|
||||||
|
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
|
||||||
|
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
|
||||||
|
"github.com/cloudreve/Cloudreve/v3/pkg/request"
|
||||||
|
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Driver Google Drive 适配器
|
||||||
|
type Driver struct {
|
||||||
|
Policy *model.Policy
|
||||||
|
HTTPClient request.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://localhost:3000/api/v3/callback/googledrive/auth?code=4/0AVHEtk4AaNbo5YoCrMSGgoJfZfe6SgEOVmA7XtalZl8BMtdsAIRWqxt6jO4NKJCxGVxyQA&scope=profile%20openid%20https://www.googleapis.com/auth/userinfo.profile%20https://www.googleapis.com/auth/drive&authuser=0&prompt=consent
|
||||||
|
// https://accounts.google.com/o/oauth2/v2/auth?client_id=89866991293-5uja7qsbl8btuak3hb41h3o8u9jhlckg.apps.googleusercontent.com&response_type=code&redirect_uri=http://localhost:3000/api/v3/callback/googledrive/auth&scope=openid+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive&access_type=offline&prompt=consent
|
||||||
|
//https://accounts.google.com/o/oauth2/auth?client_id=202264815644.apps.googleusercontent.com&response_type=code&redirect_uri=http%3A%2F%2F127.0.0.1%3A53682%2F&scope=openid+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fphotoslibrary&access_type=offline&prompt=consent&state=MjAyMjY0ODE1NjQ0LmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tOjpYNFozY2E4eGZXRGIxVm9vLUY5YTdaeEo6Omh0dHA6Ly8xMjcuMC4wLjE6NTM2ODIv
|
||||||
|
|
||||||
|
// NewDriver 从存储策略初始化新的Driver实例
|
||||||
|
func NewDriver(policy *model.Policy) (driver.Handler, error) {
|
||||||
|
return &Driver{
|
||||||
|
Policy: policy,
|
||||||
|
HTTPClient: request.NewClient(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) Put(ctx context.Context, file fsctx.FileHeader) error {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) Delete(ctx context.Context, files []string) ([]string, error) {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) Get(ctx context.Context, path string) (response.RSCloser, error) {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) Source(ctx context.Context, path string, url url.URL, ttl int64, isDownload bool, speed int) (string, error) {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) Token(ctx context.Context, ttl int64, uploadSession *serializer.UploadSession, file fsctx.FileHeader) (*serializer.UploadCredential, error) {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) CancelToken(ctx context.Context, uploadSession *serializer.UploadSession) error {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) List(ctx context.Context, path string, recursive bool) ([]response.Object, error) {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
@ -0,0 +1,174 @@
|
|||||||
|
package googledrive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||||
|
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/oauth"
|
||||||
|
"github.com/cloudreve/Cloudreve/v3/pkg/request"
|
||||||
|
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Credential 获取token时返回的凭证
|
||||||
|
type Credential struct {
|
||||||
|
ExpiresIn int64 `json:"expires_in"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuthError OAuth相关接口的错误响应
|
||||||
|
type OAuthError struct {
|
||||||
|
ErrorType string `json:"error"`
|
||||||
|
ErrorDescription string `json:"error_description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error 实现error接口
|
||||||
|
func (err OAuthError) Error() string {
|
||||||
|
return err.ErrorDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuthURL 获取OAuth认证页面URL
|
||||||
|
func (client *Client) OAuthURL(ctx context.Context, scope []string) string {
|
||||||
|
query := url.Values{
|
||||||
|
"client_id": {client.ClientID},
|
||||||
|
"scope": {strings.Join(scope, " ")},
|
||||||
|
"response_type": {"code"},
|
||||||
|
"redirect_uri": {client.Redirect},
|
||||||
|
"access_type": {"offline"},
|
||||||
|
"prompt": {"consent"},
|
||||||
|
}
|
||||||
|
|
||||||
|
u, _ := url.Parse(client.Endpoints.UserConsentEndpoint)
|
||||||
|
u.RawQuery = query.Encode()
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObtainToken 通过code或refresh_token兑换token
|
||||||
|
func (client *Client) ObtainToken(ctx context.Context, code, refreshToken string) (*Credential, error) {
|
||||||
|
body := url.Values{
|
||||||
|
"client_id": {client.ClientID},
|
||||||
|
"redirect_uri": {client.Redirect},
|
||||||
|
"client_secret": {client.ClientSecret},
|
||||||
|
}
|
||||||
|
if code != "" {
|
||||||
|
body.Add("grant_type", "authorization_code")
|
||||||
|
body.Add("code", code)
|
||||||
|
} else {
|
||||||
|
body.Add("grant_type", "refresh_token")
|
||||||
|
body.Add("refresh_token", refreshToken)
|
||||||
|
}
|
||||||
|
strBody := body.Encode()
|
||||||
|
|
||||||
|
res := client.Request.Request(
|
||||||
|
"POST",
|
||||||
|
client.Endpoints.TokenEndpoint,
|
||||||
|
io.NopCloser(strings.NewReader(strBody)),
|
||||||
|
request.WithHeader(http.Header{
|
||||||
|
"Content-Type": {"application/x-www-form-urlencoded"}},
|
||||||
|
),
|
||||||
|
request.WithContentLength(int64(len(strBody))),
|
||||||
|
)
|
||||||
|
if res.Err != nil {
|
||||||
|
return nil, res.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, err := res.GetResponse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errResp OAuthError
|
||||||
|
credential Credential
|
||||||
|
decodeErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
if res.Response.StatusCode != 200 {
|
||||||
|
decodeErr = json.Unmarshal([]byte(respBody), &errResp)
|
||||||
|
} else {
|
||||||
|
decodeErr = json.Unmarshal([]byte(respBody), &credential)
|
||||||
|
}
|
||||||
|
if decodeErr != nil {
|
||||||
|
return nil, decodeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if errResp.ErrorType != "" {
|
||||||
|
return nil, errResp
|
||||||
|
}
|
||||||
|
|
||||||
|
return &credential, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCredential 更新凭证,并检查有效期
|
||||||
|
func (client *Client) UpdateCredential(ctx context.Context, isSlave bool) error {
|
||||||
|
if isSlave {
|
||||||
|
return client.fetchCredentialFromMaster(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth.GlobalMutex.Lock(client.Policy.ID)
|
||||||
|
defer oauth.GlobalMutex.Unlock(client.Policy.ID)
|
||||||
|
|
||||||
|
// 如果已存在凭证
|
||||||
|
if client.Credential != nil && client.Credential.AccessToken != "" {
|
||||||
|
// 检查已有凭证是否过期
|
||||||
|
if client.Credential.ExpiresIn > time.Now().Unix() {
|
||||||
|
// 未过期,不要更新
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试从缓存中获取凭证
|
||||||
|
if cacheCredential, ok := cache.Get(TokenCachePrefix + client.ClientID); ok {
|
||||||
|
credential := cacheCredential.(Credential)
|
||||||
|
if credential.ExpiresIn > time.Now().Unix() {
|
||||||
|
client.Credential = &credential
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取新的凭证
|
||||||
|
if client.Credential == nil || client.Credential.RefreshToken == "" {
|
||||||
|
// 无有效的RefreshToken
|
||||||
|
util.Log().Error("Failed to refresh credential for policy %q, please login your Google account again.", client.Policy.Name)
|
||||||
|
return ErrInvalidRefreshToken
|
||||||
|
}
|
||||||
|
|
||||||
|
credential, err := client.ObtainToken(ctx, "", client.Credential.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新有效期为绝对时间戳
|
||||||
|
expires := credential.ExpiresIn - 60
|
||||||
|
credential.ExpiresIn = time.Now().Add(time.Duration(expires) * time.Second).Unix()
|
||||||
|
// refresh token for Google Drive does not expire in production
|
||||||
|
credential.RefreshToken = client.Credential.RefreshToken
|
||||||
|
client.Credential = credential
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
cache.Set(TokenCachePrefix+client.ClientID, *credential, int(expires))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) AccessToken() string {
|
||||||
|
return client.Credential.AccessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCredential 更新凭证,并检查有效期
|
||||||
|
func (client *Client) fetchCredentialFromMaster(ctx context.Context) error {
|
||||||
|
res, err := client.ClusterController.GetPolicyOauthToken(client.Policy.MasterID, client.Policy.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Credential = &Credential{AccessToken: res}
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package onedrive
|
package oauth
|
||||||
|
|
||||||
import "sync"
|
import "sync"
|
||||||
|
|
@ -0,0 +1,8 @@
|
|||||||
|
package oauth
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type TokenProvider interface {
|
||||||
|
UpdateCredential(ctx context.Context, isSlave bool) error
|
||||||
|
AccessToken() string
|
||||||
|
}
|
Loading…
Reference in new issue