diff --git a/assets b/assets index e2d4f13..4f114cf 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit e2d4f13a54dfd424cfbc129664772e104ccf97fc +Subproject commit 4f114cf386770dfdc568b60465a29feb4db65371 diff --git a/models/folder.go b/models/folder.go index 70c5509..8c8d6c0 100644 --- a/models/folder.go +++ b/models/folder.go @@ -44,6 +44,26 @@ func (folder *Folder) GetChild(name string) (*Folder, error) { return &resFolder, err } +// TraceRoot 向上递归查找父目录 +func (folder *Folder) TraceRoot() error { + if folder.ParentID == nil { + return nil + } + + var parentFolder Folder + err := DB. + Where("id = ? AND owner_id = ?", folder.ParentID, folder.OwnerID). + First(&parentFolder).Error + + if err == nil { + err := parentFolder.TraceRoot() + folder.Position = path.Join(parentFolder.Position, parentFolder.Name) + return err + } + + return err +} + // GetChildFolder 查找子目录 func (folder *Folder) GetChildFolder() ([]Folder, error) { var folders []Folder diff --git a/models/folder_test.go b/models/folder_test.go index 0ccd821..dd286f2 100644 --- a/models/folder_test.go +++ b/models/folder_test.go @@ -530,3 +530,37 @@ func TestFolder_FileInfoInterface(t *testing.T) { asserts.True(folder.IsDir()) asserts.Equal("/test", folder.GetPosition()) } + +func TestTraceRoot(t *testing.T) { + asserts := assert.New(t) + var parentId uint + parentId = 5 + folder := Folder{ + ParentID: &parentId, + OwnerID: 1, + Name: "test_name", + } + + // 成功 + { + mock.ExpectQuery("SELECT(.+)").WithArgs(5, 1). + WillReturnRows(sqlmock.NewRows([]string{"id", "name", "parent_id"}).AddRow(5, "parent", 1)) + mock.ExpectQuery("SELECT(.+)").WithArgs(1, 0). + WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(5, "/")) + asserts.NoError(folder.TraceRoot()) + asserts.Equal("/parent", folder.Position) + asserts.NoError(mock.ExpectationsWereMet()) + } + + // 出现错误 + // 成功 + { + mock.ExpectQuery("SELECT(.+)").WithArgs(5, 1). + WillReturnRows(sqlmock.NewRows([]string{"id", "name", "parent_id"}).AddRow(5, "parent", 1)) + mock.ExpectQuery("SELECT(.+)").WithArgs(1, 0). + WillReturnError(errors.New("error")) + asserts.Error(folder.TraceRoot()) + asserts.Equal("parent", folder.Position) + asserts.NoError(mock.ExpectationsWereMet()) + } +} diff --git a/models/migration.go b/models/migration.go index 4683d81..4522c8e 100644 --- a/models/migration.go +++ b/models/migration.go @@ -104,6 +104,7 @@ func addDefaultSettings() { {Name: "share_download_session_timeout", Value: `2073600`, Type: "timeout"}, {Name: "onedrive_callback_check", Value: `20`, Type: "timeout"}, {Name: "aria2_call_timeout", Value: `5`, Type: "timeout"}, + {Name: "folder_props_timeout", Value: `300`, Type: "timeout"}, {Name: "onedrive_chunk_retries", Value: `1`, Type: "retry"}, {Name: "onedrive_source_timeout", Value: `1800`, Type: "timeout"}, {Name: "reset_after_upload_failed", Value: `0`, Type: "upload"}, diff --git a/pkg/serializer/explorer.go b/pkg/serializer/explorer.go new file mode 100644 index 0000000..c3f697c --- /dev/null +++ b/pkg/serializer/explorer.go @@ -0,0 +1,23 @@ +package serializer + +import ( + "encoding/gob" + "time" +) + +func init() { + gob.Register(ObjectProps{}) +} + +// ObjectProps 文件、目录对象的详细属性信息 +type ObjectProps struct { + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Policy string `json:"policy"` + Size uint64 `json:"size"` + ChildFolderNum int `json:"child_folder_num"` + ChildFileNum int `json:"child_file_num"` + Path string `json:"path"` + + QueryDate time.Time +} diff --git a/routers/controllers/objects.go b/routers/controllers/objects.go index 8f65d73..a6095b5 100644 --- a/routers/controllers/objects.go +++ b/routers/controllers/objects.go @@ -66,3 +66,19 @@ func Rename(c *gin.Context) { c.JSON(200, ErrorResponse(err)) } } + +// Rename 重命名文件或目录 +func GetProperty(c *gin.Context) { + // 创建上下文 + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var service explorer.ItemPropertyService + service.ID = c.Param("id") + if err := c.ShouldBindQuery(&service); err == nil { + res := service.GetProperty(ctx, c) + c.JSON(200, res) + } else { + c.JSON(200, ErrorResponse(err)) + } +} diff --git a/routers/router.go b/routers/router.go index 71659ee..8cf5a9d 100644 --- a/routers/router.go +++ b/routers/router.go @@ -510,6 +510,8 @@ func InitMasterRouter() *gin.Engine { object.POST("copy", controllers.Copy) // 重命名对象 object.POST("rename", controllers.Rename) + // 获取对象属性 + object.GET("property/:id", controllers.GetProperty) } // 分享 diff --git a/service/explorer/objects.go b/service/explorer/objects.go index e090a2c..e15589f 100644 --- a/service/explorer/objects.go +++ b/service/explorer/objects.go @@ -60,6 +60,13 @@ type ItemDecompressService struct { Dst string `json:"dst" binding:"required,min=1,max=65535"` } +// ItemPropertyService 获取对象属性服务 +type ItemPropertyService struct { + ID string `binding:"required"` + TraceRoot bool `form:"trace_root"` + IsFolder bool `form:"is_folder"` +} + // Raw 批量解码HashID,获取原始ID func (service *ItemIDService) Raw() *ItemService { if service.Source != nil { @@ -353,3 +360,100 @@ func (service *ItemRenameService) Rename(ctx context.Context, c *gin.Context) se Code: 0, } } + +// GetProperty 获取对象的属性 +func (service *ItemPropertyService) GetProperty(ctx context.Context, c *gin.Context) serializer.Response { + userCtx, _ := c.Get("user") + user := userCtx.(*model.User) + + var props serializer.ObjectProps + props.QueryDate = time.Now() + + // 如果是文件对象 + if !service.IsFolder { + res, err := hashid.DecodeHashID(service.ID, hashid.FileID) + if err != nil { + return serializer.Err(serializer.CodeNotFound, "对象不存在", err) + } + + file, err := model.GetFilesByIDs([]uint{res}, user.ID) + if err != nil { + return serializer.DBErr("找不到文件", err) + } + + props.CreatedAt = file[0].CreatedAt + props.UpdatedAt = file[0].UpdatedAt + props.Policy = file[0].GetPolicy().Name + props.Size = file[0].Size + + // 查找父目录 + if service.TraceRoot { + parent, err := model.GetFoldersByIDs([]uint{file[0].FolderID}, user.ID) + if err != nil { + return serializer.DBErr("找不到父目录", err) + } + + if err := parent[0].TraceRoot(); err != nil { + return serializer.DBErr("无法溯源父目录", err) + } + + props.Path = path.Join(parent[0].Position, parent[0].Name) + } + } else { + res, err := hashid.DecodeHashID(service.ID, hashid.FolderID) + if err != nil { + return serializer.Err(serializer.CodeNotFound, "对象不存在", err) + } + + // 如果对象是目录, 先尝试返回缓存结果 + if cacheRes, ok := cache.Get(fmt.Sprintf("folder_props_%d", res)); ok { + return serializer.Response{Data: cacheRes.(serializer.ObjectProps)} + } + + folder, err := model.GetFoldersByIDs([]uint{res}, user.ID) + if err != nil { + return serializer.DBErr("找不到目录", err) + } + + props.CreatedAt = folder[0].CreatedAt + props.UpdatedAt = folder[0].UpdatedAt + + // 统计子目录 + childFolders, err := model.GetRecursiveChildFolder([]uint{folder[0].ID}, + user.ID, true) + if err != nil { + return serializer.DBErr("无法列取子目录", err) + } + props.ChildFolderNum = len(childFolders) - 1 + + // 统计子文件 + files, err := model.GetChildFilesOfFolders(&childFolders) + if err != nil { + return serializer.DBErr("无法列取子文件", err) + } + + // 统计子文件个数和大小 + props.ChildFileNum = len(files) + for i := 0; i < len(files); i++ { + props.Size += files[i].Size + } + + // 查找父目录 + if service.TraceRoot { + if err := folder[0].TraceRoot(); err != nil { + return serializer.DBErr("无法溯源父目录", err) + } + + props.Path = folder[0].Position + } + + // 如果列取对象是目录,则缓存结果 + cache.Set(fmt.Sprintf("folder_props_%d", res), props, + model.GetIntSetting("folder_props_timeout", 300)) + } + + return serializer.Response{ + Code: 0, + Data: props, + } +}