refactor: integra reconcile no boot, dedup categoria/pasta, CI, Go 1.24
ci / check (push) Successful in 4m12s
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:
1 parent
0e59ef23f4
commit
e0bef77e31
16 files changed
+518
-126
No files matched your search
+56
-17
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user