Files
nuntius/internal/storage/submissions.go
petrbalvin 3a38f00dc0
Test / test (push) Successful in 2m1s
Release / gates (push) Successful in 1m57s
Release / build (amd64, freebsd) (push) Successful in 1m26s
Release / build (amd64, linux) (push) Successful in 1m30s
Release / build (arm64, freebsd) (push) Successful in 1m28s
Release / build (arm64, linux) (push) Successful in 1m49s
Release / build (loong64, linux) (push) Successful in 1m30s
Release / build (riscv64, linux) (push) Successful in 1m29s
Release / release (push) Successful in 41s
feat: contact form backend for linux and freebsd servers
Assisted-by: GLM 5.3 Flash
2026-09-29 00:32:56 +02:00

86 lines
2.3 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
}