初始化蓝鸟文件快递柜源码与部署文档
This commit is contained in:
commit
5643d22242
70 files changed
+18669
No files matched your search
@@ -0,0 +1,216 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type adminListResult struct {
|
||||
Items []map[string]any `json:"items"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Pages int `json:"pages"`
|
||||
}
|
||||
|
||||
func adminPaging(q url.Values, defaultSize int) (adminListResult, error) {
|
||||
p := adminListResult{Items: []map[string]any{}, Page: 1, PageSize: defaultSize, Pages: 1}
|
||||
for key, dest := range map[string]*int{"page": &p.Page, "page_size": &p.PageSize} {
|
||||
if raw := q.Get(key); raw != "" {
|
||||
n, err := strconv.Atoi(raw)
|
||||
if err != nil || n < 1 || (key == "page_size" && n > 100) {
|
||||
return p, errors.New("页码须为正整数,每页条数须为1至100")
|
||||
}
|
||||
*dest = n
|
||||
}
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
func (p *adminListResult) setTotal(total int) {
|
||||
p.Total = total
|
||||
p.Pages = max(1, (total+p.PageSize-1)/p.PageSize)
|
||||
p.Page = min(p.Page, p.Pages)
|
||||
}
|
||||
func adminFilter(q url.Values, key string, limit int) (string, error) {
|
||||
v := strings.TrimSpace(q.Get(key))
|
||||
if len(v) > limit {
|
||||
return "", errors.New("搜索条件过长")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (a *App) adminParcels(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
q := r.URL.Query()
|
||||
result, err := adminPaging(q, 10)
|
||||
if err != nil {
|
||||
fail(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
keyword, err := adminFilter(q, "q", 256)
|
||||
if err != nil {
|
||||
fail(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
state, err := adminFilter(q, "state", 32)
|
||||
if err != nil {
|
||||
fail(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
if state != "" && state != "uploading" && state != "ready" && state != "revoked" && state != "expired" && state != "exhausted" && state != "restored" {
|
||||
fail(w, 400, "包裹状态无效")
|
||||
return
|
||||
}
|
||||
where := " WHERE 1=1"
|
||||
args := []any{}
|
||||
if keyword != "" {
|
||||
where += " AND (instr(lower(p.id),lower(?))>0 OR EXISTS(SELECT 1 FROM files f WHERE f.parcel=p.id AND instr(lower(f.name),lower(?))>0))"
|
||||
args = append(args, keyword, keyword)
|
||||
}
|
||||
if state != "" {
|
||||
where += " AND p.state=?"
|
||||
args = append(args, state)
|
||||
}
|
||||
// File details use the same connection after the rows are closed. Prevent
|
||||
// cleanup/publication from changing the selected parcels between those reads.
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
var total int
|
||||
if err = a.db.QueryRowContext(r.Context(), "SELECT count(*) FROM parcels p"+where, args...).Scan(&total); err != nil {
|
||||
a.serverError(w, err)
|
||||
return
|
||||
}
|
||||
result.setTotal(total)
|
||||
listArgs := append(append([]any{}, args...), result.PageSize, (result.Page-1)*result.PageSize)
|
||||
rows, err := a.db.QueryContext(r.Context(), "SELECT p.id,p.state,p.ip,p.created,p.expires,p.renewal_days,p.renewal_mode,(SELECT count(*) FROM files WHERE parcel=p.id),(SELECT COALESCE(sum(size),0) FROM files WHERE parcel=p.id) FROM parcels p"+where+" ORDER BY p.created DESC,p.id DESC LIMIT ? OFFSET ?", listArgs...)
|
||||
if err != nil {
|
||||
a.serverError(w, err)
|
||||
return
|
||||
}
|
||||
for rows.Next() {
|
||||
var id, state, ip, mode string
|
||||
var created, expiry, size int64
|
||||
var renewal, count int
|
||||
if err = rows.Scan(&id, &state, &ip, &created, &expiry, &renewal, &mode, &count, &size); err != nil {
|
||||
rows.Close()
|
||||
a.serverError(w, err)
|
||||
return
|
||||
}
|
||||
result.Items = append(result.Items, map[string]any{"id": id, "state": state, "ip": ip, "created": created, "expires": expiry, "renewal_days": renewal, "renewal_mode": mode, "count": count, "size": size})
|
||||
}
|
||||
err = rows.Err()
|
||||
rows.Close()
|
||||
if err != nil {
|
||||
a.serverError(w, err)
|
||||
return
|
||||
}
|
||||
for _, p := range result.Items {
|
||||
files, err := a.fileList(p["id"].(string))
|
||||
if err != nil {
|
||||
a.serverError(w, err)
|
||||
return
|
||||
}
|
||||
p["files"] = files
|
||||
}
|
||||
respond(w, 200, result)
|
||||
}
|
||||
|
||||
func (a *App) adminAudit(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
q := r.URL.Query()
|
||||
result, err := adminPaging(q, 20)
|
||||
if err != nil {
|
||||
fail(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
where := " WHERE 1=1"
|
||||
args := []any{}
|
||||
for _, f := range []struct {
|
||||
key string
|
||||
limit int
|
||||
}{{"action", 64}, {"source", 128}, {"target", 256}} {
|
||||
value, err := adminFilter(q, f.key, f.limit)
|
||||
if err != nil {
|
||||
fail(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if f.key == "action" {
|
||||
where += " AND action=?"
|
||||
} else {
|
||||
where += " AND instr(lower(" + f.key + "),lower(?))>0"
|
||||
}
|
||||
args = append(args, value)
|
||||
}
|
||||
var from, to int64
|
||||
for _, bound := range []struct {
|
||||
key, op string
|
||||
dest *int64
|
||||
}{{"from", ">=", &from}, {"to", "<=", &to}} {
|
||||
if raw := q.Get(bound.key); raw != "" {
|
||||
value, err := strconv.ParseInt(raw, 10, 64)
|
||||
if err != nil || value < 0 {
|
||||
fail(w, 400, "时间须为非负Unix秒数")
|
||||
return
|
||||
}
|
||||
*bound.dest = value
|
||||
where += " AND created" + bound.op + "?"
|
||||
args = append(args, value)
|
||||
}
|
||||
}
|
||||
if q.Get("from") != "" && q.Get("to") != "" && from > to {
|
||||
fail(w, 400, "开始时间不能晚于结束时间")
|
||||
return
|
||||
}
|
||||
// Count and items share a snapshot so concurrent audit writes or retention do
|
||||
// not change the response's total and page boundaries mid-query.
|
||||
tx, err := a.db.BeginTx(r.Context(), &sql.TxOptions{ReadOnly: true})
|
||||
if err != nil {
|
||||
a.serverError(w, err)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
var total int
|
||||
if err = tx.QueryRowContext(r.Context(), "SELECT count(*) FROM audit"+where, args...).Scan(&total); err != nil {
|
||||
a.serverError(w, err)
|
||||
return
|
||||
}
|
||||
result.setTotal(total)
|
||||
listArgs := append(append([]any{}, args...), result.PageSize, (result.Page-1)*result.PageSize)
|
||||
rows, err := tx.QueryContext(r.Context(), "SELECT id,created,action,target,source FROM audit"+where+" ORDER BY id DESC LIMIT ? OFFSET ?", listArgs...)
|
||||
if err != nil {
|
||||
a.serverError(w, err)
|
||||
return
|
||||
}
|
||||
for rows.Next() {
|
||||
var id, created int64
|
||||
var action, target, source string
|
||||
if err = rows.Scan(&id, &created, &action, &target, &source); err != nil {
|
||||
rows.Close()
|
||||
a.serverError(w, err)
|
||||
return
|
||||
}
|
||||
result.Items = append(result.Items, map[string]any{"id": id, "created": created, "action": action, "target": target, "source": source})
|
||||
}
|
||||
err = rows.Err()
|
||||
rows.Close()
|
||||
if err != nil {
|
||||
a.serverError(w, err)
|
||||
return
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
a.serverError(w, err)
|
||||
return
|
||||
}
|
||||
respond(w, 200, result)
|
||||
}
|
||||
Reference in new issue
Block a user