feat: add --dry-run mode to Go implementation
Add comprehensive dry-run functionality that allows users to test their configuration without making any changes to CouchDB. The feature includes: - New --dry-run/-n command line flag with help and completion support - Skips all CouchDB write operations while preserving IMAP operations - Provides detailed logging of what would be done in normal mode - Shows sample message data and metadata updates that would occur - Maintains all existing functionality when dry-run is disabled This addresses the critical usability need identified in ANALYSIS.md for safe configuration testing before making database changes. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
14d2aafbf0
commit
61ba952155
2 changed files with 100 additions and 57 deletions
150
go/main.go
150
go/main.go
|
|
@ -19,10 +19,14 @@ func main() {
|
|||
log.Fatalf("Failed to load configuration: %v", err)
|
||||
}
|
||||
|
||||
// Initialize CouchDB client
|
||||
couchClient, err := couch.NewClient(&cfg.CouchDb)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create CouchDB client: %v", err)
|
||||
// Initialize CouchDB client (skip in dry-run mode)
|
||||
var couchClient *couch.Client
|
||||
if !args.DryRun {
|
||||
var err error
|
||||
couchClient, err = couch.NewClient(&cfg.CouchDb)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create CouchDB client: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d mail source(s) to process.\n", len(cfg.MailSources))
|
||||
|
|
@ -34,21 +38,25 @@ func main() {
|
|||
// Generate per-account database name
|
||||
dbName := couch.GenerateAccountDBName(source.Name, source.User)
|
||||
|
||||
// Ensure the account-specific database exists
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
err = couchClient.EnsureDB(ctx, dbName)
|
||||
cancel()
|
||||
// Ensure the account-specific database exists (skip in dry-run mode)
|
||||
if !args.DryRun {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
err = couchClient.EnsureDB(ctx, dbName)
|
||||
cancel()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Could not ensure CouchDB database '%s' exists (is it running?): %v", dbName, err)
|
||||
continue
|
||||
if err != nil {
|
||||
log.Printf("Could not ensure CouchDB database '%s' exists (is it running?): %v", dbName, err)
|
||||
continue
|
||||
} else {
|
||||
fmt.Printf("CouchDB database '%s' is ready for account: %s\n", dbName, source.Name)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("CouchDB database '%s' is ready for account: %s\n", dbName, source.Name)
|
||||
fmt.Printf("DRY-RUN: Would ensure CouchDB database '%s' exists for account: %s\n", dbName, source.Name)
|
||||
}
|
||||
|
||||
fmt.Printf(" - Processing source: %s\n", source.Name)
|
||||
if source.Protocol == "imap" {
|
||||
err := processImapSource(&source, couchClient, dbName, args.MaxMessages)
|
||||
err := processImapSource(&source, couchClient, dbName, args.MaxMessages, args.DryRun)
|
||||
if err != nil {
|
||||
log.Printf(" ERROR: Failed to process IMAP source %s: %v", source.Name, err)
|
||||
}
|
||||
|
|
@ -56,7 +64,7 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
func processImapSource(source *config.MailSource, couchClient *couch.Client, dbName string, maxMessages int) error {
|
||||
func processImapSource(source *config.MailSource, couchClient *couch.Client, dbName string, maxMessages int, dryRun bool) error {
|
||||
fmt.Printf(" Connecting to IMAP server: %s:%d\n", source.Host, source.Port)
|
||||
imapClient, err := mail.NewImapClient(source)
|
||||
if err != nil {
|
||||
|
|
@ -92,13 +100,17 @@ func processImapSource(source *config.MailSource, couchClient *couch.Client, dbN
|
|||
for _, mailbox := range mailboxes {
|
||||
fmt.Printf(" Processing mailbox: %s (mode: %s)\n", mailbox, source.Mode)
|
||||
|
||||
// Get sync metadata to determine incremental sync date
|
||||
syncCtx, syncCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
syncMetadata, err := couchClient.GetSyncMetadata(syncCtx, dbName, mailbox)
|
||||
syncCancel()
|
||||
if err != nil {
|
||||
log.Printf(" ERROR: Failed to get sync metadata for %s: %v", mailbox, err)
|
||||
continue
|
||||
// Get sync metadata to determine incremental sync date (skip in dry-run mode)
|
||||
var syncMetadata *couch.SyncMetadata
|
||||
if !dryRun {
|
||||
syncCtx, syncCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
var err error
|
||||
syncMetadata, err = couchClient.GetSyncMetadata(syncCtx, dbName, mailbox)
|
||||
syncCancel()
|
||||
if err != nil {
|
||||
log.Printf(" ERROR: Failed to get sync metadata for %s: %v", mailbox, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the since date for incremental sync
|
||||
|
|
@ -114,7 +126,11 @@ func processImapSource(source *config.MailSource, couchClient *couch.Client, dbN
|
|||
if sinceDate != nil {
|
||||
fmt.Printf(" First sync since: %s (from config)\n", sinceDate.Format("2006-01-02"))
|
||||
} else {
|
||||
fmt.Printf(" First full sync (no date filter)\n")
|
||||
if dryRun {
|
||||
fmt.Printf(" DRY-RUN: Would perform first full sync (no date filter)\n")
|
||||
} else {
|
||||
fmt.Printf(" First full sync (no date filter)\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -125,13 +141,18 @@ func processImapSource(source *config.MailSource, couchClient *couch.Client, dbN
|
|||
continue
|
||||
}
|
||||
|
||||
// Perform sync/archive logic
|
||||
mailboxSyncCtx, mailboxSyncCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
err = couchClient.SyncMailbox(mailboxSyncCtx, dbName, mailbox, currentUIDs, source.IsSyncMode())
|
||||
mailboxSyncCancel()
|
||||
if err != nil {
|
||||
log.Printf(" ERROR: Failed to sync mailbox %s: %v", mailbox, err)
|
||||
continue
|
||||
// Perform sync/archive logic (skip in dry-run mode)
|
||||
if !dryRun {
|
||||
mailboxSyncCtx, mailboxSyncCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
err = couchClient.SyncMailbox(mailboxSyncCtx, dbName, mailbox, currentUIDs, source.IsSyncMode())
|
||||
mailboxSyncCancel()
|
||||
if err != nil {
|
||||
log.Printf(" ERROR: Failed to sync mailbox %s: %v", mailbox, err)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" DRY-RUN: Would sync mailbox %s with %d current UIDs (mode: %s)\n",
|
||||
mailbox, len(currentUIDs), source.Mode)
|
||||
}
|
||||
|
||||
if len(messages) == 0 {
|
||||
|
|
@ -149,23 +170,32 @@ func processImapSource(source *config.MailSource, couchClient *couch.Client, dbN
|
|||
docs = append(docs, doc)
|
||||
}
|
||||
|
||||
// Store messages in CouchDB with attachments
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
// Store messages in CouchDB with attachments (skip in dry-run mode)
|
||||
stored := 0
|
||||
for i, doc := range docs {
|
||||
err := couchClient.StoreMessage(ctx, dbName, doc, messages[i])
|
||||
if err != nil {
|
||||
log.Printf(" ERROR: Failed to store message %s: %v", doc.ID, err)
|
||||
} else {
|
||||
stored++
|
||||
if !dryRun {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
for i, doc := range docs {
|
||||
err := couchClient.StoreMessage(ctx, dbName, doc, messages[i])
|
||||
if err != nil {
|
||||
log.Printf(" ERROR: Failed to store message %s: %v", doc.ID, err)
|
||||
} else {
|
||||
stored++
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
fmt.Printf(" Stored %d/%d messages from %s\n", stored, len(messages), mailbox)
|
||||
} else {
|
||||
stored = len(messages) // In dry-run, assume all would be stored
|
||||
fmt.Printf(" DRY-RUN: Would store %d messages from %s\n", len(messages), mailbox)
|
||||
// Show sample of what would be stored
|
||||
if len(docs) > 0 {
|
||||
fmt.Printf(" DRY-RUN: Sample message ID: %s (Subject: %s)\n",
|
||||
docs[0].ID, docs[0].Subject)
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
|
||||
fmt.Printf(" Stored %d/%d messages from %s\n", stored, len(messages), mailbox)
|
||||
totalStored += stored
|
||||
|
||||
// Update sync metadata after successful processing
|
||||
// Update sync metadata after successful processing (skip in dry-run mode)
|
||||
if len(messages) > 0 {
|
||||
// Find the highest UID processed
|
||||
var maxUID uint32
|
||||
|
|
@ -175,26 +205,34 @@ func processImapSource(source *config.MailSource, couchClient *couch.Client, dbN
|
|||
}
|
||||
}
|
||||
|
||||
// Create/update sync metadata
|
||||
newMetadata := &couch.SyncMetadata{
|
||||
Mailbox: mailbox,
|
||||
LastSyncTime: time.Now(),
|
||||
LastMessageUID: maxUID,
|
||||
MessageCount: stored,
|
||||
}
|
||||
if !dryRun {
|
||||
// Create/update sync metadata
|
||||
newMetadata := &couch.SyncMetadata{
|
||||
Mailbox: mailbox,
|
||||
LastSyncTime: time.Now(),
|
||||
LastMessageUID: maxUID,
|
||||
MessageCount: stored,
|
||||
}
|
||||
|
||||
// Store sync metadata
|
||||
metadataCtx, metadataCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
err = couchClient.StoreSyncMetadata(metadataCtx, dbName, newMetadata)
|
||||
metadataCancel()
|
||||
if err != nil {
|
||||
log.Printf(" WARNING: Failed to store sync metadata for %s: %v", mailbox, err)
|
||||
// Store sync metadata
|
||||
metadataCtx, metadataCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
err = couchClient.StoreSyncMetadata(metadataCtx, dbName, newMetadata)
|
||||
metadataCancel()
|
||||
if err != nil {
|
||||
log.Printf(" WARNING: Failed to store sync metadata for %s: %v", mailbox, err)
|
||||
} else {
|
||||
fmt.Printf(" Updated sync metadata (last UID: %d)\n", maxUID)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" Updated sync metadata (last UID: %d)\n", maxUID)
|
||||
fmt.Printf(" DRY-RUN: Would update sync metadata (last UID: %d, %d messages)\n", maxUID, stored)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf(" Summary: Processed %d messages, stored %d new messages\n", totalMessages, totalStored)
|
||||
if dryRun {
|
||||
fmt.Printf(" DRY-RUN Summary: Found %d messages, would store %d messages\n", totalMessages, totalStored)
|
||||
} else {
|
||||
fmt.Printf(" Summary: Processed %d messages, stored %d new messages\n", totalMessages, totalStored)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue