package app import ( "database/sql" "encoding/json" "errors" "net/http" "strings" ) type RuntimeSettings struct { FileBudget int64 `json:"file_budget"` DataBudget int64 `json:"data_budget"` MinFreeBytes int64 `json:"min_free_bytes"` AuditRetentionDays int `json:"audit_retention_days"` AuditMaxRecords int `json:"audit_max_records"` AuditMaxBytes int64 `json:"audit_max_bytes"` } func (c Config) InitialRuntimeSettings() RuntimeSettings { return RuntimeSettings{c.FileBudget, c.DataBudget, c.MinFreeBytes, c.AuditRetentionDays, c.AuditMaxRecords, c.AuditMaxBytes} } func (s RuntimeSettings) Validate() error { if s.FileBudget < 1 || s.FileBudget > 1<<50 || s.DataBudget > 1<<51 || s.DataBudget-s.FileBudget < 64<<20 || s.MinFreeBytes < 0 || s.MinFreeBytes > 1<<50 { return errors.New("文件预算须为正数,数据预算须另留至少64MiB维护空间") } if s.AuditRetentionDays < 1 || s.AuditRetentionDays > 3650 || s.AuditMaxRecords < 1 || s.AuditMaxRecords > 1000000 || s.AuditMaxBytes < 1024 || s.AuditMaxBytes > 1<<30 || s.AuditMaxBytes > s.DataBudget-s.FileBudget { return errors.New("审计需保留1–3650天,条数1–100万,内容预算1KiB–1GiB且不超过维护空间") } return nil } func readRuntime(q interface{ QueryRow(string, ...any) *sql.Row }) (RuntimeSettings, error) { var text string s := RuntimeSettings{} e := q.QueryRow("SELECT json FROM runtime_settings WHERE id=1").Scan(&text) if e == nil { e = json.Unmarshal([]byte(text), &s) } if e == nil { e = s.Validate() } return s, e } func (a *App) runtimeSettings() (RuntimeSettings, error) { return readRuntime(a.db) } func (a *App) adminRuntime(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } if r.Method == "GET" { s, e := a.runtimeSettings() if e != nil { a.serverError(w, e) return } respond(w, 200, s) return } var settings RuntimeSettings if !decode(w, r, &settings) { return } if e := settings.Validate(); e != nil { fail(w, 400, e.Error()) return } a.mu.Lock() defer a.mu.Unlock() b, e := json.Marshal(settings) if e != nil { a.serverError(w, e) return } tx, e := a.db.Begin() if e != nil { a.serverError(w, e) return } defer tx.Rollback() _, e = tx.Exec("UPDATE runtime_settings SET json=? WHERE id=1", string(b)) if e == nil { e = a.trimAuditTx(tx) } if e == nil { e = tx.Commit() } if e != nil { a.serverError(w, e) return } a.audit("runtime.update", "", a.remoteIP(r)) respond(w, 200, settings) } // The content budget includes UTF-8 fields and a conservative per-record allowance. // SQLite reuses freed pages; this budget is not the physical database-file size. const auditRecordBytes = "length(CAST(action AS BLOB))+length(CAST(target AS BLOB))+length(CAST(source AS BLOB))+96" func (a *App) initializeAuditUsage() error { expression := func(prefix string) string { s := auditRecordBytes for _, name := range []string{"action", "target", "source"} { s = strings.ReplaceAll(s, name, prefix+"."+name) } return s } _, e := a.db.Exec("CREATE TABLE IF NOT EXISTS audit_usage(id INTEGER PRIMARY KEY CHECK(id=1),records INTEGER NOT NULL,content_bytes INTEGER NOT NULL); INSERT OR IGNORE INTO audit_usage SELECT 1,count(*),COALESCE(sum(" + auditRecordBytes + "),0) FROM audit; CREATE TRIGGER IF NOT EXISTS audit_insert_usage AFTER INSERT ON audit BEGIN UPDATE audit_usage SET records=records+1,content_bytes=content_bytes+(" + expression("NEW") + ") WHERE id=1; END; CREATE TRIGGER IF NOT EXISTS audit_delete_usage AFTER DELETE ON audit BEGIN UPDATE audit_usage SET records=records-1,content_bytes=content_bytes-(" + expression("OLD") + ") WHERE id=1; END;") return e } func (a *App) trimAuditTx(tx *sql.Tx) error { s, e := readRuntime(tx) if e != nil { return e } if _, e = tx.Exec("DELETE FROM audit WHERE created int64(s.AuditMaxRecords) { if _, e = tx.Exec("DELETE FROM audit WHERE id IN(SELECT id FROM audit ORDER BY id DESC LIMIT -1 OFFSET ?)", s.AuditMaxRecords); e != nil { return e } } if bytes <= s.AuditMaxBytes { return nil } _, e = tx.Exec("WITH sizes AS (SELECT id,sum("+auditRecordBytes+") OVER(ORDER BY id DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS bytes FROM audit) DELETE FROM audit WHERE id IN(SELECT id FROM sizes WHERE bytes>?)", s.AuditMaxBytes) return e } func (a *App) trimAudit() error { tx, e := a.db.Begin() if e != nil { return e } defer tx.Rollback() if e = a.trimAuditTx(tx); e != nil { return e } return tx.Commit() }