Files
selfpost/internal/store/settings.go
T
mixandClaude Opus 5.5 ad3d834392 store, web: cite the source of what they rely on (assumptions part D)
Comments only, no behaviour changed. modernc.org/sqlite v1.60.1 (the
_pragma list on every connection, busy_timeout first; extended result
codes; a deferred BEGIN; multi-statement Exec) and SQLite 3.53.4's
documentation (transactions, WAL, foreign keys, NOCASE, upsert, julianday,
partial indexes); htmx 2.0.4 src/htmx.js; the Fetch and HTML standards for
Sec-Fetch-Site, Origin, SameSite, __Host-, CSP and defer order; Go's
html/template. Comments the source contradicts are corrected - the driver
does not serialise writes (SetMaxOpenConns(1) does), htmx:load fires per
swapped element, no-store does not keep a page out of history.

Left for their own commits: D-S3 a migration two processes can both run,
D-S20 recipient case, D-B3 Origin: null under Referrer-Policy: no-referrer,
D-H5/H6/H7/H9 (audit M11, stage 4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:36:26 +03:00

171 lines
5.6 KiB
Go

package store
import (
"database/sql"
"errors"
"fmt"
"strconv"
)
const (
// SendLogRetentionDaysKey is the settings table key for delivery-journal
// retention (architecture.md § Persistence).
SendLogRetentionDaysKey = "send_log_retention_days"
SendLogRetentionDaysMin = 7
SendLogRetentionDaysMax = 365
// SendLogRetentionDaysDefault matches SEND_LOG_RETENTION_DAYS when unset.
SendLogRetentionDaysDefault = 90
)
// ErrSendLogRetentionDaysOutOfRange is returned when retention is outside the
// allowed panel range.
var ErrSendLogRetentionDaysOutOfRange = errors.New("send log retention days out of range")
// ValidateSendLogRetentionDays checks the panel-allowed retention window.
func ValidateSendLogRetentionDays(days int) error {
if days < SendLogRetentionDaysMin || days > SendLogRetentionDaysMax {
return fmt.Errorf("%w: must be between %d and %d days", ErrSendLogRetentionDaysOutOfRange, SendLogRetentionDaysMin, SendLogRetentionDaysMax)
}
return nil
}
func sendLogRetentionFallback(envDefault int) int {
if envDefault > 0 {
if err := ValidateSendLogRetentionDays(envDefault); err == nil {
return envDefault
}
}
return SendLogRetentionDaysDefault
}
// GetSendLogRetentionDays returns the effective retention window. When the
// setting is missing or invalid, envDefault is used (guide § Environment
// variables: SEND_LOG_RETENTION_DAYS).
func (s *Store) GetSendLogRetentionDays(envDefault int) (int, error) {
raw, err := s.GetSetting(SendLogRetentionDaysKey)
if err != nil {
return 0, err
}
if raw == "" {
return sendLogRetentionFallback(envDefault), nil
}
days, err := strconv.Atoi(raw)
if err != nil || ValidateSendLogRetentionDays(days) != nil {
return sendLogRetentionFallback(envDefault), nil
}
return days, nil
}
// SetSendLogRetentionDays persists the panel-configured retention window.
func (s *Store) SetSendLogRetentionDays(days int) error {
if err := ValidateSendLogRetentionDays(days); err != nil {
return err
}
return s.SetSetting(SendLogRetentionDaysKey, strconv.Itoa(days))
}
// EnsureSendLogRetentionDays seeds the setting from envDefault when it has
// never been written (first panel start after upgrade).
func (s *Store) EnsureSendLogRetentionDays(envDefault int) error {
raw, err := s.GetSetting(SendLogRetentionDaysKey)
if err != nil {
return err
}
if raw != "" {
return nil
}
return s.SetSendLogRetentionDays(sendLogRetentionFallback(envDefault))
}
const (
// InboundSpamLogRetentionDaysKey is the settings table key for the
// inbound antispam journal's retention. Same range as the send log's.
InboundSpamLogRetentionDaysKey = "inbound_spam_log_retention_days"
InboundSpamLogRetentionDaysMin = 7
InboundSpamLogRetentionDaysMax = 365
// InboundSpamLogRetentionDaysDefault matches
// INBOUND_SPAM_LOG_RETENTION_DAYS when unset.
InboundSpamLogRetentionDaysDefault = 90
)
// ErrInboundSpamLogRetentionDaysOutOfRange is returned when the inbound
// journal's retention is outside the allowed panel range.
var ErrInboundSpamLogRetentionDaysOutOfRange = errors.New("inbound journal retention days out of range")
// ValidateInboundSpamLogRetentionDays checks the panel-allowed window.
func ValidateInboundSpamLogRetentionDays(days int) error {
if days < InboundSpamLogRetentionDaysMin || days > InboundSpamLogRetentionDaysMax {
return fmt.Errorf("%w: must be between %d and %d days", ErrInboundSpamLogRetentionDaysOutOfRange, InboundSpamLogRetentionDaysMin, InboundSpamLogRetentionDaysMax)
}
return nil
}
func inboundSpamLogRetentionFallback(envDefault int) int {
if ValidateInboundSpamLogRetentionDays(envDefault) == nil {
return envDefault
}
return InboundSpamLogRetentionDaysDefault
}
// GetInboundSpamLogRetentionDays returns the effective retention window of
// the inbound journal. When the setting is missing or invalid, envDefault is
// used (INBOUND_SPAM_LOG_RETENTION_DAYS).
func (s *Store) GetInboundSpamLogRetentionDays(envDefault int) (int, error) {
raw, err := s.GetSetting(InboundSpamLogRetentionDaysKey)
if err != nil {
return 0, err
}
days, err := strconv.Atoi(raw)
if err != nil || ValidateInboundSpamLogRetentionDays(days) != nil {
return inboundSpamLogRetentionFallback(envDefault), nil
}
return days, nil
}
// SetInboundSpamLogRetentionDays persists the inbound journal's retention.
func (s *Store) SetInboundSpamLogRetentionDays(days int) error {
if err := ValidateInboundSpamLogRetentionDays(days); err != nil {
return err
}
return s.SetSetting(InboundSpamLogRetentionDaysKey, strconv.Itoa(days))
}
// EnsureInboundSpamLogRetentionDays seeds the setting from envDefault when it
// has never been written.
func (s *Store) EnsureInboundSpamLogRetentionDays(envDefault int) error {
raw, err := s.GetSetting(InboundSpamLogRetentionDaysKey)
if err != nil {
return err
}
if raw != "" {
return nil
}
return s.SetInboundSpamLogRetentionDays(inboundSpamLogRetentionFallback(envDefault))
}
// GetSetting returns a settings value or empty string when missing.
func (s *Store) GetSetting(key string) (string, error) {
var value string
err := s.db.QueryRow("SELECT value FROM settings WHERE key = ?", key).Scan(&value)
if errors.Is(err, sql.ErrNoRows) {
return "", nil
}
if err != nil {
return "", fmt.Errorf("get setting %s: %w", key, err)
}
return value, nil
}
// SetSetting upserts a settings key (https://sqlite.org/lang_upsert.html: on
// a conflict of the key, excluded.value is the value the INSERT brought).
func (s *Store) SetSetting(key, value string) error {
_, err := s.db.Exec(
"INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",
key, value,
)
if err != nil {
return fmt.Errorf("set setting %s: %w", key, err)
}
return nil
}