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
This commit is contained in:
@@ -208,7 +208,8 @@ func sizeMB(n int64) string {
|
||||
}
|
||||
|
||||
// DetachMaterialFile answers DELETE /materials/{id}/files/{file}: the file
|
||||
// row goes, then its blob and thumbnails. A file of another material is a
|
||||
// row goes (its extracted text with it), the material is resynced, then the
|
||||
// blob and thumbnails go. A file of another material is a
|
||||
// 404.
|
||||
func (h *Handlers) DetachMaterialFile(w http.ResponseWriter, r *http.Request) {
|
||||
db, ok := h.db(w, r)
|
||||
@@ -242,6 +243,9 @@ func (h *Handlers) DetachMaterialFile(w http.ResponseWriter, r *http.Request) {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
// The file's text row went with it (foreign-key cascade); the resync
|
||||
// drops the text from the material's document.
|
||||
h.MaterialIndex.Sync(r.Context(), db, m.ID)
|
||||
// The row is gone, so the file is no longer served; a blob left behind
|
||||
// by a failed delete is only disk space.
|
||||
_ = attach.DeleteKeys(r.Context(), bucket, attach.BlobKeys(f))
|
||||
|
||||
@@ -134,6 +134,10 @@ func (h *Handlers) AttachMaterialInline(w http.ResponseWriter, r *http.Request)
|
||||
err := lagoon.Transaction(r.Context(), db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
f, why, err := storeMaterialFile(ctx, tx, bucket, m, name, bytes.NewReader(data), lim)
|
||||
stored, refusal = f, why
|
||||
if err == nil && why == "" && f != nil {
|
||||
// The text job joins the transaction and syncs the material.
|
||||
h.MaterialIndex.QueueExtract(ctx, tx, m.ID)
|
||||
}
|
||||
return err
|
||||
})
|
||||
if err != nil || refusal != "" {
|
||||
@@ -302,6 +306,8 @@ func (h *Handlers) SignedMaterialUpload(w http.ResponseWriter, r *http.Request)
|
||||
refusal = singleFilePart
|
||||
return errUploadRefused
|
||||
}
|
||||
// The text job joins the transaction and syncs the material.
|
||||
h.MaterialIndex.QueueExtract(ctx, tx, m.ID)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -18,10 +18,12 @@ var (
|
||||
)
|
||||
|
||||
// MaterialIndex keeps the materials search index current once a write
|
||||
// commits (classes.MaterialIndexer). Its calls never fail the 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 }
|
||||
@@ -40,12 +42,17 @@ func installedMaterialIndex() MaterialIndex {
|
||||
return h.ix
|
||||
}
|
||||
|
||||
// FormAfterCreate syncs the new material once the save transaction commits.
|
||||
// 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 syncs the material once the save transaction commits.
|
||||
// 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)
|
||||
}
|
||||
@@ -70,8 +77,9 @@ func materialAfterCommit(ctx context.Context, model any, remove bool) error {
|
||||
tx, _ := cabana.TxFromContext(ctx)
|
||||
if remove {
|
||||
ix.RemoveAfterCommit(ctx, tx, m.ID)
|
||||
} else {
|
||||
ix.SyncAfterCommit(ctx, tx, m.ID)
|
||||
return nil
|
||||
}
|
||||
ix.QueueExtract(ctx, tx, m.ID)
|
||||
ix.SyncAfterCommit(ctx, tx, m.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -26,6 +26,11 @@ func (r *recordingIndex) RemoveAfterCommit(_ context.Context, tx *gorm.DB, id ui
|
||||
r.txs = append(r.txs, tx)
|
||||
}
|
||||
|
||||
func (r *recordingIndex) QueueExtract(_ context.Context, tx *gorm.DB, id uint) {
|
||||
r.calls = append(r.calls, "queue "+itoa(id))
|
||||
r.txs = append(r.txs, tx)
|
||||
}
|
||||
|
||||
func itoa(id uint) string { return strconv.FormatUint(uint64(id), 10) }
|
||||
|
||||
func TestMaterialsAdminHooks(t *testing.T) {
|
||||
@@ -53,7 +58,9 @@ func TestMaterialsAdminHooks(t *testing.T) {
|
||||
if err := c.FormAfterDelete(ctx, m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := []string{"sync 7", "sync 7", "remove 7"}; !slices.Equal(rec.calls, want) {
|
||||
// Create and update queue the file text job (in the save transaction)
|
||||
// before the after-commit sync; delete queues nothing.
|
||||
if want := []string{"queue 7", "sync 7", "queue 7", "sync 7", "remove 7"}; !slices.Equal(rec.calls, want) {
|
||||
t.Errorf("calls = %v, want %v", rec.calls, want)
|
||||
}
|
||||
// Outside a cabana write there is no transaction: the index then runs
|
||||
|
||||
Reference in New Issue
Block a user