初始化蓝鸟文件快递柜源码与部署文档
This commit is contained in:
commit
5643d22242
70 files changed
+18669
No files matched your search
@@ -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()
|
||||
}
|
||||
Reference in new issue
Block a user