package storage import ( "context" "errors" "kra/internal/biz/system" "path" "strings" ) func deletePrefixViaList(ctx context.Context, prefix string, list func(context.Context, string, string, int) ([]*system.StoredFile, string, bool, error), remove func(context.Context, string) error) error { prefix, err := normalizeDeletePrefix(prefix) if err != nil { return err } cursor := "" for { items, next, more, err := list(ctx, prefix+"/", cursor, 1000) if err != nil { return err } for _, item := range items { if err := remove(ctx, item.Path); err != nil { return err } } if !more { return nil } cursor, err = advanceDeletePrefixCursor(cursor, next, more) if err != nil { return err } } } // normalizeDeletePrefix rejects requests that could accidentally target the // storage root and returns the canonical key form used by all backends. func normalizeDeletePrefix(prefix string) (string, error) { prefix = strings.TrimSpace(strings.ReplaceAll(prefix, "\\", "/")) prefix = strings.Trim(prefix, "/") if prefix == "" { return "", errors.New("storage delete prefix is required") } for _, part := range strings.Split(prefix, "/") { if part == ".." { return "", errors.New("invalid storage delete prefix") } } clean := path.Clean(prefix) if clean == "." || clean == ".." || strings.HasPrefix(clean, "../") { return "", errors.New("invalid storage delete prefix") } return clean, nil } // advanceDeletePrefixCursor prevents a backend that reports a truncated page // without a new cursor from making DeletePrefix loop forever. func advanceDeletePrefixCursor(current, next string, more bool) (string, error) { if !more { return "", nil } current = strings.TrimSpace(current) next = strings.TrimSpace(next) if next == "" || next == current { return "", errors.New("storage delete prefix pagination made no progress") } return next, nil } func boundedPrefix(key, prefix string) string { if strings.HasSuffix(strings.ReplaceAll(prefix, "\\", "/"), "/") && !strings.HasSuffix(key, "/") { return key + "/" } return key }