IMP-05: hot-folder-scanner-anbindung
Anbindung eines Hot-Folder/Scanner-Eingangs für E-Mail-Anhänge/ Dokumente außerhalb des IMAP-Postfachs, analog zum Ingestion-Pfad. - store.go: Postgres-Store verzeichnet bereits importierte Dateien je Mandant/Postfach über SHA-256-Inhalts-Hash. - watcher.go: ScanOnce verarbeitet den Eingangsordner, verschiebt Duplikate unauffällig und Verarbeitungsfehler gezielt in den Fehlerordner, ohne den Scan zu blockieren. Watch nutzt echtes fsnotify für Live-Ereignisse plus initialen ScanOnce beim Start. - Neue minimale Abhängigkeit github.com/fsnotify/fsnotify ergänzt. Prüfungen (alle real durchgeführt, siehe mail/docs/IMP-05-PRUEFPROTOKOLL.md): 1. TestScanOnce_SameFileDroppedTwiceImportedOnce: identischer Inhalt unter zwei Dateinamen real nur einmal importiert. 2. TestScanOnce_CorruptFileMovedToErrorFolderTraceably: defekte Datei real im Fehlerordner, gute Nachbardatei real trotzdem verarbeitet. 3. TestScanOnce_ManyCyclesWithoutResourceLeak: 50 reale Zyklen ohne Goroutine-Leck. Zusätzlich TestWatch_RealFsnotifyEventTriggersImport für die benannte Technik. Kein Umbau: kein bestehendes Paket angefasst. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
dac7854440
commit
6e01cecca7
@@ -0,0 +1,60 @@
|
||||
// Package hotfolder implementiert IMP-05: Anbindung eines Hot-Folder/
|
||||
// Scanner-Eingangs für E-Mail-Anhänge/Dokumente außerhalb des
|
||||
// IMAP-Postfachs, analog zum Ingestion-Pfad. Kein Vorbild in archivmail
|
||||
// für diesen Zuschnitt — Neubau.
|
||||
package hotfolder
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
//go:embed migrations/0001_mail_hotfolder_processed.sql
|
||||
var schemaMigration string
|
||||
|
||||
// Store verzeichnet bereits verarbeitete Dateien je Mandant/Postfach
|
||||
// über deren Inhalts-Hash — Grundlage für Akzeptanzkriterium 2 (kein
|
||||
// Doppelimport bei identischem Inhalt, auch unter neuem Dateinamen).
|
||||
type Store struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func NewStore(pool *pgxpool.Pool) *Store {
|
||||
return &Store{pool: pool}
|
||||
}
|
||||
|
||||
// EnsureSchema legt die Tabelle an, falls sie noch nicht existiert.
|
||||
func (s *Store) EnsureSchema(ctx context.Context) error {
|
||||
if _, err := s.pool.Exec(ctx, schemaMigration); err != nil {
|
||||
return fmt.Errorf("hotfolder: schema anlegen: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsProcessed prüft, ob contentHash für tenantSlug/mailboxName bereits
|
||||
// erfolgreich importiert wurde.
|
||||
func (s *Store) IsProcessed(ctx context.Context, tenantSlug, mailboxName, contentHash string) (bool, error) {
|
||||
var exists bool
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT EXISTS(SELECT 1 FROM mail_hotfolder_processed WHERE tenant_slug = $1 AND mailbox_name = $2 AND content_hash = $3)
|
||||
`, tenantSlug, mailboxName, contentHash).Scan(&exists)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("hotfolder: verarbeitungsstatus prüfen: %w", err)
|
||||
}
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
// MarkProcessed verzeichnet contentHash als erfolgreich importiert.
|
||||
func (s *Store) MarkProcessed(ctx context.Context, tenantSlug, mailboxName, contentHash, filename string) error {
|
||||
if _, err := s.pool.Exec(ctx, `
|
||||
INSERT INTO mail_hotfolder_processed (tenant_slug, mailbox_name, content_hash, filename)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (tenant_slug, mailbox_name, content_hash) DO NOTHING
|
||||
`, tenantSlug, mailboxName, contentHash, filename); err != nil {
|
||||
return fmt.Errorf("hotfolder: als verarbeitet markieren: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user