Add nasctl: Go NAS control plane with React frontend
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
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, createdAt, updatedAt string
|
||||
if err := row.Scan(
|
||||
&share.ID,
|
||||
&share.Name,
|
||||
&share.Path,
|
||||
&share.Comment,
|
||||
&readOnly,
|
||||
&guestOK,
|
||||
&validUsers,
|
||||
&validGroups,
|
||||
&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.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, 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, 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
|
||||
}
|
||||
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)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
share.Name, share.Path, share.Comment, readOnly, guestOK, validUsers, validGroups,
|
||||
)
|
||||
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
|
||||
}
|
||||
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 = ?, updated_at = datetime('now')
|
||||
WHERE id = ?`,
|
||||
share.Name, share.Path, share.Comment, readOnly, guestOK, validUsers, validGroups, 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
|
||||
}
|
||||
Reference in New Issue
Block a user