feat(PROJ-30): Xapian → Manticore Search Migration
- internal/index/manticore.go: ManticoreTenantManager + manticoreIndex (RT-Indizes, CGO-frei) - internal/index/index.go: TenantIndexer Interface (Xapian + Manticore) - internal/index/tenant_worker.go: mgr-Typ auf TenantIndexer Interface - internal/api/server.go: idxMgr auf TenantIndexer Interface - config/config.go: IndexConfig.ManticoreDSN Feld - cmd/archivmail/cmd_reindex.go: reindex Subkommando - cmd/archivmail/main.go: Manticore-Branch + reindex Case - go.mod: github.com/go-sql-driver/mysql v1.8.1 - update.sh: Manticore auto-install, CGO_ENABLED=0, config.yml migration, auto-reindex fix(IMAP): TCP-Deadline-Wrapper für steckengebliebene Imports fix(auth): Email-Claim in JWT für User-Isolation fix(search): User-Isolation via sess.Email (fail-safe) fix(ui): Admin-Login Auth-Cache, Logout-Redirect, IMAP-Polling-Resilienz Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/archivmail/config"
|
||||
"github.com/archivmail/internal/index"
|
||||
"github.com/archivmail/internal/storage"
|
||||
"github.com/archivmail/pkg/mailparser"
|
||||
)
|
||||
|
||||
// runReindex re-indexes all (or tenant-specific) emails into the configured index backend.
|
||||
// Usage: archivmail reindex [-config /path/to/config.yml] [-tenant <id>]
|
||||
func runReindex(args []string) {
|
||||
fs := flag.NewFlagSet("reindex", flag.ExitOnError)
|
||||
configPath := fs.String("config", "/etc/archivmail/config.yml", "path to config file")
|
||||
tenantIDFlag := fs.Int64("tenant", 0, "tenant ID to reindex (0 = all tenants)")
|
||||
fs.Parse(args)
|
||||
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
||||
|
||||
cfg, err := config.Load(*configPath)
|
||||
if err != nil {
|
||||
logger.Error("failed to load config", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
storeCfg := storage.Config{
|
||||
Dir: cfg.Storage.StorePath,
|
||||
Keyfile: cfg.Storage.Keyfile,
|
||||
DSN: cfg.Database.DSN(),
|
||||
}
|
||||
mailStore, err := storage.New(storeCfg)
|
||||
if err != nil {
|
||||
logger.Error("storage init failed", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer mailStore.Close()
|
||||
|
||||
indexBackend := cfg.Index.Backend
|
||||
if indexBackend == "" {
|
||||
indexBackend = "xapian"
|
||||
}
|
||||
batchSize := cfg.Index.BatchSize
|
||||
if batchSize <= 0 {
|
||||
batchSize = 100
|
||||
}
|
||||
|
||||
var idxMgr index.TenantIndexer
|
||||
if indexBackend == "manticore" {
|
||||
dsn := cfg.Index.ManticoreDSN
|
||||
if dsn == "" {
|
||||
dsn = "manticore@tcp(127.0.0.1:9306)/"
|
||||
}
|
||||
m, err := index.NewManticoreTenantManager(dsn)
|
||||
if err != nil {
|
||||
logger.Error("manticore init failed", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
idxMgr = m
|
||||
} else {
|
||||
m, err := index.NewTenantIndexManager(cfg.Index.Path, batchSize, indexBackend)
|
||||
if err != nil {
|
||||
logger.Error("index manager init failed", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
idxMgr = m
|
||||
}
|
||||
defer func() { idxMgr.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
var ids []string
|
||||
if *tenantIDFlag > 0 {
|
||||
tid := *tenantIDFlag
|
||||
ids, err = mailStore.GetAllIDsByTenant(ctx, &tid)
|
||||
} else {
|
||||
ids, err = mailStore.GetAllIDs(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error("failed to list mail IDs", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logger.Info("reindex: starting", "backend", indexBackend, "total", len(ids))
|
||||
|
||||
indexed := 0
|
||||
errors := 0
|
||||
for i, id := range ids {
|
||||
raw, err := mailStore.Load(id)
|
||||
if err != nil {
|
||||
logger.Warn("reindex: load failed", "id", id, "err", err)
|
||||
errors++
|
||||
continue
|
||||
}
|
||||
|
||||
pm, err := mailparser.Parse(raw)
|
||||
if err != nil {
|
||||
logger.Warn("reindex: parse failed", "id", id, "err", err)
|
||||
errors++
|
||||
continue
|
||||
}
|
||||
|
||||
tenantID, _ := mailStore.GetTenantForMail(ctx, id)
|
||||
|
||||
var attachNames []string
|
||||
for _, a := range pm.Attachments {
|
||||
if a.Filename != "" {
|
||||
attachNames = append(attachNames, a.Filename)
|
||||
}
|
||||
}
|
||||
|
||||
doc := index.MailDocument{
|
||||
ID: id,
|
||||
From: pm.From,
|
||||
To: strings.Join(pm.To, ", "),
|
||||
Subject: pm.Subject,
|
||||
Body: pm.TextBody,
|
||||
AttachNames: strings.Join(attachNames, " "),
|
||||
HasAttachment: len(pm.Attachments) > 0,
|
||||
Date: pm.Date,
|
||||
Size: int64(len(raw)),
|
||||
TenantID: tenantID,
|
||||
}
|
||||
|
||||
idx := idxMgr.ForTenant(tenantID)
|
||||
if err := idx.IndexSync(doc); err != nil {
|
||||
logger.Warn("reindex: index failed", "id", id, "err", err)
|
||||
errors++
|
||||
continue
|
||||
}
|
||||
indexed++
|
||||
|
||||
if (i+1)%100 == 0 {
|
||||
logger.Info("reindex: progress", "processed", i+1, "indexed", indexed, "errors", errors)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("reindex: complete", "total", len(ids), "indexed", indexed, "errors", errors)
|
||||
}
|
||||
+24
-6
@@ -55,6 +55,9 @@ func main() {
|
||||
case "migrate-tenants":
|
||||
runMigrateTenants(os.Args[2:])
|
||||
return
|
||||
case "reindex":
|
||||
runReindex(os.Args[2:])
|
||||
return
|
||||
case "version":
|
||||
fmt.Printf("archivmail %s\n", AppVersion)
|
||||
for mod, ver := range Modules {
|
||||
@@ -124,12 +127,27 @@ func main() {
|
||||
if batchSize <= 0 {
|
||||
batchSize = 100
|
||||
}
|
||||
idxMgr, err := index.NewTenantIndexManager(cfg.Index.Path, batchSize, indexBackend)
|
||||
if err != nil {
|
||||
logger.Error("index manager init failed", "err", err)
|
||||
os.Exit(1)
|
||||
var idxMgr index.TenantIndexer
|
||||
if indexBackend == "manticore" {
|
||||
dsn := cfg.Index.ManticoreDSN
|
||||
if dsn == "" {
|
||||
dsn = "manticore@tcp(127.0.0.1:9306)/"
|
||||
}
|
||||
m, err := index.NewManticoreTenantManager(dsn)
|
||||
if err != nil {
|
||||
logger.Error("manticore index manager init failed", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
idxMgr = m
|
||||
} else {
|
||||
m, err := index.NewTenantIndexManager(cfg.Index.Path, batchSize, indexBackend)
|
||||
if err != nil {
|
||||
logger.Error("index manager init failed", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
idxMgr = m
|
||||
}
|
||||
defer idxMgr.Close()
|
||||
defer func() { idxMgr.Close() }()
|
||||
|
||||
// Global index reference for backward compatibility (IMAP importer, etc.)
|
||||
idx := idxMgr.Global()
|
||||
@@ -469,7 +487,7 @@ func runBackfill(ctx context.Context, store *storage.Store, idx index.Indexer, w
|
||||
|
||||
// reindexTenant re-indexes all emails belonging to a specific tenant.
|
||||
// Used during migration when switching from global index to per-tenant indexes.
|
||||
func reindexTenant(ctx context.Context, store *storage.Store, mgr *index.TenantIndexManager, tenantID int64, logger *slog.Logger) error {
|
||||
func reindexTenant(ctx context.Context, store *storage.Store, mgr index.TenantIndexer, tenantID int64, logger *slog.Logger) error {
|
||||
tid := tenantID
|
||||
ids, err := store.GetAllIDsByTenant(ctx, &tid)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user