mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 12:54:38 +03:00
Fix studio name uniqueness validation (#4454)
This commit is contained in:
168
pkg/studio/validate.go
Normal file
168
pkg/studio/validate.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package studio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNameMissing = errors.New("studio name must not be blank")
|
||||
ErrStudioOwnAncestor = errors.New("studio cannot be an ancestor of itself")
|
||||
)
|
||||
|
||||
type NameExistsError struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (e *NameExistsError) Error() string {
|
||||
return fmt.Sprintf("studio with name '%s' already exists", e.Name)
|
||||
}
|
||||
|
||||
type NameUsedByAliasError struct {
|
||||
Name string
|
||||
OtherStudio string
|
||||
}
|
||||
|
||||
func (e *NameUsedByAliasError) Error() string {
|
||||
return fmt.Sprintf("name '%s' is used as alias for '%s'", e.Name, e.OtherStudio)
|
||||
}
|
||||
|
||||
// EnsureStudioNameUnique returns an error if the studio name provided
|
||||
// is used as a name or alias of another existing tag.
|
||||
func EnsureStudioNameUnique(ctx context.Context, id int, name string, qb models.StudioQueryer) error {
|
||||
// ensure name is unique
|
||||
sameNameStudio, err := ByName(ctx, qb, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sameNameStudio != nil && id != sameNameStudio.ID {
|
||||
return &NameExistsError{
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
|
||||
// query by alias
|
||||
sameNameStudio, err = ByAlias(ctx, qb, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sameNameStudio != nil && id != sameNameStudio.ID {
|
||||
return &NameUsedByAliasError{
|
||||
Name: name,
|
||||
OtherStudio: sameNameStudio.Name,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnsureAliasesUnique(ctx context.Context, id int, aliases []string, qb models.StudioQueryer) error {
|
||||
for _, a := range aliases {
|
||||
if err := EnsureStudioNameUnique(ctx, id, a, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateCreate(ctx context.Context, studio models.Studio, qb models.StudioQueryer) error {
|
||||
if err := validateName(ctx, 0, studio.Name, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if studio.Aliases.Loaded() && len(studio.Aliases.List()) > 0 {
|
||||
if err := EnsureAliasesUnique(ctx, 0, studio.Aliases.List(), qb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateName(ctx context.Context, studioID int, name string, qb models.StudioQueryer) error {
|
||||
if name == "" {
|
||||
return ErrNameMissing
|
||||
}
|
||||
|
||||
if err := EnsureStudioNameUnique(ctx, studioID, name, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ValidateModifyReader interface {
|
||||
models.StudioGetter
|
||||
models.StudioQueryer
|
||||
models.AliasLoader
|
||||
}
|
||||
|
||||
// Checks to make sure that:
|
||||
// 1. The studio exists locally
|
||||
// 2. The studio is not its own ancestor
|
||||
// 3. The studio's aliases are unique
|
||||
// 4. The name is unique
|
||||
func ValidateModify(ctx context.Context, s models.StudioPartial, qb ValidateModifyReader) error {
|
||||
existing, err := qb.Find(ctx, s.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing == nil {
|
||||
return fmt.Errorf("studio with id %d not found", s.ID)
|
||||
}
|
||||
|
||||
newParentID := s.ParentID.Ptr()
|
||||
|
||||
if newParentID != nil {
|
||||
if err := validateParent(ctx, s.ID, *newParentID, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if s.Aliases != nil {
|
||||
if err := existing.LoadAliases(ctx, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
effectiveAliases := s.Aliases.Apply(existing.Aliases.List())
|
||||
if err := EnsureAliasesUnique(ctx, s.ID, effectiveAliases, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if s.Name.Set && s.Name.Value != existing.Name {
|
||||
if err := validateName(ctx, s.ID, s.Name.Value, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateParent(ctx context.Context, studioID int, newParentID int, qb models.StudioGetter) error {
|
||||
if newParentID == studioID {
|
||||
return ErrStudioOwnAncestor
|
||||
}
|
||||
|
||||
// ensure there is no cyclic dependency
|
||||
parentStudio, err := qb.Find(ctx, newParentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding parent studio: %v", err)
|
||||
}
|
||||
|
||||
if parentStudio == nil {
|
||||
return fmt.Errorf("studio with id %d not found", newParentID)
|
||||
}
|
||||
|
||||
if parentStudio.ParentID != nil {
|
||||
return validateParent(ctx, studioID, *parentStudio.ParentID, qb)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user