Files
sm-bm-plugin/controllers/materials_hooks.go
Jakub Zych f841e8ed71 feat: extract file text on every attach path and backfill on reindex
- inline and signed uploads queue the text job in their transaction
- the admin form queues it in the save transaction
- detach resyncs the material (its text row goes by cascade)
- bm:materials-reindex backfills missing PDF texts before each sync
2026-11-08 20:16:14 +01:00

86 lines
2.8 KiB
Go

package controllers
import (
"context"
"errors"
"sync/atomic"
"git.golem15.com/golem15/summercms/modules/cabana"
"git.golem15.com/golem15/summercms/modules/pact"
"git.golem15.com/jakub/sm-bm-plugin/models"
"gorm.io/gorm"
)
var (
_ pact.FormAfterCreate = materialsAdmin{}
_ pact.FormAfterUpdate = materialsAdmin{}
_ pact.FormAfterDelete = materialsAdmin{}
)
// MaterialIndex keeps the materials search index current once a write
// commits (classes.MaterialIndexer). QueueExtract enqueues the file text job
// inside tx. Its calls never fail the write.
type MaterialIndex interface {
SyncAfterCommit(ctx context.Context, tx *gorm.DB, id uint)
RemoveAfterCommit(ctx context.Context, tx *gorm.DB, id uint)
QueueExtract(ctx context.Context, tx *gorm.DB, id uint)
}
type materialIndexHolder struct{ ix MaterialIndex }
var materialIndex atomic.Value
// UseMaterialIndex installs the index the materials admin syncs after a
// save or delete; nil turns the syncs off. Boot installs it, because the
// generated controller value has no field to carry it.
func UseMaterialIndex(ix MaterialIndex) {
materialIndex.Store(materialIndexHolder{ix: ix})
}
func installedMaterialIndex() MaterialIndex {
h, _ := materialIndex.Load().(materialIndexHolder)
return h.ix
}
// FormAfterCreate queues the file text job in the save transaction (the
// form's deferred files commit in the same one) and syncs the new material
// once it commits.
func (materialsAdmin) FormAfterCreate(ctx context.Context, model any) error {
return materialAfterCommit(ctx, model, false)
}
// FormAfterUpdate queues the file text job in the save transaction and
// syncs the material once it commits. A save that added no file queues a
// job that finds no new PDF and only resyncs; a file removed in the form
// leaves the document through the cascade and this sync.
func (materialsAdmin) FormAfterUpdate(ctx context.Context, model any) error {
return materialAfterCommit(ctx, model, false)
}
// FormAfterDelete removes the material's document once the delete commits.
func (materialsAdmin) FormAfterDelete(ctx context.Context, model any) error {
return materialAfterCommit(ctx, model, true)
}
// materialAfterCommit schedules the index work of an admin save on the save
// transaction. Only a model that is not a material is an error: the index
// never fails a save.
func materialAfterCommit(ctx context.Context, model any, remove bool) error {
m, ok := model.(*models.Material)
if !ok || m == nil {
return errors.New("materials admin: form model is not a material")
}
ix := installedMaterialIndex()
if ix == nil || m.ID == 0 {
return nil
}
tx, _ := cabana.TxFromContext(ctx)
if remove {
ix.RemoveAfterCommit(ctx, tx, m.ID)
return nil
}
ix.QueueExtract(ctx, tx, m.ID)
ix.SyncAfterCommit(ctx, tx, m.ID)
return nil
}