diff --git a/go.mod b/go.mod index 53094b8..f54550b 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2 github.com/mojocn/base64Captcha v0.0.0-20190801020520-752b1cd608b2 github.com/pkg/errors v0.8.0 + github.com/qiniu/api.v7/v7 v7.4.0 github.com/smartystreets/goconvey v1.6.4 // indirect github.com/stretchr/testify v1.4.0 gopkg.in/go-playground/validator.v8 v8.18.2 diff --git a/pkg/filesystem/filesystem.go b/pkg/filesystem/filesystem.go index 598f323..511598a 100644 --- a/pkg/filesystem/filesystem.go +++ b/pkg/filesystem/filesystem.go @@ -6,7 +6,6 @@ import ( "github.com/HFO4/cloudreve/models" "github.com/HFO4/cloudreve/pkg/filesystem/local" "github.com/gin-gonic/gin" - testMock "github.com/stretchr/testify/mock" "io" ) @@ -23,17 +22,13 @@ type FileHeader interface { // Handler 存储策略适配器 type Handler interface { // 上传文件 - Put(ctx context.Context, file io.ReadCloser, dst string) error + Put(ctx context.Context, file io.ReadCloser, dst string, size uint64) error // 删除一个或多个文件 Delete(ctx context.Context, files []string) ([]string, error) } // FileSystem 管理文件的文件系统 type FileSystem struct { - /* - 测试用 - */ - testMock.Mock /* 文件系统所有者 */ @@ -43,13 +38,13 @@ type FileSystem struct { 钩子函数 */ // 上传文件前 - BeforeUpload func(ctx context.Context, fs *FileSystem) error + BeforeUpload []Hook // 上传文件后 - AfterUpload func(ctx context.Context, fs *FileSystem) error + AfterUpload []Hook // 文件保存成功,插入数据库验证失败后 - AfterValidateFailed func(ctx context.Context, fs *FileSystem) error + AfterValidateFailed []Hook // 用户取消上传后 - AfterUploadCanceled func(ctx context.Context, fs *FileSystem) error + AfterUploadCanceled []Hook /* 文件系统处理适配器 @@ -77,7 +72,6 @@ func NewFileSystem(user *model.User) (*FileSystem, error) { } // NewFileSystemFromContext 从gin.Context创建文件系统 -// TODO:test func NewFileSystemFromContext(c *gin.Context) (*FileSystem, error) { user, exist := c.Get("user") if !exist { diff --git a/pkg/filesystem/hooks.go b/pkg/filesystem/hooks.go index fc0a623..fd89db6 100644 --- a/pkg/filesystem/hooks.go +++ b/pkg/filesystem/hooks.go @@ -7,8 +7,38 @@ import ( "path" ) -// GenericBeforeUpload 通用上传前处理钩子,包含数据库操作 -func GenericBeforeUpload(ctx context.Context, fs *FileSystem) error { +// Hook 钩子函数 +type Hook func(ctx context.Context, fs *FileSystem) error + +// Use 注入钩子 +func (fs *FileSystem) Use(name string, hook Hook) { + switch name { + case "BeforeUpload": + fs.BeforeUpload = append(fs.BeforeUpload, hook) + case "AfterUpload": + fs.AfterUpload = append(fs.AfterUpload, hook) + case "AfterValidateFailed": + fs.AfterValidateFailed = append(fs.AfterValidateFailed, hook) + case "AfterUploadCanceled": + fs.AfterUploadCanceled = append(fs.AfterUploadCanceled, hook) + } +} + +// Trigger 触发钩子,遇到第一个错误时 +// 返回错误,后续钩子不会继续执行 +func (fs *FileSystem) Trigger(ctx context.Context, hooks []Hook) error { + for _, hook := range hooks { + err := hook(ctx, fs) + if err != nil { + util.Log().Warning("钩子执行失败:%s", err) + return err + } + } + return nil +} + +// HookValidateFile 一系列对文件检验的集合 +func HookValidateFile(ctx context.Context, fs *FileSystem) error { file := ctx.Value(FileHeaderCtx).(FileHeader) // 验证单文件尺寸 @@ -26,6 +56,13 @@ func GenericBeforeUpload(ctx context.Context, fs *FileSystem) error { return ErrFileExtensionNotAllowed } + return nil + +} + +// HookValidateCapacity 验证并扣除用户容量,包含数据库操作 +func HookValidateCapacity(ctx context.Context, fs *FileSystem) error { + file := ctx.Value(FileHeaderCtx).(FileHeader) // 验证并扣除容量 if !fs.ValidateCapacity(ctx, file.GetSize()) { return ErrInsufficientCapacity @@ -33,10 +70,8 @@ func GenericBeforeUpload(ctx context.Context, fs *FileSystem) error { return nil } -// GenericAfterUploadCanceled 通用上传取消处理钩子,包含数据库操作 -func GenericAfterUploadCanceled(ctx context.Context, fs *FileSystem) error { - file := ctx.Value(FileHeaderCtx).(FileHeader) - +// HookDeleteTempFile 删除已保存的临时文件 +func HookDeleteTempFile(ctx context.Context, fs *FileSystem) error { filePath := ctx.Value(SavePathCtx).(string) // 删除临时文件 if util.Exists(filePath) { @@ -46,6 +81,13 @@ func GenericAfterUploadCanceled(ctx context.Context, fs *FileSystem) error { } } + return nil +} + +// HookGiveBackCapacity 归还用户容量 +func HookGiveBackCapacity(ctx context.Context, fs *FileSystem) error { + file := ctx.Value(FileHeaderCtx).(FileHeader) + // 归还用户容量 if !fs.User.DeductionStorage(file.GetSize()) { return errors.New("无法继续降低用户已用存储") diff --git a/pkg/filesystem/hooks_test.go b/pkg/filesystem/hooks_test.go index eb1ffdb..b135410 100644 --- a/pkg/filesystem/hooks_test.go +++ b/pkg/filesystem/hooks_test.go @@ -34,20 +34,20 @@ func TestGenericBeforeUpload(t *testing.T) { }, } - asserts.Error(GenericBeforeUpload(ctx, &fs)) + asserts.Error(HookValidateFile(ctx, &fs)) file.Size = 1 file.Name = "1" ctx = context.WithValue(context.Background(), FileHeaderCtx, file) - asserts.Error(GenericBeforeUpload(ctx, &fs)) + asserts.Error(HookValidateFile(ctx, &fs)) file.Name = "1.txt" ctx = context.WithValue(context.Background(), FileHeaderCtx, file) - asserts.NoError(GenericBeforeUpload(ctx, &fs)) + asserts.NoError(HookValidateFile(ctx, &fs)) file.Name = "1.t/xt" ctx = context.WithValue(context.Background(), FileHeaderCtx, file) - asserts.Error(GenericBeforeUpload(ctx, &fs)) + asserts.Error(HookValidateFile(ctx, &fs)) } func TestGenericAfterUploadCanceled(t *testing.T) { @@ -67,7 +67,9 @@ func TestGenericAfterUploadCanceled(t *testing.T) { } // 成功 - err = GenericAfterUploadCanceled(ctx, &fs) + err = HookDeleteTempFile(ctx, &fs) + asserts.NoError(err) + err = HookGiveBackCapacity(ctx, &fs) asserts.NoError(err) asserts.Equal(uint64(0), fs.User.Storage) @@ -76,12 +78,12 @@ func TestGenericAfterUploadCanceled(t *testing.T) { f.Close() // 容量不能再降低 - err = GenericAfterUploadCanceled(ctx, &fs) + err = HookGiveBackCapacity(ctx, &fs) asserts.Error(err) //文件不存在 fs.User.Storage = 5 - err = GenericAfterUploadCanceled(ctx, &fs) + err = HookDeleteTempFile(ctx, &fs) asserts.NoError(err) } diff --git a/pkg/filesystem/local/handler.go b/pkg/filesystem/local/handler.go index 23f56b6..19d27cd 100644 --- a/pkg/filesystem/local/handler.go +++ b/pkg/filesystem/local/handler.go @@ -12,7 +12,7 @@ import ( type Handler struct{} // Put 将文件流保存到指定目录 -func (handler Handler) Put(ctx context.Context, file io.ReadCloser, dst string) error { +func (handler Handler) Put(ctx context.Context, file io.ReadCloser, dst string, size uint64) error { defer file.Close() // 如果目标目录不存在,创建 diff --git a/pkg/filesystem/local/handller_test.go b/pkg/filesystem/local/handller_test.go index eb539b3..1b9691d 100644 --- a/pkg/filesystem/local/handller_test.go +++ b/pkg/filesystem/local/handller_test.go @@ -34,7 +34,7 @@ func TestHandler_Put(t *testing.T) { } for _, testCase := range testCases { - err := handler.Put(ctx, testCase.file, testCase.dst) + err := handler.Put(ctx, testCase.file, testCase.dst, 15) if testCase.err { asserts.Error(err) } else { diff --git a/pkg/filesystem/qiniu/file.go b/pkg/filesystem/qiniu/file.go new file mode 100644 index 0000000..b97ad3c --- /dev/null +++ b/pkg/filesystem/qiniu/file.go @@ -0,0 +1,38 @@ +package qiniu + +import ( + "io" +) + +// FileStream 用户传来的文件 +type FileStream struct { + File io.ReadCloser + Size uint64 + VirtualPath string + Name string + MIMEType string +} + +func (file FileStream) Read(p []byte) (n int, err error) { + return file.File.Read(p) +} + +func (file FileStream) GetMIMEType() string { + return file.MIMEType +} + +func (file FileStream) GetSize() uint64 { + return file.Size +} + +func (file FileStream) Close() error { + return file.File.Close() +} + +func (file FileStream) GetFileName() string { + return file.Name +} + +func (file FileStream) GetVirtualPath() string { + return file.VirtualPath +} diff --git a/pkg/filesystem/qiniu/handller.go b/pkg/filesystem/qiniu/handller.go new file mode 100644 index 0000000..0936632 --- /dev/null +++ b/pkg/filesystem/qiniu/handller.go @@ -0,0 +1,63 @@ +package qiniu + +import ( + "context" + "fmt" + "github.com/HFO4/cloudreve/pkg/util" + "io" + "os" + + "github.com/qiniu/api.v7/v7/auth" + "github.com/qiniu/api.v7/v7/storage" +) + +// Handler 本地策略适配器 +type Handler struct{} + +// Put 将文件流保存到指定目录 +func (handler Handler) Put(ctx context.Context, file io.ReadCloser, dst string, size uint64) error { + // 凭证生成 + putPolicy := storage.PutPolicy{ + Scope: "cloudrevetest", + } + mac := auth.New("YNzTBBpDUq4EEiFV0-vyJCZCJ0LvUEI0_WvxtEXE", "Clm9d9M2CH7pZ8vm049ZlGZStQxrRQVRTjU_T5_0") + upToken := putPolicy.UploadToken(mac) + + cfg := storage.Config{} + // 空间对应的机房 + cfg.Zone = &storage.ZoneHuadong + formUploader := storage.NewFormUploader(&cfg) + ret := storage.PutRet{} + putExtra := storage.PutExtra{ + Params: map[string]string{}, + } + + defer file.Close() + + err := formUploader.Put(ctx, &ret, upToken, dst, file, int64(size), &putExtra) + if err != nil { + fmt.Println(err) + return err + } + fmt.Println(ret.Key, ret.Hash) + return nil +} + +// Delete 删除一个或多个文件, +// 返回已删除的文件,及遇到的最后一个错误 +func (handler Handler) Delete(ctx context.Context, files []string) ([]string, error) { + deleted := make([]string, 0, len(files)) + var retErr error + + for _, value := range files { + err := os.Remove(value) + if err == nil { + deleted = append(deleted, value) + } else { + util.Log().Warning("无法删除文件,%s", err) + retErr = err + } + } + + return deleted, retErr +} diff --git a/pkg/filesystem/upload.go b/pkg/filesystem/upload.go index 98cfb03..2887ea3 100644 --- a/pkg/filesystem/upload.go +++ b/pkg/filesystem/upload.go @@ -18,11 +18,9 @@ func (fs *FileSystem) Upload(ctx context.Context, file FileHeader) (err error) { ctx = context.WithValue(ctx, FileHeaderCtx, file) // 上传前的钩子 - if fs.BeforeUpload != nil { - err = fs.BeforeUpload(ctx, fs) - if err != nil { - return err - } + err = fs.Trigger(ctx, fs.BeforeUpload) + if err != nil { + return err } // 生成文件名和路径 @@ -32,27 +30,24 @@ func (fs *FileSystem) Upload(ctx context.Context, file FileHeader) (err error) { go fs.CancelUpload(ctx, savePath, file) // 保存文件 - err = fs.Handler.Put(ctx, file, savePath) + err = fs.Handler.Put(ctx, file, savePath, file.GetSize()) if err != nil { return err } // 上传完成后的钩子 - if fs.AfterUpload != nil { - ctx = context.WithValue(ctx, SavePathCtx, savePath) - err = fs.AfterUpload(ctx, fs) + ctx = context.WithValue(ctx, SavePathCtx, savePath) + err = fs.Trigger(ctx, fs.AfterUpload) - if err != nil { - // 上传完成后续处理失败 - if fs.AfterValidateFailed != nil { - followUpErr := fs.AfterValidateFailed(ctx, fs) - // 失败后再失败... - if followUpErr != nil { - util.Log().Warning("AfterValidateFailed 钩子执行失败,%s", followUpErr) - } - } - return err + if err != nil { + // 上传完成后续处理失败 + followUpErr := fs.Trigger(ctx, fs.AfterValidateFailed) + // 失败后再失败... + if followUpErr != nil { + util.Log().Warning("AfterValidateFailed 钩子执行失败,%s", followUpErr) } + + return err } util.Log().Info("新文件上传:%s , 大小:%d, 上传者:%s", file.GetFileName(), file.GetSize(), fs.User.Nick) @@ -88,7 +83,7 @@ func (fs *FileSystem) CancelUpload(ctx context.Context, path string, file FileHe return } ctx = context.WithValue(ctx, SavePathCtx, path) - err := fs.AfterUploadCanceled(ctx, fs) + err := fs.Trigger(ctx, fs.AfterUploadCanceled) if err != nil { util.Log().Warning("执行 AfterUploadCanceled 钩子出错,%s", err) } diff --git a/pkg/filesystem/upload_test.go b/pkg/filesystem/upload_test.go index 8d47758..e23256a 100644 --- a/pkg/filesystem/upload_test.go +++ b/pkg/filesystem/upload_test.go @@ -19,7 +19,7 @@ type FileHeaderMock struct { testMock.Mock } -func (m FileHeaderMock) Put(ctx context.Context, file io.ReadCloser, dst string) error { +func (m FileHeaderMock) Put(ctx context.Context, file io.ReadCloser, dst string, size uint64) error { args := m.Called(ctx, file, dst) return args.Error(0) } @@ -61,9 +61,9 @@ func TestFileSystem_Upload(t *testing.T) { asserts.NoError(err) // BeforeUpload 返回错误 - fs.BeforeUpload = func(ctx context.Context, fs *FileSystem) error { + fs.Use("BeforeUpload", func(ctx context.Context, fs *FileSystem) error { return errors.New("error") - } + }) err = fs.Upload(ctx, file) asserts.Error(err) fs.BeforeUpload = nil diff --git a/routers/controllers/file.go b/routers/controllers/file.go index 6455f4c..df10489 100644 --- a/routers/controllers/file.go +++ b/routers/controllers/file.go @@ -55,10 +55,13 @@ func FileUploadStream(c *gin.Context) { } // 给文件系统分配钩子 - fs.BeforeUpload = filesystem.GenericBeforeUpload - fs.AfterUploadCanceled = filesystem.GenericAfterUploadCanceled - fs.AfterUpload = filesystem.GenericAfterUpload - fs.AfterValidateFailed = filesystem.GenericAfterUploadCanceled + fs.Use("BeforeUpload", filesystem.HookValidateFile) + fs.Use("BeforeUpload", filesystem.HookValidateCapacity) + fs.Use("AfterUploadCanceled", filesystem.HookDeleteTempFile) + fs.Use("AfterUploadCanceled", filesystem.HookGiveBackCapacity) + fs.Use("AfterUpload", filesystem.GenericAfterUpload) + fs.Use("AfterValidateFailed", filesystem.HookDeleteTempFile) + fs.Use("AfterValidateFailed", filesystem.HookGiveBackCapacity) // 执行上传 uploadCtx := context.WithValue(ctx, filesystem.GinCtx, c)