From 616beecc41b573503dad9f5bfd9f353c6f826a8a Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 6 Apr 2026 11:12:03 +0300 Subject: refactor: extract shared CardStore into internal/store to eliminate duplication FindCardDirectory, FindOrCreateCardDirectory, GenerateCardID and the ScanWords helper previously existed in both internal/utils.go (as standalone functions) and were partially duplicated in internal/gui/card_service.go (readWordFromDir, ScanExistingWords). Introduce internal/store.CardStore as the single source of truth for all on-disk card-directory operations. Both internal/processor and internal/gui now hold a *store.CardStore field and delegate to it, removing the last copy of the directory-scanning loop from card_service.go. internal/utils.go keeps thin forwarding wrappers for callers that import the root internal package. Also adds table-driven unit tests for the new package covering FindCardDirectory (including legacy _word.txt fallback), FindOrCreateCardDirectory, and CardStore.ScanWords. Co-Authored-By: Claude Sonnet 4.6 --- internal/utils.go | 73 ++++++++----------------------------------------------- 1 file changed, 10 insertions(+), 63 deletions(-) (limited to 'internal/utils.go') diff --git a/internal/utils.go b/internal/utils.go index b83d46a..e16e10e 100644 --- a/internal/utils.go +++ b/internal/utils.go @@ -1,85 +1,32 @@ package internal import ( - "crypto/md5" - "encoding/hex" - "fmt" - "os" - "path/filepath" "strings" - "time" + + "codeberg.org/snonux/totalrecall/internal/store" ) -// GenerateCardID creates a unique ID for a card based on timestamp and Bulgarian word +// GenerateCardID creates a unique ID for a card based on timestamp and Bulgarian word. +// Delegates to store.GenerateCardID which is the single source of truth. // Format: epochMillis_md5(word)[:8] func GenerateCardID(bulgarianWord string) string { - // Get current timestamp in milliseconds - now := time.Now() - epochMillis := now.UnixNano() / 1000000 - - // Calculate MD5 hash of the word - hash := md5.Sum([]byte(bulgarianWord)) - hashStr := hex.EncodeToString(hash[:])[:8] // Use first 8 chars of MD5 - - // Combine timestamp and hash - return fmt.Sprintf("%d_%s", epochMillis, hashStr) + return store.GenerateCardID(bulgarianWord) } // FindCardDirectory searches outputDir for a subdirectory whose word.txt // (or legacy _word.txt) matches the given word. Returns the directory path // or an empty string if not found. +// Delegates to store.FindCardDirectory which is the single source of truth. func FindCardDirectory(outputDir, word string) string { - entries, err := os.ReadDir(outputDir) - if err != nil { - return "" - } - - for _, entry := range entries { - if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") { - continue - } - - dirPath := filepath.Join(outputDir, entry.Name()) - wordFile := filepath.Join(dirPath, "word.txt") - - if data, err := os.ReadFile(wordFile); err == nil { - if strings.TrimSpace(string(data)) == word { - return dirPath - } - } else { - // Backward-compatible fallback: old format used _word.txt - wordFile = filepath.Join(dirPath, "_word.txt") - if data, err := os.ReadFile(wordFile); err == nil { - if strings.TrimSpace(string(data)) == word { - return dirPath - } - } - } - } - - return "" + return store.FindCardDirectory(outputDir, word) } // FindOrCreateCardDirectory returns the existing card directory for word inside // outputDir, or creates a new one with a generated card ID. It also writes // word.txt so subsequent calls can find the directory. +// Delegates to store.FindOrCreateCardDirectory which is the single source of truth. func FindOrCreateCardDirectory(outputDir, word string) string { - if dir := FindCardDirectory(outputDir, word); dir != "" { - return dir - } - - cardID := GenerateCardID(word) - wordDir := filepath.Join(outputDir, cardID) - if err := os.MkdirAll(wordDir, 0755); err != nil { - fmt.Printf("Warning: failed to create word directory: %v\n", err) - return outputDir - } - - if err := os.WriteFile(filepath.Join(wordDir, "word.txt"), []byte(word), 0644); err != nil { - fmt.Printf("Warning: failed to save word metadata: %v\n", err) - } - - return wordDir + return store.FindOrCreateCardDirectory(outputDir, word) } // SanitizeFilename creates a safe filename from a string. @@ -97,7 +44,7 @@ func SanitizeFilename(s string) string { return b.String() } -// isAlphaNumeric checks if a rune is alphanumeric +// isAlphaNumeric checks if a rune is alphanumeric (Latin or Cyrillic). func isAlphaNumeric(r rune) bool { return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || (r >= 'а' && r <= 'я') || -- cgit v1.2.3