2bab61209c
Go-Modul in go.mod und allen 45 Go-Dateien umbenannt.
242 lines
6.7 KiB
Go
242 lines
6.7 KiB
Go
package api
|
|
|
|
import (
|
|
"bufio"
|
|
"math"
|
|
"net/http"
|
|
"os"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"archivmail/internal/audit"
|
|
"archivmail/internal/storage"
|
|
"archivmail/pkg/mailparser"
|
|
)
|
|
|
|
// ── Audit Log handler ─────────────────────────────────────────────────────
|
|
|
|
func (s *Server) handleAuditLog(w http.ResponseWriter, r *http.Request) {
|
|
pageStr := r.URL.Query().Get("page")
|
|
pageSizeStr := r.URL.Query().Get("page_size")
|
|
username := r.URL.Query().Get("username")
|
|
eventType := r.URL.Query().Get("event_type")
|
|
|
|
page, _ := strconv.Atoi(pageStr)
|
|
pageSize, _ := strconv.Atoi(pageSizeStr)
|
|
if pageSize <= 0 {
|
|
pageSize = 50
|
|
}
|
|
|
|
entries, total, err := s.audlog.Query(audit.QueryFilter{
|
|
Username: username,
|
|
EventType: eventType,
|
|
PageSize: pageSize,
|
|
Page: page,
|
|
})
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "audit query failed")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"total": total,
|
|
"entries": entries,
|
|
})
|
|
}
|
|
|
|
// ── System stats handler ─────────────────────────────────────────────────
|
|
|
|
type diskStat struct {
|
|
Mount string `json:"mount"`
|
|
TotalBytes uint64 `json:"total_bytes"`
|
|
UsedBytes uint64 `json:"used_bytes"`
|
|
FreeBytes uint64 `json:"free_bytes"`
|
|
UsedPct float64 `json:"used_pct"`
|
|
FSType string `json:"fstype"`
|
|
}
|
|
|
|
type mailInfo struct {
|
|
ID string `json:"id"`
|
|
Date string `json:"date"`
|
|
From string `json:"from"`
|
|
Subject string `json:"subject"`
|
|
}
|
|
|
|
var excludedFSTypes = map[string]bool{
|
|
"tmpfs": true, "proc": true, "sysfs": true, "devtmpfs": true,
|
|
"cgroup": true, "cgroup2": true, "overlay": true, "squashfs": true,
|
|
"debugfs": true, "tracefs": true, "securityfs": true, "pstore": true,
|
|
"efivarfs": true, "bpf": true, "hugetlbfs": true, "mqueue": true,
|
|
"ramfs": true, "devpts": true, "fusectl": true, "configfs": true,
|
|
"autofs": true, "nsfs": true, "rpc_pipefs": true,
|
|
"fuse.lxcfs": true, "fuse": true,
|
|
}
|
|
|
|
func (s *Server) handleSystemStats(w http.ResponseWriter, r *http.Request) {
|
|
// CPU: /proc/loadavg
|
|
cpuResp := map[string]interface{}{"load1": 0.0, "load5": 0.0, "load15": 0.0, "num_cpu": runtime.NumCPU()}
|
|
if data, err := os.ReadFile("/proc/loadavg"); err == nil {
|
|
parts := strings.Fields(string(data))
|
|
if len(parts) >= 3 {
|
|
l1, _ := strconv.ParseFloat(parts[0], 64)
|
|
l5, _ := strconv.ParseFloat(parts[1], 64)
|
|
l15, _ := strconv.ParseFloat(parts[2], 64)
|
|
cpuResp = map[string]interface{}{"load1": l1, "load5": l5, "load15": l15, "num_cpu": runtime.NumCPU()}
|
|
}
|
|
}
|
|
|
|
// RAM: /proc/meminfo
|
|
ramResp := map[string]interface{}{"total_bytes": uint64(0), "used_bytes": uint64(0), "free_bytes": uint64(0), "used_pct": 0.0}
|
|
if data, err := os.ReadFile("/proc/meminfo"); err == nil {
|
|
kv := parseMeminfo(string(data))
|
|
total := kv["MemTotal"] * 1024
|
|
available := kv["MemAvailable"] * 1024
|
|
used := total - available
|
|
var usedPct float64
|
|
if total > 0 {
|
|
usedPct = math.Round(float64(used)/float64(total)*1000) / 10
|
|
}
|
|
ramResp = map[string]interface{}{
|
|
"total_bytes": total,
|
|
"used_bytes": used,
|
|
"free_bytes": available,
|
|
"used_pct": usedPct,
|
|
}
|
|
}
|
|
|
|
// Disks: /proc/mounts + syscall.Statfs
|
|
var disks []diskStat
|
|
seenMounts := map[string]bool{} // deduplicate by mountpoint
|
|
seenDevices := map[string]bool{} // deduplicate by device (catches ZFS bind-mounts)
|
|
if data, err := os.ReadFile("/proc/mounts"); err == nil {
|
|
scanner := bufio.NewScanner(strings.NewReader(string(data)))
|
|
for scanner.Scan() {
|
|
fields := strings.Fields(scanner.Text())
|
|
if len(fields) < 3 {
|
|
continue
|
|
}
|
|
device := fields[0]
|
|
mount := fields[1]
|
|
fstype := fields[2]
|
|
if excludedFSTypes[fstype] || seenMounts[mount] || seenDevices[device] {
|
|
continue
|
|
}
|
|
seenMounts[mount] = true
|
|
var stat syscall.Statfs_t
|
|
if err := syscall.Statfs(mount, &stat); err != nil {
|
|
continue
|
|
}
|
|
total := stat.Blocks * uint64(stat.Bsize)
|
|
free := stat.Bavail * uint64(stat.Bsize)
|
|
used := total - free
|
|
if total == 0 {
|
|
continue // skip pseudo-mounts with no storage (e.g. lxcfs overlays)
|
|
}
|
|
seenDevices[device] = true
|
|
var usedPct float64
|
|
if total > 0 {
|
|
usedPct = math.Round(float64(used)/float64(total)*1000) / 10
|
|
}
|
|
disks = append(disks, diskStat{
|
|
Mount: mount,
|
|
TotalBytes: total,
|
|
UsedBytes: used,
|
|
FreeBytes: free,
|
|
UsedPct: usedPct,
|
|
FSType: fstype,
|
|
})
|
|
}
|
|
}
|
|
if disks == nil {
|
|
disks = []diskStat{}
|
|
}
|
|
|
|
// Uptime: /proc/uptime
|
|
uptimeResp := map[string]interface{}{"seconds": 0.0}
|
|
if data, err := os.ReadFile("/proc/uptime"); err == nil {
|
|
parts := strings.Fields(string(data))
|
|
if len(parts) >= 1 {
|
|
if secs, err := strconv.ParseFloat(parts[0], 64); err == nil {
|
|
uptimeResp["seconds"] = secs
|
|
}
|
|
}
|
|
}
|
|
|
|
// Archive: first & last mail
|
|
archiveResp := map[string]interface{}{"first_mail": nil, "last_mail": nil}
|
|
first, last, err := s.store.FirstAndLastMail()
|
|
if err == nil {
|
|
if first != nil {
|
|
archiveResp["first_mail"] = mailRefToInfo(s.store, first)
|
|
}
|
|
if last != nil {
|
|
archiveResp["last_mail"] = mailRefToInfo(s.store, last)
|
|
}
|
|
}
|
|
|
|
// Mail activity (60min / 24h / 7d / 30d)
|
|
activity, _ := s.store.MailActivityStats(r.Context())
|
|
|
|
// Storage estimate — use free bytes of the first non-root disk (or root)
|
|
var freeDiskBytes uint64
|
|
for _, d := range disks {
|
|
freeDiskBytes = d.FreeBytes
|
|
break
|
|
}
|
|
estimate, _ := s.store.StorageEstimateStats(r.Context(), freeDiskBytes)
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"cpu": cpuResp,
|
|
"ram": ramResp,
|
|
"disks": disks,
|
|
"uptime": uptimeResp,
|
|
"archive": archiveResp,
|
|
"activity": activity,
|
|
"estimate": estimate,
|
|
})
|
|
}
|
|
|
|
func parseMeminfo(content string) map[string]uint64 {
|
|
result := make(map[string]uint64)
|
|
scanner := bufio.NewScanner(strings.NewReader(content))
|
|
for scanner.Scan() {
|
|
k, v, ok := strings.Cut(scanner.Text(), ":")
|
|
if !ok {
|
|
continue
|
|
}
|
|
fields := strings.Fields(strings.TrimSpace(v))
|
|
if len(fields) == 0 {
|
|
continue
|
|
}
|
|
val, err := strconv.ParseUint(fields[0], 10, 64)
|
|
if err == nil {
|
|
result[k] = val
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func mailRefToInfo(store *storage.Store, ref *storage.MailRef) *mailInfo {
|
|
dateStr := ref.ModTime.UTC().Format(time.RFC3339)
|
|
raw, err := store.Load(ref.ID)
|
|
if err != nil {
|
|
return &mailInfo{ID: ref.ID, Date: dateStr}
|
|
}
|
|
pm, err := mailparser.Parse(raw)
|
|
if err != nil {
|
|
return &mailInfo{ID: ref.ID, Date: dateStr}
|
|
}
|
|
if !pm.Date.IsZero() {
|
|
dateStr = pm.Date.UTC().Format(time.RFC3339)
|
|
}
|
|
return &mailInfo{
|
|
ID: ref.ID,
|
|
Date: dateStr,
|
|
From: pm.From,
|
|
Subject: pm.Subject,
|
|
}
|
|
}
|