Files
box-manifest-android/server/internal/importer/service.go
T

147 lines
3.2 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
}
normalized[index] = tree.ImportNode{Name: name, 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
}