154 lines
3.4 KiB
Go
154 lines
3.4 KiB
Go
package importer
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"strings"
|
|
"sync"
|
|
|
|
"box-manifest/server/internal/tree"
|
|
)
|
|
|
|
const (
|
|
MaxNodes = 10_000
|
|
MaxDepth = 64
|
|
)
|
|
|
|
type Manifest struct {
|
|
Nodes []tree.ImportNode `json:"nodes"`
|
|
}
|
|
|
|
type Summary struct {
|
|
Nodes int `json:"nodes"`
|
|
MaximumDepth int `json:"maximumDepth"`
|
|
}
|
|
|
|
type Preview struct {
|
|
PlanID string `json:"planId"`
|
|
Summary Summary `json:"summary"`
|
|
Warnings []string `json:"warnings"`
|
|
Manifest Manifest `json:"manifest"`
|
|
}
|
|
|
|
type plan struct {
|
|
parentID string
|
|
manifest Manifest
|
|
committing bool
|
|
}
|
|
|
|
type Service struct {
|
|
store *tree.Store
|
|
mu sync.Mutex
|
|
plans map[string]*plan
|
|
}
|
|
|
|
func New(store *tree.Store) *Service {
|
|
return &Service{store: store, plans: make(map[string]*plan)}
|
|
}
|
|
|
|
func (s *Service) Preview(ctx context.Context, parentID string, manifest Manifest) (Preview, error) {
|
|
if _, err := s.store.Get(ctx, parentID); err != nil {
|
|
return Preview{}, err
|
|
}
|
|
normalized, summary, err := normalize(manifest)
|
|
if err != nil {
|
|
return Preview{}, err
|
|
}
|
|
planID, err := newPlanID()
|
|
if err != nil {
|
|
return Preview{}, err
|
|
}
|
|
|
|
s.mu.Lock()
|
|
s.plans[planID] = &plan{parentID: parentID, manifest: normalized}
|
|
s.mu.Unlock()
|
|
return Preview{
|
|
PlanID: planID,
|
|
Summary: summary,
|
|
Warnings: []string{},
|
|
Manifest: normalized,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) Commit(ctx context.Context, planID string) (Summary, error) {
|
|
s.mu.Lock()
|
|
p, exists := s.plans[planID]
|
|
if !exists {
|
|
s.mu.Unlock()
|
|
return Summary{}, tree.ErrNotFound
|
|
}
|
|
if p.committing {
|
|
s.mu.Unlock()
|
|
return Summary{}, tree.ErrConflict
|
|
}
|
|
p.committing = true
|
|
s.mu.Unlock()
|
|
|
|
created, err := s.store.CreateTree(ctx, p.parentID, p.manifest.Nodes)
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if err != nil {
|
|
p.committing = false
|
|
return Summary{}, err
|
|
}
|
|
delete(s.plans, planID)
|
|
_, summary, _ := normalize(p.manifest)
|
|
summary.Nodes = created
|
|
return summary, nil
|
|
}
|
|
|
|
func normalize(manifest Manifest) (Manifest, Summary, error) {
|
|
if len(manifest.Nodes) == 0 {
|
|
return Manifest{}, Summary{}, tree.ErrInvalid
|
|
}
|
|
count := 0
|
|
maximumDepth := 0
|
|
var visit func([]tree.ImportNode, int) ([]tree.ImportNode, error)
|
|
visit = func(nodes []tree.ImportNode, depth int) ([]tree.ImportNode, error) {
|
|
if depth > MaxDepth {
|
|
return nil, tree.ErrInvalid
|
|
}
|
|
normalized := make([]tree.ImportNode, len(nodes))
|
|
for index, node := range nodes {
|
|
count++
|
|
if count > MaxNodes {
|
|
return nil, tree.ErrInvalid
|
|
}
|
|
name := strings.TrimSpace(node.Name)
|
|
if name == "" || node.Quantity != nil && *node.Quantity < 0 {
|
|
return nil, tree.ErrInvalid
|
|
}
|
|
children, err := visit(node.Children, depth+1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var description *string
|
|
if node.Description != nil {
|
|
trimmed := strings.TrimSpace(*node.Description)
|
|
if trimmed != "" {
|
|
description = &trimmed
|
|
}
|
|
}
|
|
normalized[index] = tree.ImportNode{Name: name, Description: description, Quantity: node.Quantity, Children: children}
|
|
if depth > maximumDepth {
|
|
maximumDepth = depth
|
|
}
|
|
}
|
|
return normalized, nil
|
|
}
|
|
nodes, err := visit(manifest.Nodes, 1)
|
|
if err != nil {
|
|
return Manifest{}, Summary{}, err
|
|
}
|
|
return Manifest{Nodes: nodes}, Summary{Nodes: count, MaximumDepth: maximumDepth}, nil
|
|
}
|
|
|
|
func newPlanID() (string, error) {
|
|
var value [16]byte
|
|
if _, err := rand.Read(value[:]); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(value[:]), nil
|
|
}
|