package manager import ( "context" "crypto/sha1" "encoding/json" "fmt" "strconv" "strings" "github.com/cloudreve/Cloudreve/v4/application/constants" "github.com/cloudreve/Cloudreve/v4/application/dependency" "github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs" "github.com/cloudreve/Cloudreve/v4/pkg/hashid" "github.com/cloudreve/Cloudreve/v4/pkg/serializer" "github.com/go-playground/validator/v10" "github.com/samber/lo" ) type ( metadataValidator func(ctx context.Context, m *manager, patch *fs.MetadataPatch) error ) const ( wildcardMetadataKey = "*" customizeMetadataSuffix = "customize" tagMetadataSuffix = "tag" customPropsMetadataSuffix = "props" iconColorMetadataKey = customizeMetadataSuffix + ":icon_color" emojiIconMetadataKey = customizeMetadataSuffix + ":emoji" shareOwnerMetadataKey = dbfs.MetadataSysPrefix + "shared_owner" shareRedirectMetadataKey = dbfs.MetadataSysPrefix + "shared_redirect" ) var ( validate = validator.New() lastEmojiHash = "" emojiPresets = map[string]struct{}{} // validateColor validates a color value validateColor = func(optional bool) metadataValidator { return func(ctx context.Context, m *manager, patch *fs.MetadataPatch) error { patch.UpdateModifiedAt = true if patch.Remove { return nil } tag := "omitempty,iscolor" if !optional { tag = "required,iscolor" } res := validate.Var(patch.Value, tag) if res != nil { return fmt.Errorf("invalid color: %w", res) } return nil } } validators = map[string]map[string]metadataValidator{ "sys": { wildcardMetadataKey: func(ctx context.Context, m *manager, patch *fs.MetadataPatch) error { if patch.Remove { return fmt.Errorf("cannot remove system metadata") } patch.UpdateModifiedAt = true dep := dependency.FromContext(ctx) // Validate share owner is valid hashid if patch.Key == shareOwnerMetadataKey { hasher := dep.HashIDEncoder() _, err := hasher.Decode(patch.Value, hashid.UserID) if err != nil { return fmt.Errorf("invalid share owner: %w", err) } return nil } // Validate share redirect uri is valid share uri if patch.Key == shareRedirectMetadataKey { uri, err := fs.NewUriFromString(patch.Value) if err != nil || uri.FileSystem() != constants.FileSystemShare { return fmt.Errorf("invalid redirect uri: %w", err) } return nil } return fmt.Errorf("unsupported system metadata key: %s", patch.Key) }, }, "dav": {}, customizeMetadataSuffix: { iconColorMetadataKey: validateColor(false), emojiIconMetadataKey: func(ctx context.Context, m *manager, patch *fs.MetadataPatch) error { patch.UpdateModifiedAt = true if patch.Remove { return nil } // Validate if patched emoji is within preset list. emojis := m.settings.EmojiPresets(ctx) current := fmt.Sprintf("%x", (sha1.Sum([]byte(emojis)))) if current != lastEmojiHash { presets := make(map[string][]string) if err := json.Unmarshal([]byte(emojis), &presets); err != nil { return fmt.Errorf("failed to read emoji setting: %w", err) } emojiPresets = make(map[string]struct{}) for _, v := range presets { for _, emoji := range v { emojiPresets[emoji] = struct{}{} } } } if _, ok := emojiPresets[patch.Value]; !ok { return fmt.Errorf("unsupported emoji") } return nil }, }, tagMetadataSuffix: { wildcardMetadataKey: func(ctx context.Context, m *manager, patch *fs.MetadataPatch) error { patch.UpdateModifiedAt = true if err := validateColor(true)(ctx, m, patch); err != nil { return err } if patch.Key == tagMetadataSuffix+":" { return fmt.Errorf("invalid metadata key") } return nil }, }, customPropsMetadataSuffix: { wildcardMetadataKey: func(ctx context.Context, m *manager, patch *fs.MetadataPatch) error { patch.UpdateModifiedAt = true if patch.Remove { return nil } customProps := m.settings.CustomProps(ctx) propId := strings.TrimPrefix(patch.Key, customPropsMetadataSuffix+":") for _, prop := range customProps { if prop.ID == propId { switch prop.Type { case types.CustomPropsTypeText: if prop.Min > 0 && prop.Min > len(patch.Value) { return fmt.Errorf("value is too short") } if prop.Max > 0 && prop.Max < len(patch.Value) { return fmt.Errorf("value is too long") } return nil case types.CustomPropsTypeRating: if patch.Value == "" { return nil } // validate the value is a number rating, err := strconv.Atoi(patch.Value) if err != nil { return fmt.Errorf("value is not a number") } if prop.Max < rating { return fmt.Errorf("value is too large") } return nil case types.CustomPropsTypeNumber: if patch.Value == "" { return nil } value, err := strconv.Atoi(patch.Value) if err != nil { return fmt.Errorf("value is not a number") } if prop.Min > value { return fmt.Errorf("value is too small") } if prop.Max > 0 && prop.Max < value { return fmt.Errorf("value is too large") } return nil case types.CustomPropsTypeBoolean: if patch.Value == "" { return nil } if patch.Value != "true" && patch.Value != "false" { return fmt.Errorf("value is not a boolean") } return nil case types.CustomPropsTypeSelect: if patch.Value == "" { return nil } for _, option := range prop.Options { if option == patch.Value { return nil } } return fmt.Errorf("invalid option") case types.CustomPropsTypeMultiSelect: if patch.Value == "" { return nil } var values []string if err := json.Unmarshal([]byte(patch.Value), &values); err != nil { return fmt.Errorf("invalid multi select value: %w", err) } // make sure all values are in the options for _, value := range values { if !lo.Contains(prop.Options, value) { return fmt.Errorf("invalid option") } } return nil case types.CustomPropsTypeLink: if patch.Value == "" { return nil } if prop.Min > 0 && len(patch.Value) < prop.Min { return fmt.Errorf("value is too small") } if prop.Max > 0 && len(patch.Value) > prop.Max { return fmt.Errorf("value is too large") } return nil default: return nil } } } return fmt.Errorf("unkown custom props") }, }, } ) func (m *manager) PatchMedata(ctx context.Context, path []*fs.URI, data ...fs.MetadataPatch) error { data, err := m.validateMetadata(ctx, data...) if err != nil { return err } return m.fs.PatchMetadata(ctx, path, data...) } func (m *manager) validateMetadata(ctx context.Context, data ...fs.MetadataPatch) ([]fs.MetadataPatch, error) { validated := make([]fs.MetadataPatch, 0, len(data)) for _, patch := range data { category := strings.Split(patch.Key, ":") if len(category) < 2 { return validated, serializer.NewError(serializer.CodeParamErr, "Invalid metadata key", nil) } categoryValidators, ok := validators[category[0]] if !ok { return validated, serializer.NewError(serializer.CodeParamErr, "Invalid metadata key", fmt.Errorf("unknown category: %s", category[0])) } // Explicit validators if v, ok := categoryValidators[patch.Key]; ok { if err := v(ctx, m, &patch); err != nil { return validated, serializer.NewError(serializer.CodeParamErr, "Invalid metadata patch", err) } } // Wildcard validators if v, ok := categoryValidators[wildcardMetadataKey]; ok { if err := v(ctx, m, &patch); err != nil { return validated, serializer.NewError(serializer.CodeParamErr, "Invalid metadata patch", err) } } validated = append(validated, patch) } return validated, nil }