refactor: integra reconcile no boot, dedup categoria/pasta, CI, Go 1.24
ci / check (push) Successful in 4m12s

- Boot chama ReconcileBlobs com log de órfãos (era código morto)
- treeNode/validTreeName/siblingDup/moveCreatesCycle compartilhados
  entre Create/Update Category/Folder (mesma regra, um só lugar)
- mapStoreErrorFor com mensagens 404 por recurso (nota/arquivo/pasta/categoria)
- revisionHash helper + teste GET /revisions/{hash} (200 + 404)
- ReconcileBlobs com lock curto (coleta sob RLock, I/O fora do lock)
- AddFile com LimitReader no stream (cap antes de gravar, não depois)
- DeleteFile/DeleteFolder transacionais com rollback em memória
- attachmentKind único no frontend (syntax/html/preview)
- CI: gofmt, vet, test -race, node --check; go.mod 1.24 = Dockerfile
- README: notas operacionais (memória, proxy, boot, SSE, 413/400)
This commit is contained in:
lgcosta committed 2026-11-05 19:57:38 -03:00
1 parent 0e59ef23f4
commit e0bef77e31
16 files changed
+518 -126

No files matched your search

+56 -17
View File
@@ -164,6 +164,44 @@ func New(dir string) (*Store, error) {
// Árvore de categorias (categories.json)
// ---------------------------------------------------------------------------
// treeNode é a interface mínima para validar árvores categoria/pasta com o
// mesmo algoritmo (nomes únicos entre irmãos, sem ciclos).
type treeNode struct {
ID string
Name string
Parent string
}
// validTreeName normaliza e valida o comprimento do nome.
func validTreeName(name string, maxLen int) (string, bool) {
n := strings.TrimSpace(name)
if n == "" || len([]rune(n)) > maxLen {
return "", false
}
return n, true
}
// siblingDup diz se já existe irmão com o mesmo nome (case-insensitive).
func siblingDup(nodes []treeNode, id, parent, name string) bool {
lower := strings.ToLower(name)
for _, o := range nodes {
if o.ID != id && o.Parent == parent && strings.ToLower(o.Name) == lower {
return true
}
}
return false
}
// moveCreatesCycle diz se mover id para newParent criaria ciclo.
func moveCreatesCycle(subtree []string, newParent string) bool {
for _, d := range subtree {
if d == newParent {
return true
}
}
return false
}
func (s *Store) catsPath() string { return filepath.Join(s.dir, "categories.json") }
func (s *Store) loadCats() error {
@@ -211,8 +249,8 @@ func (s *Store) catName(id string) string {
// CreateCategory cria uma pasta; nomes únicos entre irmãos (case-insensitive).
func (s *Store) CreateCategory(name, parent string) (*Category, error) {
name = strings.TrimSpace(name)
if r := []rune(name); name == "" || len(r) > maxCategoryLen {
name, ok := validTreeName(name, maxCategoryLen)
if !ok {
return nil, fmt.Errorf("%w: nome de categoria inválido", ErrInvalid)
}
s.mu.Lock()
@@ -220,11 +258,8 @@ func (s *Store) CreateCategory(name, parent string) (*Category, error) {
if parent != "" && s.catByID(parent) == nil {
return nil, fmt.Errorf("%w: categoria pai inexistente", ErrInvalid)
}
lower := strings.ToLower(name)
for _, c := range s.cats {
if c.Parent == parent && strings.ToLower(c.Name) == lower {
return nil, fmt.Errorf("%w: já existe uma categoria %q nesse nível", ErrInvalid, name)
}
if siblingDup(catNodes(s.cats), "", parent, name) {
return nil, fmt.Errorf("%w: já existe uma categoria %q nesse nível", ErrInvalid, name)
}
cat := Category{ID: newID(), Name: name, Parent: parent, CreatedAt: time.Now().UTC()}
s.cats = append(s.cats, cat)
@@ -234,6 +269,15 @@ func (s *Store) CreateCategory(name, parent string) (*Category, error) {
return &cat, nil
}
// catNodes projeta categorias para validação genérica de árvore.
func catNodes(cats []Category) []treeNode {
out := make([]treeNode, len(cats))
for i, c := range cats {
out[i] = treeNode{ID: c.ID, Name: c.Name, Parent: c.Parent}
}
return out
}
// UpdateCategory renomeia (name == "" mantém) e/ou move (parent != nil) a categoria.
func (s *Store) UpdateCategory(id, name string, parent *string) (*Category, error) {
s.mu.Lock()
@@ -246,22 +290,17 @@ func (s *Store) UpdateCategory(id, name string, parent *string) (*Category, erro
if *parent != "" && s.catByID(*parent) == nil {
return nil, fmt.Errorf("%w: categoria pai inexistente", ErrInvalid)
}
for _, d := range s.catSubtree(id) { // sem ciclos: pai não pode ser descendente
if d == *parent {
return nil, fmt.Errorf("%w: não é possível mover uma categoria para dentro dela mesma", ErrInvalid)
}
if moveCreatesCycle(s.catSubtree(id), *parent) {
return nil, fmt.Errorf("%w: não é possível mover uma categoria para dentro dela mesma", ErrInvalid)
}
c.Parent = *parent
}
if n := strings.TrimSpace(name); n != "" && n != c.Name {
if len([]rune(n)) > maxCategoryLen {
if _, ok := validTreeName(n, maxCategoryLen); !ok {
return nil, fmt.Errorf("%w: nome de categoria inválido", ErrInvalid)
}
lower := strings.ToLower(n)
for _, other := range s.cats {
if other.ID != id && other.Parent == c.Parent && strings.ToLower(other.Name) == lower {
return nil, fmt.Errorf("%w: já existe uma categoria %q nesse nível", ErrInvalid, n)
}
if siblingDup(catNodes(s.cats), id, c.Parent, n) {
return nil, fmt.Errorf("%w: já existe uma categoria %q nesse nível", ErrInvalid, n)
}
c.Name = n
}