// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT //go:build linux || freebsd package storage import ( "encoding/json" "fmt" "os" "path/filepath" "sync" "time" ) // Submission is one archived form submission, one JSON object per line. // Newsletter forms do not write here: they persist through the double // opt-in subscriber log instead. type Submission struct { Form string `json:"form"` Name string `json:"name,omitempty"` Email string `json:"email"` Service string `json:"service,omitempty"` Message string `json:"message"` IP string `json:"ip,omitempty"` CreatedAt time.Time `json:"created_at"` } // ArchiveStore is a file-based append-only log of submissions. All methods // are safe for concurrent use. The file and its parent directory are // created lazily on first Append, so a form that archives nothing writes // nothing. type ArchiveStore struct { path string mu sync.Mutex } // NewArchiveStore returns a store that appends to path. func NewArchiveStore(path string) *ArchiveStore { return &ArchiveStore{path: path} } // Path returns the file path this store writes to. func (s *ArchiveStore) Path() string { return s.path } // Append writes sub as a single JSON line to the log. func (s *ArchiveStore) Append(sub Submission) error { if sub.CreatedAt.IsZero() { sub.CreatedAt = time.Now().UTC() } line, err := json.Marshal(sub) if err != nil { return fmt.Errorf("marshal submission: %w", err) } line = append(line, '\n') s.mu.Lock() defer s.mu.Unlock() return appendLine(s.path, line) } // appendLine writes one marshalled JSON line to an append-only log, // creating the file and its parent directory on first use. The caller // holds the store's lock. func appendLine(path string, line []byte) (err error) { if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return fmt.Errorf("mkdir %s: %w", filepath.Dir(path), err) } f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return fmt.Errorf("open %s: %w", path, err) } defer func() { if cerr := f.Close(); cerr != nil && err == nil { err = fmt.Errorf("close %s: %w", path, cerr) } }() if _, err := f.Write(line); err != nil { return fmt.Errorf("write %s: %w", path, err) } return nil }