diff --git a/middleware/auth.go b/middleware/auth.go index 52651f8..eb7f466 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -273,3 +273,19 @@ func UpyunCallbackAuth() gin.HandlerFunc { c.Next() } } + +// OneDriveCallbackAuth OneDrive回调签名验证 +// TODO 解耦 +func OneDriveCallbackAuth() gin.HandlerFunc { + return func(c *gin.Context) { + // 验证key并查找用户 + resp, _ := uploadCallbackCheck(c) + if resp.Code != 0 { + c.JSON(401, serializer.QiniuCallbackFailed{Error: resp.Msg}) + c.Abort() + return + } + + c.Next() + } +} diff --git a/pkg/filesystem/driver/onedrive/api.go b/pkg/filesystem/driver/onedrive/api.go new file mode 100644 index 0000000..0d8dd31 --- /dev/null +++ b/pkg/filesystem/driver/onedrive/api.go @@ -0,0 +1,192 @@ +package onedrive + +import ( + "context" + "encoding/json" + "github.com/HFO4/cloudreve/pkg/request" + "io/ioutil" + "net/http" + "net/url" + "path" + "strings" +) + +// RespError 接口返回错误 +type RespError struct { + APIError APIError `json:"error"` +} + +// APIError 接口返回的错误内容 +type APIError struct { + Code string `json:"code"` + Message string `json:"message"` +} + +// UploadSessionResponse 分片上传会话 +type UploadSessionResponse struct { + DataContext string `json:"@odata.context"` + ExpirationDateTime string `json:"expirationDateTime"` + NextExpectedRanges []string `json:"nextExpectedRanges"` + UploadURL string `json:"uploadUrl"` +} + +// FileInfo 文件元信息 +type FileInfo struct { + Name string `json:"name"` + Size uint64 `json:"size"` + Image imageInfo `json:"image"` + ParentReference parentReference `json:"parentReference"` +} + +type imageInfo struct { + Height int `json:"height"` + Width int `json:"width"` +} + +type parentReference struct { + Path string `json:"path"` + Name string `json:"name"` + ID string `json:"id"` +} + +// GetSourcePath 获取文件的绝对路径 +func (info *FileInfo) GetSourcePath() string { + res, err := url.PathUnescape( + strings.TrimPrefix( + path.Join( + strings.TrimPrefix(info.ParentReference.Path, "/drive/root:"), + info.Name, + ), + "/", + ), + ) + if err != nil { + return "" + } + return res +} + +// Error 实现error接口 +func (err RespError) Error() string { + return err.APIError.Message +} + +func (client *Client) getRequestURL(api string) string { + base, _ := url.Parse(client.Endpoints.EndpointURL) + if base == nil { + return "" + } + base.Path = path.Join(base.Path, api) + return base.String() +} + +// Meta 根据资源ID获取文件元信息 +func (client *Client) Meta(ctx context.Context, id string) (*FileInfo, error) { + + requestURL := client.getRequestURL("/me/drive/items/" + id) + res, err := client.request(ctx, "GET", requestURL+"?expand=thumbnails", "") + if err != nil { + return nil, err + } + + var ( + decodeErr error + fileInfo FileInfo + ) + decodeErr = json.Unmarshal([]byte(res), &fileInfo) + if decodeErr != nil { + return nil, decodeErr + } + + return &fileInfo, nil + +} + +// CreateUploadSession 创建分片上传会话 +func (client *Client) CreateUploadSession(ctx context.Context, dst string, opts ...Option) (string, error) { + + options := newDefaultOption() + for _, o := range opts { + o.apply(options) + } + + dst = strings.TrimPrefix(dst, "/") + requestURL := client.getRequestURL("me/drive/root:/" + dst + ":/createUploadSession") + body := map[string]map[string]interface{}{ + "item": { + "@microsoft.graph.conflictBehavior": options.conflictBehavior, + }, + } + bodyBytes, _ := json.Marshal(body) + + res, err := client.request(ctx, "POST", requestURL, string(bodyBytes)) + if err != nil { + return "", err + } + + var ( + decodeErr error + uploadSession UploadSessionResponse + ) + decodeErr = json.Unmarshal([]byte(res), &uploadSession) + if decodeErr != nil { + return "", decodeErr + } + + return uploadSession.UploadURL, nil +} + +func sysError(err error) *RespError { + return &RespError{APIError: APIError{ + Code: "system", + Message: err.Error(), + }} +} + +func (client *Client) request(ctx context.Context, method string, url string, body string) (string, *RespError) { + + // 获取凭证 + err := client.UpdateCredential(ctx) + if err != nil { + return "", sysError(err) + } + + // 发送请求 + bodyReader := ioutil.NopCloser(strings.NewReader(body)) + res := client.Request.Request( + method, + url, + bodyReader, + request.WithContentLength(int64(len(body))), + request.WithHeader(http.Header{ + "Authorization": {"Bearer " + client.Credential.AccessToken}, + "Content-Type": {"application/json"}, + }), + request.WithContext(ctx), + ) + + if res.Err != nil { + return "", sysError(res.Err) + } + + respBody, err := res.GetResponse() + if err != nil { + return "", sysError(err) + } + + // 解析请求响应 + var ( + errResp RespError + decodeErr error + ) + // 如果有错误 + if res.Response.StatusCode != 200 { + decodeErr = json.Unmarshal([]byte(respBody), &errResp) + if decodeErr != nil { + return "", sysError(err) + } + return "", &errResp + } + + return respBody, nil +} diff --git a/pkg/filesystem/driver/onedrive/handller.go b/pkg/filesystem/driver/onedrive/handller.go index 90a43a1..e66427b 100644 --- a/pkg/filesystem/driver/onedrive/handller.go +++ b/pkg/filesystem/driver/onedrive/handller.go @@ -4,6 +4,7 @@ import ( "context" "errors" 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/serializer" "io" @@ -51,18 +52,25 @@ func (handler Driver) Source( // Token 获取上传策略和认证Token func (handler Driver) Token(ctx context.Context, TTL int64, key string) (serializer.UploadCredential, error) { - err := handler.Client.UpdateCredential(ctx) + + // 读取上下文中生成的存储路径 + savePath, ok := ctx.Value(fsctx.SavePathCtx).(string) + if !ok { + return serializer.UploadCredential{}, errors.New("无法获取存储路径") + } + + // 生成回调地址 + siteURL := model.GetSiteURL() + apiBaseURI, _ := url.Parse("/api/v3/callback/onedrive/finish/" + key) + apiURL := siteURL.ResolveReference(apiBaseURI) + + uploadURL, err := handler.Client.CreateUploadSession(ctx, savePath, WithConflictBehavior("fail")) if err != nil { return serializer.UploadCredential{}, err } + return serializer.UploadCredential{ - Policy: handler.Client.Credential.AccessToken, + Policy: uploadURL, + Token: apiURL.String(), }, 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 } diff --git a/pkg/filesystem/driver/onedrive/options.go b/pkg/filesystem/driver/onedrive/options.go index e9e444c..dd5d855 100644 --- a/pkg/filesystem/driver/onedrive/options.go +++ b/pkg/filesystem/driver/onedrive/options.go @@ -1,14 +1,18 @@ package onedrive +import "time" + // Option 发送请求的额外设置 type Option interface { apply(*options) } type options struct { - redirect string - code string - refreshToken string + redirect string + code string + refreshToken string + conflictBehavior string + expires time.Time } type optionFunc func(*options) @@ -27,10 +31,27 @@ func WithRefreshToken(t string) Option { }) } +// WithConflictBehavior 设置文件重名后的处理方式 +func WithConflictBehavior(t string) Option { + return optionFunc(func(o *options) { + o.conflictBehavior = t + }) +} + +// WithExpires 设置过期时间 +func WithExpires(t time.Time) Option { + return optionFunc(func(o *options) { + o.expires = t + }) +} + func (f optionFunc) apply(o *options) { f(o) } func newDefaultOption() *options { - return &options{} + return &options{ + conflictBehavior: "fail", + expires: time.Now().UTC().Add(time.Duration(1) * time.Hour), + } } diff --git a/pkg/filesystem/filesystem.go b/pkg/filesystem/filesystem.go index e780514..4c32883 100644 --- a/pkg/filesystem/filesystem.go +++ b/pkg/filesystem/filesystem.go @@ -2,6 +2,7 @@ package filesystem import ( "context" + "errors" "github.com/HFO4/cloudreve/models" "github.com/HFO4/cloudreve/pkg/auth" "github.com/HFO4/cloudreve/pkg/conf" @@ -195,7 +196,6 @@ func (fs *FileSystem) DispatchHandler() error { } // NewFileSystemFromContext 从gin.Context创建文件系统 -// TODO 用户不存在时使用匿名文件系统 func NewFileSystemFromContext(c *gin.Context) (*FileSystem, error) { user, exist := c.Get("user") if !exist { @@ -205,6 +205,33 @@ func NewFileSystemFromContext(c *gin.Context) (*FileSystem, error) { return fs, err } +// NewFileSystemFromCallback 从gin.Context创建回调用文件系统 +// TODO 测试 +func NewFileSystemFromCallback(c *gin.Context) (*FileSystem, error) { + fs, err := NewFileSystemFromContext(c) + if err != nil { + return nil, err + } + + // 获取回调会话 + callbackSessionRaw, ok := c.Get("callbackSession") + if !ok { + return nil, errors.New("找不到回调会话") + } + callbackSession := callbackSessionRaw.(*serializer.UploadSession) + + // 重新指向上传策略 + policy, err := model.GetPolicyByID(callbackSession.PolicyID) + if err != nil { + return nil, err + } + fs.Policy = &policy + fs.User.Policy = policy + err = fs.DispatchHandler() + + return fs, err +} + // SetTargetFile 设置当前处理的目标文件 func (fs *FileSystem) SetTargetFile(files *[]model.File) { if len(fs.FileTarget) == 0 { diff --git a/pkg/filesystem/upload.go b/pkg/filesystem/upload.go index 4d2d0c8..f1be15e 100644 --- a/pkg/filesystem/upload.go +++ b/pkg/filesystem/upload.go @@ -148,8 +148,10 @@ func (fs *FileSystem) GetUploadToken(ctx context.Context, path string, size uint var err error // 是否需要预先生成存储路径 + var savePath string if fs.User.Policy.IsPathGenerateNeeded() { - ctx = context.WithValue(ctx, fsctx.SavePathCtx, fs.GenerateSavePath(ctx, local.FileStream{})) + savePath = fs.GenerateSavePath(ctx, local.FileStream{Name: name}) + ctx = context.WithValue(ctx, fsctx.SavePathCtx, savePath) } ctx = context.WithValue(ctx, fsctx.FileSizeCtx, size) @@ -168,6 +170,8 @@ func (fs *FileSystem) GetUploadToken(ctx context.Context, path string, size uint PolicyID: fs.User.GetPolicyID(), VirtualPath: path, Name: name, + Size: size, + SavePath: savePath, }, callBackSessionTTL, ) diff --git a/pkg/serializer/upload.go b/pkg/serializer/upload.go index 8a94aff..d031987 100644 --- a/pkg/serializer/upload.go +++ b/pkg/serializer/upload.go @@ -30,6 +30,8 @@ type UploadSession struct { PolicyID uint VirtualPath string Name string + Size uint64 + SavePath string } // UploadCallback 上传回调正文 diff --git a/routers/controllers/callback.go b/routers/controllers/callback.go index 8878b23..34f5bb1 100644 --- a/routers/controllers/callback.go +++ b/routers/controllers/callback.go @@ -65,3 +65,14 @@ func UpyunCallback(c *gin.Context) { c.JSON(200, ErrorResponse(err)) } } + +// OneDriveCallback OneDrive上传完成客户端回调 +func OneDriveCallback(c *gin.Context) { + var callbackBody callback.OneDriveCallback + if err := c.ShouldBindJSON(&callbackBody); err == nil { + res := callbackBody.PreProcess(c) + c.JSON(200, res) + } else { + c.JSON(200, ErrorResponse(err)) + } +} diff --git a/routers/router.go b/routers/router.go index 22f89ab..e6a4155 100644 --- a/routers/router.go +++ b/routers/router.go @@ -150,6 +150,15 @@ func InitMasterRouter() *gin.Engine { middleware.UpyunCallbackAuth(), controllers.UpyunCallback, ) + onedrive := callback.Group("onedrive") + { + // 文件上传完成 + onedrive.POST( + "finish/:key", + middleware.OneDriveCallbackAuth(), + controllers.OneDriveCallback, + ) + } } // 需要登录保护的 diff --git a/service/callback/upload.go b/service/callback/upload.go index dcbbd11..3db4d55 100644 --- a/service/callback/upload.go +++ b/service/callback/upload.go @@ -2,13 +2,15 @@ package callback import ( "context" - model "github.com/HFO4/cloudreve/models" + "fmt" "github.com/HFO4/cloudreve/pkg/filesystem" "github.com/HFO4/cloudreve/pkg/filesystem/driver/local" + "github.com/HFO4/cloudreve/pkg/filesystem/driver/onedrive" "github.com/HFO4/cloudreve/pkg/filesystem/fsctx" "github.com/HFO4/cloudreve/pkg/serializer" "github.com/HFO4/cloudreve/pkg/util" "github.com/gin-gonic/gin" + "strings" ) // CallbackProcessService 上传请求回调正文接口 @@ -44,6 +46,12 @@ type UpyunCallbackService struct { Size uint64 `form:"file_size"` } +// OneDriveCallback OneDrive 客户端回调正文 +type OneDriveCallback struct { + ID string `json:"id" binding:"required"` + Meta *onedrive.FileInfo +} + // GetBody 返回回调正文 func (service UpyunCallbackService) GetBody(session *serializer.UploadSession) serializer.UploadCallback { res := serializer.UploadCallback{ @@ -68,37 +76,34 @@ func (service UploadCallbackService) GetBody(session *serializer.UploadSession) } } +// GetBody 返回回调正文 +func (service OneDriveCallback) GetBody(session *serializer.UploadSession) serializer.UploadCallback { + var picInfo = "0,0" + if service.Meta.Image.Width != 0 { + picInfo = fmt.Sprintf("%d,%d", service.Meta.Image.Width, service.Meta.Image.Height) + } + return serializer.UploadCallback{ + Name: session.Name, + SourceName: session.SavePath, + PicInfo: picInfo, + Size: session.Size, + } +} + // ProcessCallback 处理上传结果回调 func ProcessCallback(service CallbackProcessService, c *gin.Context) serializer.Response { // 创建文件系统 - fs, err := filesystem.NewFileSystemFromContext(c) + fs, err := filesystem.NewFileSystemFromCallback(c) if err != nil { return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err) } defer fs.Recycle() // 获取回调会话 - callbackSessionRaw, ok := c.Get("callbackSession") - if !ok { - return serializer.Err(serializer.CodeInternalSetting, "找不到回调会话", nil) - } + callbackSessionRaw, _ := c.Get("callbackSession") callbackSession := callbackSessionRaw.(*serializer.UploadSession) - - // 获取回调正文 callbackBody := service.GetBody(callbackSession) - // 重新指向上传策略 - policy, err := model.GetPolicyByID(callbackSession.PolicyID) - if err != nil { - return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err) - } - fs.Policy = &policy - fs.User.Policy = policy - err = fs.DispatchHandler() - if err != nil { - return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err) - } - // 获取父目录 exist, parentFolder := fs.IsPathExist(callbackSession.VirtualPath) if !exist { @@ -140,3 +145,32 @@ func ProcessCallback(service CallbackProcessService, c *gin.Context) serializer. Code: 0, } } + +// PreProcess 对OneDrive客户端回调进行预处理验证 +func (service *OneDriveCallback) PreProcess(c *gin.Context) serializer.Response { + // 创建文件系统 + fs, err := filesystem.NewFileSystemFromCallback(c) + if err != nil { + return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err) + } + defer fs.Recycle() + + // 获取回调会话 + callbackSessionRaw, _ := c.Get("callbackSession") + callbackSession := callbackSessionRaw.(*serializer.UploadSession) + + // 获取文件信息 + info, err := fs.Handler.(onedrive.Driver).Client.Meta(context.Background(), service.ID) + if err != nil { + return serializer.Err(serializer.CodeUploadFailed, "文件元信息查询失败", err) + } + + // 验证与回调会话中是否一致 + actualPath := strings.TrimPrefix(callbackSession.SavePath, "/") + if callbackSession.Size != info.Size || info.GetSourcePath() != actualPath { + // TODO 删除文件信息 + return serializer.Err(serializer.CodeUploadFailed, "文件信息不一致", err) + } + service.Meta = info + return ProcessCallback(service, c) +}