diff --git a/models/file.go b/models/file.go index 21f38da..7487325 100644 --- a/models/file.go +++ b/models/file.go @@ -161,3 +161,8 @@ func (file *File) Rename(new string) error { func (file *File) UpdatePicInfo(value string) error { return DB.Model(&file).Update("pic_info", value).Error } + +// UpdatePicInfo 更新文件的图像信息 +func (file *File) UpdateSize(value uint64) error { + return DB.Model(&file).Update("size", value).Error +} diff --git a/pkg/filesystem/hooks.go b/pkg/filesystem/hooks.go index a509efd..b1d3365 100644 --- a/pkg/filesystem/hooks.go +++ b/pkg/filesystem/hooks.go @@ -3,8 +3,12 @@ package filesystem import ( "context" "errors" + model "github.com/HFO4/cloudreve/models" + "github.com/HFO4/cloudreve/pkg/conf" "github.com/HFO4/cloudreve/pkg/filesystem/fsctx" "github.com/HFO4/cloudreve/pkg/util" + "io/ioutil" + "strings" ) // Hook 钩子函数 @@ -71,6 +75,17 @@ func HookValidateFile(ctx context.Context, fs *FileSystem) error { } +// HookResetPolicy 重设存储策略为已有文件 +func HookResetPolicy(ctx context.Context, fs *FileSystem) error { + originFile, ok := ctx.Value(fsctx.FileModelCtx).(model.File) + if !ok { + return ErrObjectNotExist + } + + fs.Policy = originFile.GetPolicy() + return fs.dispatchHandler() +} + // HookValidateCapacity 验证并扣除用户容量,包含数据库操作 func HookValidateCapacity(ctx context.Context, fs *FileSystem) error { file := ctx.Value(fsctx.FileHeaderCtx).(FileHeader) @@ -81,10 +96,27 @@ func HookValidateCapacity(ctx context.Context, fs *FileSystem) error { return nil } +// HookChangeCapacity 根据原有文件和新文件的大小更新用户容量 +func HookChangeCapacity(ctx context.Context, fs *FileSystem) error { + newFile := ctx.Value(fsctx.FileHeaderCtx).(FileHeader) + originFile := ctx.Value(fsctx.FileModelCtx).(model.File) + + if newFile.GetSize() > originFile.Size { + if !fs.ValidateCapacity(ctx, newFile.GetSize()-originFile.Size) { + return ErrInsufficientCapacity + } + return nil + } + + fs.User.DeductionStorage(originFile.Size - newFile.GetSize()) + return nil +} + // HookDeleteTempFile 删除已保存的临时文件 func HookDeleteTempFile(ctx context.Context, fs *FileSystem) error { filePath := ctx.Value(fsctx.SavePathCtx).(string) // 删除临时文件 + // TODO 其他策略。Exists? if util.Exists(filePath) { _, err := fs.Handler.Delete(ctx, []string{filePath}) if err != nil { @@ -95,6 +127,22 @@ func HookDeleteTempFile(ctx context.Context, fs *FileSystem) error { return nil } +// HookCleanFileContent 清空文件内容 +func HookCleanFileContent(ctx context.Context, fs *FileSystem) error { + filePath := ctx.Value(fsctx.SavePathCtx).(string) + // 清空内容 + return fs.Handler.Put(ctx, ioutil.NopCloser(strings.NewReader("")), filePath, 0) +} + +// HookClearFileSize 将原始文件的尺寸设为0 +func HookClearFileSize(ctx context.Context, fs *FileSystem) error { + originFile, ok := ctx.Value(fsctx.FileModelCtx).(model.File) + if !ok { + return ErrObjectNotExist + } + return originFile.UpdateSize(0) +} + // HookGiveBackCapacity 归还用户容量 func HookGiveBackCapacity(ctx context.Context, fs *FileSystem) error { file := ctx.Value(fsctx.FileHeaderCtx).(FileHeader) @@ -106,6 +154,34 @@ func HookGiveBackCapacity(ctx context.Context, fs *FileSystem) error { return nil } +// GenericAfterUpdate 文件内容更新后 +func GenericAfterUpdate(ctx context.Context, fs *FileSystem) error { + // 更新文件尺寸 + originFile, ok := ctx.Value(fsctx.FileModelCtx).(model.File) + if !ok { + return ErrObjectNotExist + } + newFile, ok := ctx.Value(fsctx.FileHeaderCtx).(FileHeader) + if !ok { + return ErrObjectNotExist + } + err := originFile.UpdateSize(newFile.GetSize()) + if err != nil { + return err + } + + // 尝试清空原有缩略图 + go func() { + if originFile.PicInfo != "" { + _, _ = fs.Handler.Delete(ctx, []string{originFile.SourceName + conf.ThumbConfig.FileSuffix}) + fs.GenerateThumbnail(ctx, &originFile) + } + + }() + + return nil +} + // GenericAfterUpload 文件上传完成后,包含数据库操作 func GenericAfterUpload(ctx context.Context, fs *FileSystem) error { // 文件存放的虚拟路径 diff --git a/pkg/filesystem/upload.go b/pkg/filesystem/upload.go index c066c82..39ac3c2 100644 --- a/pkg/filesystem/upload.go +++ b/pkg/filesystem/upload.go @@ -2,6 +2,7 @@ package filesystem import ( "context" + model "github.com/HFO4/cloudreve/models" "github.com/HFO4/cloudreve/pkg/filesystem/fsctx" "github.com/HFO4/cloudreve/pkg/util" "github.com/gin-gonic/gin" @@ -23,8 +24,14 @@ func (fs *FileSystem) Upload(ctx context.Context, file FileHeader) (err error) { return err } - // 生成文件名和路径 - savePath := fs.GenerateSavePath(ctx, file) + // 生成文件名和路径, 如果是更新操作就从原始文件获取 + var savePath string + originFile, ok := ctx.Value(fsctx.FileModelCtx).(model.File) + if ok { + savePath = originFile.SourceName + } else { + savePath = fs.GenerateSavePath(ctx, file) + } // 处理客户端未完成上传时,关闭连接 go fs.CancelUpload(ctx, savePath, file) @@ -50,7 +57,7 @@ func (fs *FileSystem) Upload(ctx context.Context, file FileHeader) (err error) { return err } - util.Log().Info("新文件上传:%s , 大小:%d, 上传者:%s", file.GetFileName(), file.GetSize(), fs.User.Nick) + util.Log().Info("新文件PUT:%s , 大小:%d, 上传者:%s", file.GetFileName(), file.GetSize(), fs.User.Nick) return nil } diff --git a/routers/controllers/file.go b/routers/controllers/file.go index 79ae686..32c8957 100644 --- a/routers/controllers/file.go +++ b/routers/controllers/file.go @@ -143,7 +143,7 @@ func Preview(c *gin.Context) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - var service explorer.FileDownloadCreateService + var service explorer.SingleFileService if err := c.ShouldBindUri(&service); err == nil { res := service.PreviewContent(ctx, c) // 是否需要重定向 @@ -164,7 +164,7 @@ func CreateDownloadSession(c *gin.Context) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - var service explorer.FileDownloadCreateService + var service explorer.SingleFileService if err := c.ShouldBindUri(&service); err == nil { res := service.CreateDownloadSession(ctx, c) c.JSON(200, res) @@ -190,6 +190,21 @@ func Download(c *gin.Context) { } } +// PutContent 更新文件内容 +func PutContent(c *gin.Context) { + // 创建上下文 + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var service explorer.SingleFileService + if err := c.ShouldBindUri(&service); err == nil { + res := service.PutContent(ctx, c) + c.JSON(200, res) + } else { + c.JSON(200, ErrorResponse(err)) + } +} + // FileUploadStream 本地策略流式上传 func FileUploadStream(c *gin.Context) { // 创建上下文 @@ -251,6 +266,5 @@ func FileUploadStream(c *gin.Context) { c.JSON(200, serializer.Response{ Code: 0, - Msg: "Pong", }) } diff --git a/routers/router.go b/routers/router.go index ccea9cf..052fdcb 100644 --- a/routers/router.go +++ b/routers/router.go @@ -104,6 +104,8 @@ func InitRouter() *gin.Engine { { // 文件上传 file.POST("upload", controllers.FileUploadStream) + // 更新文件 + file.PUT("update/*path", controllers.PutContent) // 创建文件下载会话 file.PUT("download/*path", controllers.CreateDownloadSession) // 预览文件 diff --git a/service/explorer/file.go b/service/explorer/file.go index f68654d..6d1448d 100644 --- a/service/explorer/file.go +++ b/service/explorer/file.go @@ -6,14 +6,17 @@ import ( "github.com/HFO4/cloudreve/pkg/cache" "github.com/HFO4/cloudreve/pkg/filesystem" "github.com/HFO4/cloudreve/pkg/filesystem/fsctx" + "github.com/HFO4/cloudreve/pkg/filesystem/local" "github.com/HFO4/cloudreve/pkg/serializer" "github.com/gin-gonic/gin" "net/http" + "path" + "strconv" "time" ) -// FileDownloadCreateService 文件下载会话创建服务,path为文件完整路径 -type FileDownloadCreateService struct { +// SingleFileService 对单文件进行操作的五福,path为文件完整路径 +type SingleFileService struct { Path string `uri:"path" binding:"required,min=1,max=65535"` } @@ -93,7 +96,7 @@ func (service *FileAnonymousGetService) Download(ctx context.Context, c *gin.Con } // CreateDownloadSession 创建下载会话,获取下载URL -func (service *FileDownloadCreateService) CreateDownloadSession(ctx context.Context, c *gin.Context) serializer.Response { +func (service *SingleFileService) CreateDownloadSession(ctx context.Context, c *gin.Context) serializer.Response { // 创建文件系统 fs, err := filesystem.NewFileSystemFromContext(c) if err != nil { @@ -152,7 +155,7 @@ func (service *DownloadService) Download(ctx context.Context, c *gin.Context) se } // PreviewContent 预览文件,需要登录会话 -func (service *FileDownloadCreateService) PreviewContent(ctx context.Context, c *gin.Context) serializer.Response { +func (service *SingleFileService) PreviewContent(ctx context.Context, c *gin.Context) serializer.Response { // 创建文件系统 fs, err := filesystem.NewFileSystemFromContext(c) if err != nil { @@ -172,3 +175,61 @@ func (service *FileDownloadCreateService) PreviewContent(ctx context.Context, c Code: 0, } } + +// PutContent 更新文件内容 +func (service *SingleFileService) PutContent(ctx context.Context, c *gin.Context) serializer.Response { + // 创建上下文 + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // 取得文件大小 + fileSize, err := strconv.ParseUint(c.Request.Header.Get("Content-Length"), 10, 64) + if err != nil { + + return serializer.ParamErr("无法解析文件尺寸", err) + } + + fileData := local.FileStream{ + MIMEType: c.Request.Header.Get("Content-Type"), + File: c.Request.Body, + Size: fileSize, + Name: path.Base(service.Path), + VirtualPath: path.Dir(service.Path), + } + + // 创建文件系统 + fs, err := filesystem.NewFileSystemFromContext(c) + if err != nil { + return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err) + } + + // 取得现有文件 + exist, originFile := fs.IsFileExist(service.Path) + if !exist { + return serializer.Err(404, "文件不存在", nil) + } + + // 给文件系统分配钩子 + fs.Use("BeforeUpload", filesystem.HookValidateFile) + fs.Use("BeforeUpload", filesystem.HookResetPolicy) + fs.Use("BeforeUpload", filesystem.HookChangeCapacity) + fs.Use("AfterUploadCanceled", filesystem.HookCleanFileContent) + fs.Use("AfterUploadCanceled", filesystem.HookClearFileSize) + fs.Use("AfterUploadCanceled", filesystem.HookGiveBackCapacity) + fs.Use("AfterUpload", filesystem.GenericAfterUpdate) + fs.Use("AfterValidateFailed", filesystem.HookCleanFileContent) + fs.Use("AfterValidateFailed", filesystem.HookClearFileSize) + fs.Use("AfterValidateFailed", filesystem.HookGiveBackCapacity) + + // 执行上传 + uploadCtx := context.WithValue(ctx, fsctx.GinCtx, c) + uploadCtx = context.WithValue(uploadCtx, fsctx.FileModelCtx, *originFile) + err = fs.Upload(uploadCtx, fileData) + if err != nil { + return serializer.Err(serializer.CodeUploadFailed, err.Error(), err) + } + + return serializer.Response{ + Code: 0, + } +}