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

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

+247
View File
@@ -0,0 +1,247 @@
package app
import (
"encoding/json"
"golang.org/x/sys/unix"
"net/http"
)
func (a *App) requireAdmin(w http.ResponseWriter, r *http.Request) bool {
if !a.isAdmin(r) {
fail(w, 401, "请先登录管理员")
return false
}
return true
}
func (a *App) adminLogin(w http.ResponseWriter, r *http.Request) {
if !a.allow(r, "login", 5) {
fail(w, 429, "登录尝试过于频繁")
return
}
var in struct {
Username string `json:"username"`
Password string `json:"password"`
}
if !decode(w, r, &in) {
return
}
select {
case a.passwordSlots <- struct{}{}:
default:
fail(w, 429, "登录繁忙")
return
}
defer func() { <-a.passwordSlots }()
var username, hash string
e := a.db.QueryRow("SELECT username,password_hash FROM admins WHERE id=1").Scan(&username, &hash)
valid := e == nil && verifyPassword(in.Password, hash) && in.Username == username
if !valid {
a.audit("admin.login_failed", "", a.remoteIP(r))
fail(w, 401, "账号或密码错误")
return
}
a.mu.Lock()
defer a.mu.Unlock()
if _, e = a.db.Exec("DELETE FROM admin_sessions WHERE expires<=? OR token_hash IN(SELECT token_hash FROM admin_sessions ORDER BY expires DESC LIMIT -1 OFFSET 49)", a.now().Unix()); e != nil {
a.serverError(w, e)
return
}
token := randomToken()
_, e = a.db.Exec("INSERT INTO admin_sessions VALUES(?,?)", tokenHash(token), a.now().Unix()+8*3600)
if e != nil {
a.serverError(w, e)
return
}
a.cookie(w, r, "bb_admin", token, "/api/v1/admin", 8*3600)
a.audit("admin.login", "", a.remoteIP(r))
respond(w, 200, map[string]bool{"authenticated": true})
}
func (a *App) adminLogout(w http.ResponseWriter, r *http.Request) {
_, e := a.db.Exec("DELETE FROM admin_sessions WHERE token_hash=?", tokenHash(cookieValue(r, "bb_admin")))
if e != nil {
a.serverError(w, e)
return
}
a.cookie(w, r, "bb_admin", "", "/api/v1/admin", -1)
respond(w, 200, map[string]bool{"authenticated": false})
}
func (a *App) adminStatus(w http.ResponseWriter, r *http.Request) {
respond(w, 200, map[string]bool{"authenticated": a.isAdmin(r)})
}
func (a *App) adminStats(w http.ResponseWriter, r *http.Request) {
if !a.requireAdmin(w, r) {
return
}
a.mu.Lock()
defer a.mu.Unlock()
used, reserved, meta, e := a.capacity()
if e != nil {
a.serverError(w, e)
return
}
var raw int64
var files, objects, parcels int
queries := []struct {
sql string
dst any
}{{"SELECT COALESCE(sum(size),0) FROM files WHERE object_id IS NOT NULL", &raw}, {"SELECT count(*) FROM files", &files}, {"SELECT count(*) FROM objects", &objects}, {"SELECT count(*) FROM parcels WHERE state='ready'", &parcels}}
for _, q := range queries {
if e = a.db.QueryRow(q.sql).Scan(q.dst); e != nil {
a.serverError(w, e)
return
}
}
limits, e := a.runtimeSettings()
if e != nil {
a.serverError(w, e)
return
}
var disk unix.Statfs_t
if e = unix.Statfs(a.cfg.DataDir, &disk); e != nil {
a.serverError(w, e)
return
}
var auditCount, auditBytes int64
if e = a.db.QueryRow("SELECT records,content_bytes FROM audit_usage WHERE id=1").Scan(&auditCount, &auditBytes); e != nil {
a.serverError(w, e)
return
}
a.transferMu.Lock()
up, down := a.transfers["upload"], a.transfers["download"]
a.transferMu.Unlock()
respond(w, 200, map[string]any{"object_bytes": used, "reserved_bytes": reserved, "metadata_bytes": meta, "logical_bytes": raw, "objects": objects, "files": files, "active_parcels": parcels, "data_budget": limits.DataBudget, "file_budget": limits.FileBudget, "min_free_bytes": limits.MinFreeBytes, "disk_total_bytes": int64(disk.Blocks) * int64(disk.Bsize), "disk_free_bytes": int64(disk.Bavail) * int64(disk.Bsize), "audit_records": auditCount, "audit_content_bytes": auditBytes, "active_uploads": up, "active_downloads": down, "encryption": a.cfg.Encryption, "deduplication": a.cfg.Deduplication})
}
func (a *App) adminDelete(w http.ResponseWriter, r *http.Request) {
if !a.requireAdmin(w, r) {
return
}
a.mu.Lock()
defer a.mu.Unlock()
id := r.PathValue("parcel")
res, e := a.db.Exec("UPDATE parcels SET state='revoked',version=version+1 WHERE id=?", id)
if e != nil {
a.serverError(w, e)
return
}
n, _ := res.RowsAffected()
if n == 0 {
fail(w, 404, "包裹不存在")
return
}
a.audit("admin.delete", id, a.remoteIP(r))
respond(w, 200, map[string]any{"revoked": true, "message": "已禁止新下载,实体文件由清理任务回收"})
}
func (a *App) adminPolicy(w http.ResponseWriter, r *http.Request) {
if !a.requireAdmin(w, r) {
return
}
if r.Method == "GET" {
p, e := a.policy()
if e != nil {
a.serverError(w, e)
return
}
respond(w, 200, p)
return
}
var p Policy
if !decode(w, r, &p) {
return
}
if e := p.Validate(); e != nil {
fail(w, 400, e.Error())
return
}
a.mu.Lock()
defer a.mu.Unlock()
b, e := json.Marshal(p)
if e == nil {
_, e = a.db.Exec("UPDATE settings SET json=? WHERE id=1", string(b))
}
if e != nil {
a.serverError(w, e)
return
}
a.audit("policy.update", "", a.remoteIP(r))
respond(w, 200, p)
}
func (a *App) adminCleanup(w http.ResponseWriter, r *http.Request) {
if !a.requireAdmin(w, r) {
return
}
if e := a.Cleanup(); e != nil {
a.serverError(w, e)
return
}
a.audit("admin.cleanup", "", a.remoteIP(r))
respond(w, 200, map[string]bool{"cleaned": true})
}
func (a *App) adminDeleteFile(w http.ResponseWriter, r *http.Request) {
if !a.requireAdmin(w, r) {
return
}
a.mu.Lock()
defer a.mu.Unlock()
fid := r.PathValue("file")
res, e := a.db.Exec("UPDATE files SET status='deleting' WHERE id=?", fid)
if e != nil {
a.serverError(w, e)
return
}
n, _ := res.RowsAffected()
if n == 0 {
fail(w, 404, "文件不存在")
return
}
a.audit("admin.file_delete", fid, a.remoteIP(r))
respond(w, 200, map[string]bool{"deleted": true})
}
func (a *App) adminReissue(w http.ResponseWriter, r *http.Request) {
if !a.requireAdmin(w, r) {
return
}
a.mu.Lock()
defer a.mu.Unlock()
id := r.PathValue("parcel")
var state string
if e := a.db.QueryRow("SELECT state FROM parcels WHERE id=?", id).Scan(&state); e != nil || state != "restored" {
fail(w, 409, "仅允许重新发布恢复待审核包裹")
return
}
p, e := a.policy()
if e != nil {
a.serverError(w, e)
return
}
now := a.now().Unix()
expiry := now + int64(p.DefaultDays)*86400
hard := now + int64(p.MaxLifetimeDays)*86400
code := pickupCode()
tx, e := a.db.Begin()
if e != nil {
a.serverError(w, e)
return
}
defer tx.Rollback()
res, e := tx.Exec("UPDATE files SET status='ready',remaining=?,expires=?,hard_expires=? WHERE parcel=? AND object_id IS NOT NULL AND status NOT IN('missing','deleted','deleting')", p.DefaultDownloads, expiry, hard, id)
if e != nil {
a.serverError(w, e)
return
}
n, _ := res.RowsAffected()
if n == 0 {
fail(w, 409, "没有可重新发布的文件")
return
}
_, e = tx.Exec("UPDATE parcels SET state='ready',created=?,expires=?,hard_expires=?,code_hash=?,version=version+1,renewal_days=?,renewal_mode=? WHERE id=?", now, expiry, hard, a.keyed(code), p.RenewalDays, p.RenewalMode, id)
if e == nil {
e = tx.Commit()
}
if e != nil {
a.serverError(w, e)
return
}
a.audit("admin.reissue", id, a.remoteIP(r))
respond(w, 200, a.shareResult(r, code, expiry))
}