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
61 lines
2.1 KiB
Go
61 lines
2.1 KiB
Go
// 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
|
|
}
|