Files
stash/pkg/tag/update.go
WithoutPants 9b5c0b0e48 Match tag names/aliases exactly when testing uniqueness (#6809)
* Add tagStore.FindByAlias method
* Change tag.ByName and ByAlias to use exact queries instead of fuzzy matching
2026-04-08 13:11:12 +10:00

223 lines
5.2 KiB
Go

package tag
import (
"context"
"fmt"
"github.com/stashapp/stash/pkg/models"
)
type NameExistsError struct {
Name string
}
func (e *NameExistsError) Error() string {
return fmt.Sprintf("tag with name '%s' already exists", e.Name)
}
type NameUsedByAliasError struct {
Name string
OtherTag string
}
func (e *NameUsedByAliasError) Error() string {
return fmt.Sprintf("name '%s' is used as alias for '%s'", e.Name, e.OtherTag)
}
type InvalidTagHierarchyError struct {
Direction string
CurrentRelation string
InvalidTag string
ApplyingTag string
TagPath string
}
func (e *InvalidTagHierarchyError) Error() string {
if e.ApplyingTag == "" {
return fmt.Sprintf("cannot apply tag \"%s\" as a %s of tag as it is already %s", e.InvalidTag, e.Direction, e.CurrentRelation)
}
return fmt.Sprintf("cannot apply tag \"%s\" as a %s of \"%s\" as it is already %s (%s)", e.InvalidTag, e.Direction, e.ApplyingTag, e.CurrentRelation, e.TagPath)
}
// EnsureTagNameUnique returns an error if the tag name provided
// is used as a name or alias of another existing tag.
func EnsureTagNameUnique(ctx context.Context, id int, name string, qb models.TagNameFinder) error {
// ensure name is unique
sameNameTag, err := ByName(ctx, qb, name)
if err != nil {
return err
}
if sameNameTag != nil && id != sameNameTag.ID {
return &NameExistsError{
Name: name,
}
}
// query by alias
sameNameTag, err = ByAlias(ctx, qb, name)
if err != nil {
return err
}
if sameNameTag != nil && id != sameNameTag.ID {
return &NameUsedByAliasError{
Name: name,
OtherTag: sameNameTag.Name,
}
}
return nil
}
func EnsureAliasesUnique(ctx context.Context, id int, aliases []string, qb models.TagNameFinder) error {
for _, a := range aliases {
if err := EnsureTagNameUnique(ctx, id, a, qb); err != nil {
return err
}
}
return nil
}
type RelationshipFinder interface {
FindAllAncestors(ctx context.Context, tagID int, excludeIDs []int) ([]*models.TagPath, error)
FindAllDescendants(ctx context.Context, tagID int, excludeIDs []int) ([]*models.TagPath, error)
models.TagRelationLoader
}
func ValidateHierarchyNew(ctx context.Context, parentIDs, childIDs []int, qb RelationshipFinder) error {
allAncestors := make(map[int]*models.TagPath)
allDescendants := make(map[int]*models.TagPath)
for _, parentID := range parentIDs {
parentsAncestors, err := qb.FindAllAncestors(ctx, parentID, nil)
if err != nil {
return err
}
for _, ancestorTag := range parentsAncestors {
allAncestors[ancestorTag.ID] = ancestorTag
}
}
for _, childID := range childIDs {
childsDescendants, err := qb.FindAllDescendants(ctx, childID, nil)
if err != nil {
return err
}
for _, descendentTag := range childsDescendants {
allDescendants[descendentTag.ID] = descendentTag
}
}
// Validate that the tag is not a parent of any of its ancestors
validateParent := func(testID int) error {
if parentTag, exists := allDescendants[testID]; exists {
return &InvalidTagHierarchyError{
Direction: "parent",
CurrentRelation: "a descendant",
InvalidTag: parentTag.Name,
TagPath: parentTag.Path,
}
}
return nil
}
// Validate that the tag is not a child of any of its ancestors
validateChild := func(testID int) error {
if childTag, exists := allAncestors[testID]; exists {
return &InvalidTagHierarchyError{
Direction: "child",
CurrentRelation: "an ancestor",
InvalidTag: childTag.Name,
TagPath: childTag.Path,
}
}
return nil
}
for _, parentID := range parentIDs {
if err := validateParent(parentID); err != nil {
return err
}
}
for _, childID := range childIDs {
if err := validateChild(childID); err != nil {
return err
}
}
return nil
}
func ValidateHierarchyExisting(ctx context.Context, tag *models.Tag, parentIDs, childIDs []int, qb RelationshipFinder) error {
allAncestors := make(map[int]*models.TagPath)
allDescendants := make(map[int]*models.TagPath)
parentsAncestors, err := qb.FindAllAncestors(ctx, tag.ID, nil)
if err != nil {
return err
}
for _, ancestorTag := range parentsAncestors {
allAncestors[ancestorTag.ID] = ancestorTag
}
childsDescendants, err := qb.FindAllDescendants(ctx, tag.ID, nil)
if err != nil {
return err
}
for _, descendentTag := range childsDescendants {
allDescendants[descendentTag.ID] = descendentTag
}
validateParent := func(testID int) error {
if parentTag, exists := allDescendants[testID]; exists {
return &InvalidTagHierarchyError{
Direction: "parent",
CurrentRelation: "a descendant",
InvalidTag: parentTag.Name,
ApplyingTag: tag.Name,
TagPath: parentTag.Path,
}
}
return nil
}
validateChild := func(testID int) error {
if childTag, exists := allAncestors[testID]; exists {
return &InvalidTagHierarchyError{
Direction: "child",
CurrentRelation: "an ancestor",
InvalidTag: childTag.Name,
ApplyingTag: tag.Name,
TagPath: childTag.Path,
}
}
return nil
}
for _, parentID := range parentIDs {
if err := validateParent(parentID); err != nil {
return err
}
}
for _, childID := range childIDs {
if err := validateChild(childID); err != nil {
return err
}
}
return nil
}