Files
VoCat/internal/store/settings.go
T
2026-08-11 22:56:19 +08:00

743 lines
23 KiB
Go

package store
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"time"
)
func DefaultNotificationSensitiveFields(channel string) []string {
switch strings.ToLower(strings.TrimSpace(channel)) {
case "telegram":
return []string{"bot_token"}
case "email":
return []string{"password"}
case "webhook":
return []string{"secret"}
case "pushplus":
return []string{"token"}
case "wecom":
return []string{"urls"}
default:
return nil
}
}
func (s *Store) UpsertNotificationSetting(
ctx context.Context,
value NotificationSetting,
) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin notification setting update: %w", err)
}
defer tx.Rollback()
if err := upsertNotificationSetting(ctx, tx, value); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit notification setting update: %w", err)
}
return nil
}
func upsertNotificationSetting(
ctx context.Context,
executor contextQueryExecer,
value NotificationSetting,
) error {
value.Channel = strings.ToLower(strings.TrimSpace(value.Channel))
if value.Channel == "" {
return errors.New("notification channel is required")
}
config, err := normalizeJSONObject(value.Config)
if err != nil {
return fmt.Errorf("normalize %s notification config: %w", value.Channel, err)
}
current, currentErr := notificationSetting(executor.QueryRowContext(
ctx,
notificationSettingSelect+` WHERE channel = ?`,
value.Channel,
))
if currentErr != nil && !errors.Is(currentErr, ErrNotFound) {
return fmt.Errorf("read %s notification setting before update: %w", value.Channel, currentErr)
}
fields := uniqueNonemptyStrings(
DefaultNotificationSensitiveFields(value.Channel),
value.SensitiveFields,
)
if currentErr == nil {
fields = uniqueNonemptyStrings(fields, current.SensitiveFields)
config, err = mergeJSONSecrets(config, current.Config, fields)
if err != nil {
return fmt.Errorf("preserve %s notification secrets: %w", value.Channel, err)
}
}
fieldsJSON, err := json.Marshal(fields)
if err != nil {
return fmt.Errorf("encode notification sensitive fields: %w", err)
}
now := time.Now().UTC()
createdAt := value.CreatedAt
if createdAt.IsZero() {
createdAt = now
}
updatedAt := value.UpdatedAt
if updatedAt.IsZero() {
updatedAt = now
}
_, err = executor.ExecContext(ctx, `
INSERT INTO notification_settings (
channel, enabled, config_json, sensitive_fields_json,
created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(channel) DO UPDATE SET
enabled = excluded.enabled,
config_json = excluded.config_json,
sensitive_fields_json = excluded.sensitive_fields_json,
updated_at = excluded.updated_at
`,
value.Channel, boolInt(value.Enabled), string(config),
string(fieldsJSON), createdAt.Unix(), updatedAt.Unix(),
)
if err != nil {
return fmt.Errorf("upsert %s notification setting: %w", value.Channel, err)
}
return nil
}
// SaveNotificationSettings applies a multi-channel settings form atomically.
func (s *Store) SaveNotificationSettings(
ctx context.Context,
values []NotificationSetting,
) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin notification settings batch: %w", err)
}
defer tx.Rollback()
seen := make(map[string]struct{}, len(values))
for index, value := range values {
channel := strings.ToLower(strings.TrimSpace(value.Channel))
if _, duplicate := seen[channel]; duplicate {
return fmt.Errorf("duplicate notification channel %q", channel)
}
if err := upsertNotificationSetting(ctx, tx, value); err != nil {
return fmt.Errorf("save notification channel %d: %w", index, err)
}
seen[channel] = struct{}{}
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit notification settings batch: %w", err)
}
return nil
}
func (s *Store) NotificationSetting(
ctx context.Context,
channel string,
) (NotificationSetting, error) {
return notificationSetting(s.db.QueryRowContext(
ctx,
notificationSettingSelect+` WHERE channel = ?`,
strings.ToLower(strings.TrimSpace(channel)),
))
}
func (s *Store) ListNotificationSettings(ctx context.Context) ([]NotificationSetting, error) {
rows, err := s.db.QueryContext(ctx, notificationSettingSelect+` ORDER BY channel`)
if err != nil {
return nil, fmt.Errorf("list notification settings: %w", err)
}
defer rows.Close()
values := make([]NotificationSetting, 0)
for rows.Next() {
value, err := notificationSetting(rows)
if err != nil {
return nil, fmt.Errorf("scan notification setting: %w", err)
}
values = append(values, value)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate notification settings: %w", err)
}
return values, nil
}
func (s *Store) DeleteNotificationSetting(ctx context.Context, channel string) error {
result, err := s.db.ExecContext(
ctx,
`DELETE FROM notification_settings WHERE channel = ?`,
strings.ToLower(strings.TrimSpace(channel)),
)
if err != nil {
return fmt.Errorf("delete notification setting %q: %w", channel, err)
}
return requireAffected(result)
}
const notificationSettingSelect = `
SELECT channel, enabled, config_json, sensitive_fields_json,
created_at, updated_at
FROM notification_settings`
func notificationSetting(row rowScanner) (NotificationSetting, error) {
var value NotificationSetting
var enabled int
var config, fields string
var createdAt, updatedAt int64
err := row.Scan(
&value.Channel, &enabled, &config, &fields, &createdAt, &updatedAt,
)
if errors.Is(err, sql.ErrNoRows) {
return NotificationSetting{}, ErrNotFound
}
if err != nil {
return NotificationSetting{}, err
}
if err := json.Unmarshal([]byte(fields), &value.SensitiveFields); err != nil {
return NotificationSetting{}, fmt.Errorf("decode sensitive fields: %w", err)
}
value.Enabled = enabled != 0
value.Config = []byte(config)
value.CreatedAt = time.Unix(createdAt, 0).UTC()
value.UpdatedAt = time.Unix(updatedAt, 0).UTC()
return value, nil
}
func uniqueNonemptyStrings(groups ...[]string) []string {
seen := make(map[string]struct{})
for _, group := range groups {
for _, item := range group {
item = strings.TrimSpace(item)
if item != "" {
seen[item] = struct{}{}
}
}
}
result := make([]string, 0, len(seen))
for item := range seen {
result = append(result, item)
}
sort.Strings(result)
return result
}
func (s *Store) UpsertAppSetting(ctx context.Context, value AppSetting) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin app setting update: %w", err)
}
defer tx.Rollback()
if err := upsertAppSetting(ctx, tx, value); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit app setting update: %w", err)
}
return nil
}
func upsertAppSetting(
ctx context.Context,
executor contextQueryExecer,
value AppSetting,
) error {
value.Key = strings.TrimSpace(value.Key)
if value.Key == "" {
return errors.New("app setting key is required")
}
normalized, err := normalizeJSONValue(value.Value)
if err != nil {
return fmt.Errorf("normalize app setting %q: %w", value.Key, err)
}
if value.Sensitive && maskedJSONValue(normalized) {
current, currentErr := appSetting(executor.QueryRowContext(
ctx,
appSettingSelect+` WHERE key = ?`,
value.Key,
))
switch {
case currentErr == nil:
normalized = current.Value
case errors.Is(currentErr, ErrNotFound):
return fmt.Errorf("new sensitive app setting %q requires a value", value.Key)
default:
return fmt.Errorf("read app setting before update: %w", currentErr)
}
}
updatedAt := value.UpdatedAt
if updatedAt.IsZero() {
updatedAt = time.Now().UTC()
}
_, err = executor.ExecContext(ctx, `
INSERT INTO app_settings (key, value_json, sensitive, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(key) DO UPDATE SET
value_json = excluded.value_json,
sensitive = excluded.sensitive,
updated_at = excluded.updated_at
`, value.Key, string(normalized), boolInt(value.Sensitive), updatedAt.Unix())
if err != nil {
return fmt.Errorf("upsert app setting %q: %w", value.Key, err)
}
return nil
}
func (s *Store) AppSetting(ctx context.Context, key string) (AppSetting, error) {
return appSetting(s.db.QueryRowContext(
ctx,
appSettingSelect+` WHERE key = ?`,
strings.TrimSpace(key),
))
}
func (s *Store) ListAppSettings(ctx context.Context) ([]AppSetting, error) {
rows, err := s.db.QueryContext(ctx, appSettingSelect+` ORDER BY key`)
if err != nil {
return nil, fmt.Errorf("list app settings: %w", err)
}
defer rows.Close()
values := make([]AppSetting, 0)
for rows.Next() {
value, err := appSetting(rows)
if err != nil {
return nil, fmt.Errorf("scan app setting: %w", err)
}
values = append(values, value)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate app settings: %w", err)
}
return values, nil
}
func (s *Store) DeleteAppSetting(ctx context.Context, key string) error {
result, err := s.db.ExecContext(ctx, `DELETE FROM app_settings WHERE key = ?`, key)
if err != nil {
return fmt.Errorf("delete app setting %q: %w", key, err)
}
return requireAffected(result)
}
const appSettingSelect = `
SELECT key, value_json, sensitive, updated_at
FROM app_settings`
func appSetting(row rowScanner) (AppSetting, error) {
var value AppSetting
var sensitive int
var raw string
var updatedAt int64
err := row.Scan(&value.Key, &raw, &sensitive, &updatedAt)
if errors.Is(err, sql.ErrNoRows) {
return AppSetting{}, ErrNotFound
}
if err != nil {
return AppSetting{}, err
}
value.Value = []byte(raw)
value.Sensitive = sensitive != 0
value.UpdatedAt = time.Unix(updatedAt, 0).UTC()
return value, nil
}
func maskedJSONValue(value json.RawMessage) bool {
if bytes.Equal(bytes.TrimSpace(value), []byte(`null`)) {
return true
}
var text string
if json.Unmarshal(value, &text) == nil {
return text == "" || text == SecretMask
}
return false
}
func (s *Store) UpsertCardPolicy(ctx context.Context, value CardPolicy) error {
value.ICCID = strings.TrimSpace(value.ICCID)
value.CustomPhoneNumber = strings.TrimSpace(value.CustomPhoneNumber)
if value.ICCID == "" {
return errors.New("card policy ICCID is required")
}
value.IPVersion = strings.ToUpper(strings.TrimSpace(value.IPVersion))
switch value.IPVersion {
case "", "IP", "IPV6", "IPV4V6":
default:
return fmt.Errorf("unsupported card policy IP version %q", value.IPVersion)
}
now := time.Now().UTC()
createdAt := value.CreatedAt
if createdAt.IsZero() {
createdAt = now
}
updatedAt := value.UpdatedAt
if updatedAt.IsZero() {
updatedAt = now
}
_, err := s.db.ExecContext(ctx, `
INSERT INTO card_policies (
iccid, network_enabled, vowifi_enabled, airplane_enabled,
apn, ip_version, custom_phone_number, source, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(iccid) DO UPDATE SET
network_enabled = excluded.network_enabled,
vowifi_enabled = excluded.vowifi_enabled,
airplane_enabled = excluded.airplane_enabled,
apn = excluded.apn,
ip_version = excluded.ip_version,
custom_phone_number = excluded.custom_phone_number,
source = excluded.source,
updated_at = excluded.updated_at
`,
value.ICCID, boolInt(value.NetworkEnabled), boolInt(value.VoWiFiEnabled),
boolInt(value.AirplaneEnabled), value.APN, value.IPVersion,
value.CustomPhoneNumber, value.Source, createdAt.Unix(), updatedAt.Unix(),
)
if err != nil {
return fmt.Errorf("upsert card policy %q: %w", value.ICCID, err)
}
return nil
}
func (s *Store) CardPolicy(ctx context.Context, iccid string) (CardPolicy, error) {
return cardPolicy(s.db.QueryRowContext(
ctx,
cardPolicySelect+` WHERE iccid = ?`,
strings.TrimSpace(iccid),
))
}
func (s *Store) ListCardPolicies(ctx context.Context) ([]CardPolicy, error) {
rows, err := s.db.QueryContext(ctx, cardPolicySelect+` ORDER BY iccid`)
if err != nil {
return nil, fmt.Errorf("list card policies: %w", err)
}
defer rows.Close()
values := make([]CardPolicy, 0)
for rows.Next() {
value, err := cardPolicy(rows)
if err != nil {
return nil, fmt.Errorf("scan card policy: %w", err)
}
values = append(values, value)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate card policies: %w", err)
}
return values, nil
}
func (s *Store) DeleteCardPolicy(ctx context.Context, iccid string) error {
result, err := s.db.ExecContext(ctx, `DELETE FROM card_policies WHERE iccid = ?`, iccid)
if err != nil {
return fmt.Errorf("delete card policy %q: %w", iccid, err)
}
return requireAffected(result)
}
const cardPolicySelect = `
SELECT iccid, network_enabled, vowifi_enabled, airplane_enabled,
apn, ip_version, custom_phone_number, source, created_at, updated_at
FROM card_policies`
func cardPolicy(row rowScanner) (CardPolicy, error) {
var value CardPolicy
var networkEnabled, vowifiEnabled, airplaneEnabled int
var createdAt, updatedAt int64
err := row.Scan(
&value.ICCID, &networkEnabled, &vowifiEnabled, &airplaneEnabled,
&value.APN, &value.IPVersion, &value.CustomPhoneNumber, &value.Source, &createdAt, &updatedAt,
)
if errors.Is(err, sql.ErrNoRows) {
return CardPolicy{}, ErrNotFound
}
if err != nil {
return CardPolicy{}, err
}
value.NetworkEnabled = networkEnabled != 0
value.VoWiFiEnabled = vowifiEnabled != 0
value.AirplaneEnabled = airplaneEnabled != 0
value.CreatedAt = time.Unix(createdAt, 0).UTC()
value.UpdatedAt = time.Unix(updatedAt, 0).UTC()
return value, nil
}
func (s *Store) UpsertCardAPNProfile(ctx context.Context, value CardAPNProfile) (CardAPNProfile, error) {
value.ICCID = strings.TrimSpace(value.ICCID)
value.APN = strings.TrimSpace(value.APN)
value.IPVersion = strings.ToUpper(strings.TrimSpace(value.IPVersion))
if value.ICCID == "" || value.APN == "" {
return CardAPNProfile{}, errors.New("card APN profile ICCID and APN are required")
}
if value.IPVersion == "" {
value.IPVersion = "IPV4V6"
}
switch value.IPVersion {
case "IP", "IPV6", "IPV4V6":
default:
return CardAPNProfile{}, fmt.Errorf("unsupported card APN profile IP version %q", value.IPVersion)
}
value.RoamingIPVersion = strings.ToUpper(strings.TrimSpace(value.RoamingIPVersion))
if value.RoamingIPVersion == "" {
value.RoamingIPVersion = "IP"
}
switch value.RoamingIPVersion {
case "IP", "IPV6", "IPV4V6":
default:
return CardAPNProfile{}, fmt.Errorf("unsupported card APN roaming IP version %q", value.RoamingIPVersion)
}
value.AuthType = strings.ToUpper(strings.TrimSpace(value.AuthType))
if value.AuthType == "" {
value.AuthType = "NONE"
}
switch value.AuthType {
case "NONE", "PAP", "CHAP", "PAP_OR_CHAP":
default:
return CardAPNProfile{}, fmt.Errorf("unsupported card APN authentication type %q", value.AuthType)
}
value.Username = strings.TrimSpace(value.Username)
value.Proxy = strings.TrimSpace(value.Proxy)
value.MCC = strings.TrimSpace(value.MCC)
value.MNC = strings.TrimSpace(value.MNC)
now := time.Now().UTC().Unix()
var createdAt, updatedAt int64
err := s.db.QueryRowContext(ctx, `
INSERT INTO card_apn_profiles (
iccid, apn, username, password, proxy, mcc, mnc,
ip_version, roaming_ip_version, auth_type, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(iccid, apn, ip_version) DO UPDATE SET
username = excluded.username, password = excluded.password,
proxy = excluded.proxy, mcc = excluded.mcc, mnc = excluded.mnc,
roaming_ip_version = excluded.roaming_ip_version,
auth_type = excluded.auth_type, updated_at = excluded.updated_at
RETURNING id, iccid, apn, username, password, proxy, mcc, mnc,
ip_version, roaming_ip_version, auth_type, created_at, updated_at
`, value.ICCID, value.APN, value.Username, value.Password, value.Proxy, value.MCC, value.MNC,
value.IPVersion, value.RoamingIPVersion, value.AuthType, now, now).Scan(
&value.ID, &value.ICCID, &value.APN, &value.Username, &value.Password,
&value.Proxy, &value.MCC, &value.MNC, &value.IPVersion,
&value.RoamingIPVersion, &value.AuthType, &createdAt, &updatedAt,
)
if err != nil {
return CardAPNProfile{}, fmt.Errorf("upsert card APN profile: %w", err)
}
value.CreatedAt = time.Unix(createdAt, 0).UTC()
value.UpdatedAt = time.Unix(updatedAt, 0).UTC()
return value, nil
}
func (s *Store) ListCardAPNProfiles(ctx context.Context, iccid string) ([]CardAPNProfile, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT id, iccid, apn, username, password, proxy, mcc, mnc,
ip_version, roaming_ip_version, auth_type, created_at, updated_at
FROM card_apn_profiles WHERE iccid = ? ORDER BY id
`, strings.TrimSpace(iccid))
if err != nil {
return nil, fmt.Errorf("list card APN profiles: %w", err)
}
defer rows.Close()
values := make([]CardAPNProfile, 0)
for rows.Next() {
var value CardAPNProfile
var createdAt, updatedAt int64
if err := rows.Scan(&value.ID, &value.ICCID, &value.APN, &value.Username,
&value.Password, &value.Proxy, &value.MCC, &value.MNC, &value.IPVersion,
&value.RoamingIPVersion, &value.AuthType, &createdAt, &updatedAt); err != nil {
return nil, fmt.Errorf("scan card APN profile: %w", err)
}
value.CreatedAt = time.Unix(createdAt, 0).UTC()
value.UpdatedAt = time.Unix(updatedAt, 0).UTC()
values = append(values, value)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate card APN profiles: %w", err)
}
return values, nil
}
func (s *Store) CardAPNProfileByAPN(ctx context.Context, iccid, apn, ipVersion string) (CardAPNProfile, error) {
profiles, err := s.ListCardAPNProfiles(ctx, iccid)
if err != nil {
return CardAPNProfile{}, err
}
for _, profile := range profiles {
if strings.EqualFold(profile.APN, strings.TrimSpace(apn)) &&
strings.EqualFold(profile.IPVersion, strings.TrimSpace(ipVersion)) {
return profile, nil
}
}
return CardAPNProfile{}, ErrNotFound
}
func (s *Store) UpdateCardAPNProfile(ctx context.Context, value CardAPNProfile) (CardAPNProfile, error) {
value.ICCID = strings.TrimSpace(value.ICCID)
value.APN = strings.TrimSpace(value.APN)
value.Username = strings.TrimSpace(value.Username)
value.Proxy = strings.TrimSpace(value.Proxy)
value.MCC = strings.TrimSpace(value.MCC)
value.MNC = strings.TrimSpace(value.MNC)
value.IPVersion = strings.ToUpper(strings.TrimSpace(value.IPVersion))
value.RoamingIPVersion = strings.ToUpper(strings.TrimSpace(value.RoamingIPVersion))
value.AuthType = strings.ToUpper(strings.TrimSpace(value.AuthType))
if value.ID < 1 || value.ICCID == "" || value.APN == "" {
return CardAPNProfile{}, errors.New("card APN profile ID, ICCID, and APN are required")
}
now := time.Now().UTC().Unix()
var createdAt, updatedAt int64
err := s.db.QueryRowContext(ctx, `
UPDATE card_apn_profiles SET
apn = ?, username = ?, password = ?, proxy = ?, mcc = ?, mnc = ?,
ip_version = ?, roaming_ip_version = ?, auth_type = ?, updated_at = ?
WHERE id = ? AND iccid = ?
RETURNING id, iccid, apn, username, password, proxy, mcc, mnc,
ip_version, roaming_ip_version, auth_type, created_at, updated_at
`, value.APN, value.Username, value.Password, value.Proxy, value.MCC, value.MNC,
value.IPVersion, value.RoamingIPVersion, value.AuthType, now, value.ID, value.ICCID).Scan(
&value.ID, &value.ICCID, &value.APN, &value.Username, &value.Password,
&value.Proxy, &value.MCC, &value.MNC, &value.IPVersion,
&value.RoamingIPVersion, &value.AuthType, &createdAt, &updatedAt,
)
if errors.Is(err, sql.ErrNoRows) {
return CardAPNProfile{}, ErrNotFound
}
if err != nil {
return CardAPNProfile{}, fmt.Errorf("update card APN profile: %w", err)
}
value.CreatedAt = time.Unix(createdAt, 0).UTC()
value.UpdatedAt = time.Unix(updatedAt, 0).UTC()
return value, nil
}
func (s *Store) DeleteCardAPNProfile(ctx context.Context, iccid string, id int64) error {
result, err := s.db.ExecContext(ctx, `DELETE FROM card_apn_profiles WHERE iccid = ? AND id = ?`, strings.TrimSpace(iccid), id)
if err != nil {
return fmt.Errorf("delete card APN profile: %w", err)
}
return requireAffected(result)
}
func (s *Store) UpsertTrafficBucket(ctx context.Context, value TrafficBucket) error {
return s.writeTrafficBucket(ctx, value, false)
}
// AddTrafficBucket atomically accumulates counters for concurrent collectors.
func (s *Store) AddTrafficBucket(ctx context.Context, value TrafficBucket) error {
return s.writeTrafficBucket(ctx, value, true)
}
func (s *Store) writeTrafficBucket(
ctx context.Context,
value TrafficBucket,
accumulate bool,
) error {
value.DeviceID = strings.TrimSpace(value.DeviceID)
value.Bucket = strings.TrimSpace(value.Bucket)
if value.DeviceID == "" || value.Bucket == "" {
return errors.New("traffic bucket device id and bucket are required")
}
if value.PeriodStart.IsZero() {
return errors.New("traffic bucket period start is required")
}
if value.RXBytes < 0 || value.TXBytes < 0 {
return errors.New("traffic byte counters cannot be negative")
}
update := `
rx_bytes = excluded.rx_bytes,
tx_bytes = excluded.tx_bytes`
if accumulate {
update = `
rx_bytes = traffic_buckets.rx_bytes + excluded.rx_bytes,
tx_bytes = traffic_buckets.tx_bytes + excluded.tx_bytes`
}
_, err := s.db.ExecContext(ctx, `
INSERT INTO traffic_buckets (
device_id, bucket, period_start, rx_bytes, tx_bytes
) VALUES (?, ?, ?, ?, ?)
ON CONFLICT(device_id, bucket, period_start) DO UPDATE SET`+update,
value.DeviceID, value.Bucket, value.PeriodStart.UTC().Unix(),
value.RXBytes, value.TXBytes,
)
if err != nil {
return fmt.Errorf("write traffic bucket: %w", err)
}
return nil
}
func (s *Store) ListTrafficBuckets(
ctx context.Context,
filter TrafficFilter,
) ([]TrafficBucket, error) {
clauses := make([]string, 0, 4)
args := make([]any, 0, 5)
if filter.DeviceID != "" {
clauses = append(clauses, `device_id = ?`)
args = append(args, filter.DeviceID)
}
if filter.Bucket != "" {
clauses = append(clauses, `bucket = ?`)
args = append(args, filter.Bucket)
}
if !filter.Since.IsZero() {
clauses = append(clauses, `period_start >= ?`)
args = append(args, filter.Since.UTC().Unix())
}
if !filter.Until.IsZero() {
clauses = append(clauses, `period_start < ?`)
args = append(args, filter.Until.UTC().Unix())
}
query := `
SELECT device_id, bucket, period_start, rx_bytes, tx_bytes
FROM traffic_buckets`
if len(clauses) > 0 {
query += ` WHERE ` + strings.Join(clauses, ` AND `)
}
query += ` ORDER BY period_start ASC, device_id LIMIT ?`
args = append(args, normalizedLimit(filter.Limit))
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("list traffic buckets: %w", err)
}
defer rows.Close()
values := make([]TrafficBucket, 0)
for rows.Next() {
var value TrafficBucket
var periodStart int64
if err := rows.Scan(
&value.DeviceID, &value.Bucket, &periodStart,
&value.RXBytes, &value.TXBytes,
); err != nil {
return nil, fmt.Errorf("scan traffic bucket: %w", err)
}
value.PeriodStart = time.Unix(periodStart, 0).UTC()
values = append(values, value)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate traffic buckets: %w", err)
}
return values, nil
}
func (s *Store) DeleteTrafficBefore(ctx context.Context, before time.Time) (int64, error) {
result, err := s.db.ExecContext(
ctx,
`DELETE FROM traffic_buckets WHERE period_start < ?`,
before.UTC().Unix(),
)
if err != nil {
return 0, fmt.Errorf("delete old traffic buckets: %w", err)
}
affected, err := result.RowsAffected()
if err != nil {
return 0, fmt.Errorf("read deleted traffic bucket count: %w", err)
}
return affected, nil
}