Add storage analytics page
This commit is contained in:
@@ -4,9 +4,11 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shirou/gopsutil/v3/disk"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@@ -209,3 +211,66 @@ func (s *Service) buildPath(id, filename string) string {
|
||||
func (s *Service) GetAllFiles() ([]FileRecord, error) {
|
||||
return s.repo.GetAll()
|
||||
}
|
||||
|
||||
func (s *Service) GetStorageStats() (*StorageStats, error) {
|
||||
files, err := s.repo.GetAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stats := &StorageStats{}
|
||||
|
||||
var total int64
|
||||
var active, deleted int
|
||||
|
||||
for _, f := range files {
|
||||
stats.TotalFiles++
|
||||
|
||||
if f.Deleted {
|
||||
deleted++
|
||||
} else {
|
||||
active++
|
||||
}
|
||||
|
||||
total += f.Size
|
||||
}
|
||||
|
||||
stats.TotalBytes = total
|
||||
stats.ActiveFiles = active
|
||||
stats.DeletedFiles = deleted
|
||||
|
||||
if stats.TotalFiles > 0 {
|
||||
stats.AverageFileSize = total / int64(stats.TotalFiles)
|
||||
}
|
||||
|
||||
// Biggest files
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
return files[i].Size > files[j].Size
|
||||
})
|
||||
|
||||
if len(files) > 10 {
|
||||
stats.LargestFiles = files[:10]
|
||||
} else {
|
||||
stats.LargestFiles = files
|
||||
}
|
||||
|
||||
usage, err := disk.Usage(s.storageDir)
|
||||
if err == nil {
|
||||
stats.DiskTotalBytes = int64(usage.Total)
|
||||
stats.DiskFreeBytes = int64(usage.Free)
|
||||
stats.DiskUsedBytes = int64(usage.Used)
|
||||
}
|
||||
|
||||
// tmp chunk usage
|
||||
var tmpTotal int64
|
||||
filepath.Walk("tmp", func(path string, info os.FileInfo, err error) error {
|
||||
if err == nil && !info.IsDir() {
|
||||
tmpTotal += info.Size()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
stats.TempBytes = tmpTotal
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
18
internal/file/stats.go
Normal file
18
internal/file/stats.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package file
|
||||
|
||||
type StorageStats struct {
|
||||
TotalFiles int
|
||||
ActiveFiles int
|
||||
DeletedFiles int
|
||||
|
||||
TotalBytes int64
|
||||
AverageFileSize int64
|
||||
|
||||
DiskTotalBytes int64
|
||||
DiskFreeBytes int64
|
||||
DiskUsedBytes int64
|
||||
|
||||
TempBytes int64
|
||||
|
||||
LargestFiles []FileRecord
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func (h *Handler) AdminPage(c *gin.Context) {
|
||||
page = 1
|
||||
}
|
||||
|
||||
limit := 10
|
||||
limit := 25
|
||||
offset := (page - 1) * limit
|
||||
|
||||
files, totalCount, err := h.fileService.GetPaginatedFiles(limit, offset)
|
||||
@@ -103,11 +103,6 @@ func (h *Handler) AdminPage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Only check files on the current page.
|
||||
// Status meanings:
|
||||
// - green: file exists on disk
|
||||
// - red: file missing
|
||||
// - rainbow: stat error (something unexpected)
|
||||
type AdminFileView struct {
|
||||
file.FileRecord
|
||||
ActualStatus string
|
||||
@@ -153,4 +148,26 @@ func (h *Handler) Logout(c *gin.Context) {
|
||||
|
||||
func (h *Handler) ChangePasswordPage(c *gin.Context) {
|
||||
c.HTML(200, "changePassword.html", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) AdminStatsPage(c *gin.Context) {
|
||||
log := middleware.StructuredLog(c).With().
|
||||
Str("event", "admin_stats_page").
|
||||
Logger()
|
||||
|
||||
stats, err := h.fileService.GetStorageStats()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to load storage stats")
|
||||
|
||||
c.HTML(500, "admin_stats.html", gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Msg("Admin stats page viewed")
|
||||
|
||||
c.HTML(200, "admin_stats.html", gin.H{
|
||||
"Stats": stats,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ func RegisterRoutes(r *gin.Engine, h *Handler, userService *user.Service) {
|
||||
adminRoutes.Use(user.ForcePasswordChangeMiddleware(userService))
|
||||
|
||||
adminRoutes.GET("/admin", h.AdminPage)
|
||||
adminRoutes.GET("/admin/stats", h.AdminStatsPage)
|
||||
adminRoutes.GET("/config", h.ConfigPage)
|
||||
adminRoutes.POST("/config", h.ConfigSave)
|
||||
adminRoutes.GET("/logout", h.Logout)
|
||||
|
||||
Reference in New Issue
Block a user