初始化蓝鸟文件快递柜源码与部署文档

This commit is contained in:
lhc committed 2026-10-05 23:41:07 +08:00
commit 5643d22242
70 files changed
+18669

No files matched your search

+143
View File
@@ -0,0 +1,143 @@
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<?", a.now().Unix()-int64(s.AuditRetentionDays)*86400); e != nil {
return e
}
var count, bytes int64
if e = tx.QueryRow("SELECT records,content_bytes FROM audit_usage WHERE id=1").Scan(&count, &bytes); e != nil {
return e
}
if count > 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()
}