mirror of
				https://github.com/cloudreve/cloudreve.git
				synced 2025-10-31 08:39:10 +08:00 
			
		
		
		
	Feat: OneDrive OAuth / refresh token
This commit is contained in:
		| @ -43,6 +43,9 @@ type PolicyOption struct { | ||||
| 	FileType []string `json:"file_type"` | ||||
| 	// MimeType | ||||
| 	MimeType string `json:"mimetype"` | ||||
|  | ||||
| 	// OdRedirect Onedrive重定向地址 | ||||
| 	OdRedirect string `json:"od_redirect,omitempty"` | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| @ -190,3 +193,17 @@ func (policy *Policy) GetUploadURL() string { | ||||
| 	} | ||||
| 	return server.ResolveReference(controller).String() | ||||
| } | ||||
|  | ||||
| // UpdateAccessKey 更新 AccessKey | ||||
| // TODO 测试 | ||||
| func (policy *Policy) UpdateAccessKey(key string) error { | ||||
| 	policy.AccessKey = key | ||||
| 	err := DB.Save(policy).Error | ||||
| 	policy.ClearCache() | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| // ClearCache 清空policy缓存 | ||||
| func (policy *Policy) ClearCache() { | ||||
| 	cache.Deletes([]string{strconv.FormatUint(uint64(policy.ID), 10)}, "policy_") | ||||
| } | ||||
|  | ||||
							
								
								
									
										59
									
								
								pkg/filesystem/driver/onedrive/client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								pkg/filesystem/driver/onedrive/client.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | ||||
| package onedrive | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	model "github.com/HFO4/cloudreve/models" | ||||
| 	"github.com/HFO4/cloudreve/pkg/request" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	// ErrAuthEndpoint 无法解析授权端点地址 | ||||
| 	ErrAuthEndpoint        = errors.New("无法解析授权端点地址") | ||||
| 	ErrInvalidRefreshToken = errors.New("上传策略无有效的RefreshToken") | ||||
| ) | ||||
|  | ||||
| // Client OneDrive客户端 | ||||
| type Client struct { | ||||
| 	Endpoints  *Endpoints | ||||
| 	Policy     *model.Policy | ||||
| 	Credential *Credential | ||||
|  | ||||
| 	ClientID     string | ||||
| 	ClientSecret string | ||||
| 	Redirect     string | ||||
|  | ||||
| 	Request request.Client | ||||
| } | ||||
|  | ||||
| // Endpoints OneDrive客户端相关设置 | ||||
| type Endpoints struct { | ||||
| 	OAuthURL       string // OAuth认证的基URL | ||||
| 	OAuthEndpoints *oauthEndpoint | ||||
| 	EndpointURL    string // 接口请求的基URL | ||||
| } | ||||
|  | ||||
| // NewClient 根据存储策略获取新的client | ||||
| func NewClient(policy *model.Policy) (*Client, error) { | ||||
| 	client := &Client{ | ||||
| 		Endpoints: &Endpoints{ | ||||
| 			OAuthURL:    policy.BaseURL, | ||||
| 			EndpointURL: policy.Server, | ||||
| 		}, | ||||
| 		Credential: &Credential{ | ||||
| 			RefreshToken: policy.AccessKey, | ||||
| 		}, | ||||
| 		Policy:       policy, | ||||
| 		ClientID:     policy.BucketName, | ||||
| 		ClientSecret: policy.SecretKey, | ||||
| 		Redirect:     policy.OptionsSerialized.OdRedirect, | ||||
| 		Request:      request.HTTPClient{}, | ||||
| 	} | ||||
|  | ||||
| 	oauthBase := client.getOAuthEndpoint() | ||||
| 	if oauthBase == nil { | ||||
| 		return nil, ErrAuthEndpoint | ||||
| 	} | ||||
| 	client.Endpoints.OAuthEndpoints = oauthBase | ||||
|  | ||||
| 	return client, nil | ||||
| } | ||||
							
								
								
									
										68
									
								
								pkg/filesystem/driver/onedrive/handller.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								pkg/filesystem/driver/onedrive/handller.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,68 @@ | ||||
| package onedrive | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	model "github.com/HFO4/cloudreve/models" | ||||
| 	"github.com/HFO4/cloudreve/pkg/filesystem/response" | ||||
| 	"github.com/HFO4/cloudreve/pkg/serializer" | ||||
| 	"io" | ||||
| 	"net/url" | ||||
| ) | ||||
|  | ||||
| // Driver OneDrive 适配器 | ||||
| type Driver struct { | ||||
| 	Policy *model.Policy | ||||
| 	Client *Client | ||||
| } | ||||
|  | ||||
| // Get 获取文件 | ||||
| func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) { | ||||
| 	return nil, errors.New("未实现") | ||||
| } | ||||
|  | ||||
| // Put 将文件流保存到指定目录 | ||||
| func (handler Driver) Put(ctx context.Context, file io.ReadCloser, dst string, size uint64) error { | ||||
| 	return errors.New("未实现") | ||||
| } | ||||
|  | ||||
| // Delete 删除一个或多个文件, | ||||
| // 返回未删除的文件,及遇到的最后一个错误 | ||||
| func (handler Driver) Delete(ctx context.Context, files []string) ([]string, error) { | ||||
| 	return []string{}, errors.New("未实现") | ||||
| } | ||||
|  | ||||
| // Thumb 获取文件缩略图 | ||||
| func (handler Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) { | ||||
| 	return nil, errors.New("未实现") | ||||
| } | ||||
|  | ||||
| // Source 获取外链URL | ||||
| func (handler Driver) Source( | ||||
| 	ctx context.Context, | ||||
| 	path string, | ||||
| 	baseURL url.URL, | ||||
| 	ttl int64, | ||||
| 	isDownload bool, | ||||
| 	speed int, | ||||
| ) (string, error) { | ||||
| 	return "", errors.New("未实现") | ||||
| } | ||||
|  | ||||
| // Token 获取上传策略和认证Token | ||||
| func (handler Driver) Token(ctx context.Context, TTL int64, key string) (serializer.UploadCredential, error) { | ||||
| 	err := handler.Client.UpdateCredential(ctx) | ||||
| 	if err != nil { | ||||
| 		return serializer.UploadCredential{}, err | ||||
| 	} | ||||
| 	return serializer.UploadCredential{ | ||||
| 		Policy: handler.Client.Credential.AccessToken, | ||||
| 	}, nil | ||||
| 	//res,err := handler.Client.ObtainToken(ctx,WithCode("M2e92c4a9-de12-cdda-9cf4-e01f67272831")) | ||||
| 	//if err != nil{ | ||||
| 	//	return serializer.UploadCredential{},err | ||||
| 	//} | ||||
| 	//return serializer.UploadCredential{ | ||||
| 	//	Policy:res.RefreshToken, | ||||
| 	//}, nil | ||||
| } | ||||
							
								
								
									
										192
									
								
								pkg/filesystem/driver/onedrive/oauth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								pkg/filesystem/driver/onedrive/oauth.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,192 @@ | ||||
| package onedrive | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/gob" | ||||
| 	"encoding/json" | ||||
| 	"github.com/HFO4/cloudreve/pkg/cache" | ||||
| 	"github.com/HFO4/cloudreve/pkg/request" | ||||
| 	"github.com/HFO4/cloudreve/pkg/util" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| // oauthEndpoint OAuth接口地址 | ||||
| type oauthEndpoint struct { | ||||
| 	token     url.URL | ||||
| 	authorize url.URL | ||||
| } | ||||
|  | ||||
| // Credential 获取token时返回的凭证 | ||||
| type Credential struct { | ||||
| 	TokenType    string `json:"token_type"` | ||||
| 	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"` | ||||
| 	CorrelationID    string `json:"correlation_id"` | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	gob.Register(Credential{}) | ||||
| } | ||||
|  | ||||
| // 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}, | ||||
| 	} | ||||
| 	client.Endpoints.OAuthEndpoints.authorize.RawQuery = query.Encode() | ||||
| 	return client.Endpoints.OAuthEndpoints.authorize.String() | ||||
| } | ||||
|  | ||||
| // getOAuthEndpoint 根据指定的AuthURL获取详细的认证接口地址 | ||||
| func (client *Client) getOAuthEndpoint() *oauthEndpoint { | ||||
| 	base, err := url.Parse(client.Endpoints.OAuthURL) | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	var ( | ||||
| 		token     *url.URL | ||||
| 		authorize *url.URL | ||||
| 	) | ||||
| 	switch base.Host { | ||||
| 	case "login.live.com": | ||||
| 		token, _ = url.Parse("https://login.live.com/oauth20_token.srf") | ||||
| 		authorize, _ = url.Parse("https://login.live.com/oauth20_authorize.srf") | ||||
| 	default: | ||||
| 		token, _ = url.Parse("https://login.microsoftonline.com/common/oauth2/v2.0/token") | ||||
| 		authorize, _ = url.Parse("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") | ||||
| 	} | ||||
|  | ||||
| 	return &oauthEndpoint{ | ||||
| 		token:     *token, | ||||
| 		authorize: *authorize, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // ObtainToken 通过code或refresh_token兑换token | ||||
| func (client *Client) ObtainToken(ctx context.Context, opts ...Option) (*Credential, error) { | ||||
| 	options := newDefaultOption() | ||||
| 	for _, o := range opts { | ||||
| 		o.apply(options) | ||||
| 	} | ||||
|  | ||||
| 	body := url.Values{ | ||||
| 		"client_id":     {client.ClientID}, | ||||
| 		"redirect_uri":  {client.Redirect}, | ||||
| 		"client_secret": {client.ClientSecret}, | ||||
| 	} | ||||
| 	if options.code != "" { | ||||
| 		body.Add("grant_type", "authorization_code") | ||||
| 		body.Add("code", options.code) | ||||
| 	} else { | ||||
| 		body.Add("grant_type", "refresh_token") | ||||
| 		body.Add("refresh_token", options.refreshToken) | ||||
| 	} | ||||
| 	strBody := body.Encode() | ||||
|  | ||||
| 	res := client.Request.Request( | ||||
| 		"POST", | ||||
| 		client.Endpoints.OAuthEndpoints.token.String(), | ||||
| 		ioutil.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) error { | ||||
| 	// 如果已存在凭证 | ||||
| 	if client.Credential != nil && client.Credential.AccessToken != "" { | ||||
| 		// 检查已有凭证是否过期 | ||||
| 		if client.Credential.ExpiresIn > time.Now().Unix() { | ||||
| 			// 未过期,不要更新 | ||||
| 			return nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// 尝试从缓存中获取凭证 | ||||
| 	if cacheCredential, ok := cache.Get("onedrive_" + 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("上传策略[%s]凭证刷新失败,请重新授权OneDrive账号", client.Policy.Name) | ||||
| 		return ErrInvalidRefreshToken | ||||
| 	} | ||||
|  | ||||
| 	credential, err := client.ObtainToken(ctx, WithRefreshToken(client.Credential.RefreshToken)) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// 更新有效期为绝对时间戳 | ||||
| 	expires := credential.ExpiresIn - 60 | ||||
| 	credential.ExpiresIn = time.Now().Add(time.Duration(expires) * time.Second).Unix() | ||||
| 	client.Credential = credential | ||||
|  | ||||
| 	// 更新存储策略的 RefreshToken | ||||
| 	client.Policy.UpdateAccessKey(credential.RefreshToken) | ||||
|  | ||||
| 	// 更新缓存 | ||||
| 	cache.Set("onedrive_"+client.ClientID, *credential, int(expires)) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										36
									
								
								pkg/filesystem/driver/onedrive/options.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								pkg/filesystem/driver/onedrive/options.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | ||||
| package onedrive | ||||
|  | ||||
| // Option 发送请求的额外设置 | ||||
| type Option interface { | ||||
| 	apply(*options) | ||||
| } | ||||
|  | ||||
| type options struct { | ||||
| 	redirect     string | ||||
| 	code         string | ||||
| 	refreshToken string | ||||
| } | ||||
|  | ||||
| type optionFunc func(*options) | ||||
|  | ||||
| // WithCode 设置接口Code | ||||
| func WithCode(t string) Option { | ||||
| 	return optionFunc(func(o *options) { | ||||
| 		o.code = t | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // WithRefreshToken 设置接口RefreshToken | ||||
| func WithRefreshToken(t string) Option { | ||||
| 	return optionFunc(func(o *options) { | ||||
| 		o.refreshToken = t | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func (f optionFunc) apply(o *options) { | ||||
| 	f(o) | ||||
| } | ||||
|  | ||||
| func newDefaultOption() *options { | ||||
| 	return &options{} | ||||
| } | ||||
| @ -12,9 +12,11 @@ import ( | ||||
| 	model "github.com/HFO4/cloudreve/models" | ||||
| 	"github.com/HFO4/cloudreve/pkg/filesystem/fsctx" | ||||
| 	"github.com/HFO4/cloudreve/pkg/filesystem/response" | ||||
| 	"github.com/HFO4/cloudreve/pkg/request" | ||||
| 	"github.com/HFO4/cloudreve/pkg/serializer" | ||||
| 	"github.com/upyun/go-sdk/upyun" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| @ -40,12 +42,63 @@ type Driver struct { | ||||
|  | ||||
| // Get 获取文件 | ||||
| func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) { | ||||
| 	return nil, errors.New("未实现") | ||||
| 	// 给文件名加上随机参数以强制拉取 | ||||
| 	path = fmt.Sprintf("%s?v=%d", path, time.Now().UnixNano()) | ||||
|  | ||||
| 	// 获取文件源地址 | ||||
| 	downloadURL, err := handler.Source( | ||||
| 		ctx, | ||||
| 		path, | ||||
| 		url.URL{}, | ||||
| 		int64(model.GetIntSetting("preview_timeout", 60)), | ||||
| 		false, | ||||
| 		0, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// 获取文件数据流 | ||||
| 	client := request.HTTPClient{} | ||||
| 	resp, err := client.Request( | ||||
| 		"GET", | ||||
| 		downloadURL, | ||||
| 		nil, | ||||
| 		request.WithContext(ctx), | ||||
| 		request.WithHeader( | ||||
| 			http.Header{"Cache-Control": {"no-cache", "no-store", "must-revalidate"}}, | ||||
| 		), | ||||
| 	).CheckHTTPResponse(200).GetRSCloser() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	resp.SetFirstFakeChunk() | ||||
|  | ||||
| 	// 尝试自主获取文件大小 | ||||
| 	if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok { | ||||
| 		resp.SetContentLength(int64(file.Size)) | ||||
| 	} | ||||
|  | ||||
| 	return resp, nil | ||||
|  | ||||
| } | ||||
|  | ||||
| // Put 将文件流保存到指定目录 | ||||
| func (handler Driver) Put(ctx context.Context, file io.ReadCloser, dst string, size uint64) error { | ||||
| 	return errors.New("未实现") | ||||
| 	defer file.Close() | ||||
|  | ||||
| 	up := upyun.NewUpYun(&upyun.UpYunConfig{ | ||||
| 		Bucket:   handler.Policy.BucketName, | ||||
| 		Operator: handler.Policy.AccessKey, | ||||
| 		Password: handler.Policy.SecretKey, | ||||
| 	}) | ||||
| 	err := up.Put(&upyun.PutObjectConfig{ | ||||
| 		Path:   dst, | ||||
| 		Reader: file, | ||||
| 	}) | ||||
|  | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| // Delete 删除一个或多个文件, | ||||
|  | ||||
| @ -6,6 +6,7 @@ import ( | ||||
| 	"github.com/HFO4/cloudreve/pkg/auth" | ||||
| 	"github.com/HFO4/cloudreve/pkg/conf" | ||||
| 	"github.com/HFO4/cloudreve/pkg/filesystem/driver/local" | ||||
| 	"github.com/HFO4/cloudreve/pkg/filesystem/driver/onedrive" | ||||
| 	"github.com/HFO4/cloudreve/pkg/filesystem/driver/oss" | ||||
| 	"github.com/HFO4/cloudreve/pkg/filesystem/driver/qiniu" | ||||
| 	"github.com/HFO4/cloudreve/pkg/filesystem/driver/remote" | ||||
| @ -181,6 +182,13 @@ func (fs *FileSystem) DispatchHandler() error { | ||||
| 			Policy: currentPolicy, | ||||
| 		} | ||||
| 		return nil | ||||
| 	case "onedrive": | ||||
| 		client, err := onedrive.NewClient(currentPolicy) | ||||
| 		fs.Handler = onedrive.Driver{ | ||||
| 			Policy: currentPolicy, | ||||
| 			Client: client, | ||||
| 		} | ||||
| 		return err | ||||
| 	default: | ||||
| 		return ErrUnknownPolicyType | ||||
| 	} | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 HFO4
					HFO4