package db import ( "database/sql" "encoding/json" "fmt" "time" ) func parseTime(value string) time.Time { t, err := time.Parse(time.RFC3339, value) if err != nil { t, _ = time.Parse("2006-01-02 15:04:05", value) } return t } func encodeJSONStrings(values []string) (string, error) { if values == nil { values = []string{} } data, err := json.Marshal(values) if err != nil { return "", err } return string(data), nil } func decodeJSONStrings(raw string) ([]string, error) { if raw == "" { return []string{}, nil } var values []string if err := json.Unmarshal([]byte(raw), &values); err != nil { return nil, err } return values, nil } func scanSambaShare(row interface { Scan(dest ...any) error }) (SambaShare, error) { var share SambaShare var readOnly, guestOK int var validUsers, validGroups, invalidUsers, createdAt, updatedAt string if err := row.Scan( &share.ID, &share.Name, &share.Path, &share.Comment, &readOnly, &guestOK, &validUsers, &validGroups, &invalidUsers, &createdAt, &updatedAt, ); err != nil { return SambaShare{}, err } share.ReadOnly = readOnly != 0 share.GuestOK = guestOK != 0 var err error share.ValidUsers, err = decodeJSONStrings(validUsers) if err != nil { return SambaShare{}, err } share.ValidGroups, err = decodeJSONStrings(validGroups) if err != nil { return SambaShare{}, err } share.InvalidUsers, err = decodeJSONStrings(invalidUsers) if err != nil { return SambaShare{}, err } share.CreatedAt = parseTime(createdAt) share.UpdatedAt = parseTime(updatedAt) return share, nil } func (d *DB) MarkDirty(module string) error { _, err := d.conn.Exec( `INSERT INTO dirty_modules (module, marked_at) VALUES (?, datetime('now')) ON CONFLICT(module) DO UPDATE SET marked_at = datetime('now')`, module, ) if err != nil { return fmt.Errorf("mark dirty %q: %w", module, err) } return nil } func (d *DB) ClearDirty(module string) error { _, err := d.conn.Exec(`DELETE FROM dirty_modules WHERE module = ?`, module) if err != nil { return fmt.Errorf("clear dirty %q: %w", module, err) } return nil } func (d *DB) IsDirty(module string) (bool, error) { var count int err := d.conn.QueryRow(`SELECT COUNT(1) FROM dirty_modules WHERE module = ?`, module).Scan(&count) if err != nil { return false, fmt.Errorf("is dirty %q: %w", module, err) } return count > 0, nil } func (d *DB) ListDirty() ([]DirtyModule, error) { rows, err := d.conn.Query(`SELECT module, marked_at FROM dirty_modules ORDER BY marked_at ASC`) if err != nil { return nil, fmt.Errorf("list dirty: %w", err) } defer rows.Close() var modules []DirtyModule for rows.Next() { var module DirtyModule var markedAt string if err := rows.Scan(&module.Module, &markedAt); err != nil { return nil, fmt.Errorf("scan dirty: %w", err) } module.MarkedAt = parseTime(markedAt) modules = append(modules, module) } if err := rows.Err(); err != nil { return nil, err } return modules, nil } func (d *DB) AppendApplyLog(module, message string, success bool) error { successInt := 0 if success { successInt = 1 } _, err := d.conn.Exec( `INSERT INTO apply_log (module, message, success, created_at) VALUES (?, ?, ?, datetime('now'))`, module, message, successInt, ) if err != nil { return fmt.Errorf("append apply log: %w", err) } return nil } func (d *DB) ListApplyLog(limit int) ([]ApplyLogEntry, error) { if limit <= 0 { limit = 100 } rows, err := d.conn.Query( `SELECT id, module, message, success, created_at FROM apply_log ORDER BY id DESC LIMIT ?`, limit, ) if err != nil { return nil, fmt.Errorf("list apply log: %w", err) } defer rows.Close() var entries []ApplyLogEntry for rows.Next() { var entry ApplyLogEntry var success int var createdAt string if err := rows.Scan(&entry.ID, &entry.Module, &entry.Message, &success, &createdAt); err != nil { return nil, fmt.Errorf("scan apply log: %w", err) } entry.Success = success != 0 entry.CreatedAt = parseTime(createdAt) entries = append(entries, entry) } return entries, rows.Err() } func (d *DB) ListSambaShares() ([]SambaShare, error) { rows, err := d.conn.Query(` SELECT id, name, path, comment, read_only, guest_ok, valid_users, valid_groups, invalid_users, created_at, updated_at FROM samba_shares ORDER BY name ASC`) if err != nil { return nil, fmt.Errorf("list samba shares: %w", err) } defer rows.Close() var shares []SambaShare for rows.Next() { share, err := scanSambaShare(rows) if err != nil { return nil, fmt.Errorf("scan samba share: %w", err) } shares = append(shares, share) } return shares, rows.Err() } func (d *DB) GetSambaShare(id int64) (SambaShare, error) { row := d.conn.QueryRow(` SELECT id, name, path, comment, read_only, guest_ok, valid_users, valid_groups, invalid_users, created_at, updated_at FROM samba_shares WHERE id = ?`, id) share, err := scanSambaShare(row) if err == sql.ErrNoRows { return SambaShare{}, fmt.Errorf("samba share not found") } if err != nil { return SambaShare{}, fmt.Errorf("get samba share: %w", err) } return share, nil } func (d *DB) CreateSambaShare(share SambaShare) (SambaShare, error) { validUsers, err := encodeJSONStrings(share.ValidUsers) if err != nil { return SambaShare{}, err } validGroups, err := encodeJSONStrings(share.ValidGroups) if err != nil { return SambaShare{}, err } invalidUsers, err := encodeJSONStrings(share.InvalidUsers) if err != nil { return SambaShare{}, err } readOnly := 0 if share.ReadOnly { readOnly = 1 } guestOK := 0 if share.GuestOK { guestOK = 1 } result, err := d.conn.Exec(` INSERT INTO samba_shares (name, path, comment, read_only, guest_ok, valid_users, valid_groups, invalid_users) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, share.Name, share.Path, share.Comment, readOnly, guestOK, validUsers, validGroups, invalidUsers, ) if err != nil { return SambaShare{}, fmt.Errorf("create samba share: %w", err) } id, err := result.LastInsertId() if err != nil { return SambaShare{}, fmt.Errorf("last insert id: %w", err) } return d.GetSambaShare(id) } func (d *DB) UpdateSambaShare(id int64, share SambaShare) (SambaShare, error) { validUsers, err := encodeJSONStrings(share.ValidUsers) if err != nil { return SambaShare{}, err } validGroups, err := encodeJSONStrings(share.ValidGroups) if err != nil { return SambaShare{}, err } invalidUsers, err := encodeJSONStrings(share.InvalidUsers) if err != nil { return SambaShare{}, err } readOnly := 0 if share.ReadOnly { readOnly = 1 } guestOK := 0 if share.GuestOK { guestOK = 1 } result, err := d.conn.Exec(` UPDATE samba_shares SET name = ?, path = ?, comment = ?, read_only = ?, guest_ok = ?, valid_users = ?, valid_groups = ?, invalid_users = ?, updated_at = datetime('now') WHERE id = ?`, share.Name, share.Path, share.Comment, readOnly, guestOK, validUsers, validGroups, invalidUsers, id, ) if err != nil { return SambaShare{}, fmt.Errorf("update samba share: %w", err) } rows, err := result.RowsAffected() if err != nil { return SambaShare{}, err } if rows == 0 { return SambaShare{}, fmt.Errorf("samba share not found") } return d.GetSambaShare(id) } func (d *DB) DeleteSambaShare(id int64) error { result, err := d.conn.Exec(`DELETE FROM samba_shares WHERE id = ?`, id) if err != nil { return fmt.Errorf("delete samba share: %w", err) } rows, err := result.RowsAffected() if err != nil { return err } if rows == 0 { return fmt.Errorf("samba share not found") } return nil }