From 691041814f4d04c36b3f1373a14c49ddf776ac29 Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 18 May 2026 01:09:34 +0200 Subject: [PATCH] Add storage analytics page --- cmd/server/main.go | 6 + go.mod | 6 +- go.sum | 10 ++ internal/file/service.go | 65 ++++++++ internal/file/stats.go | 18 +++ internal/web/handler.go | 31 +++- internal/web/routes.go | 1 + templates/admin.html | 1 + templates/admin_stats.html | 309 +++++++++++++++++++++++++++++++++++++ 9 files changed, 439 insertions(+), 8 deletions(-) create mode 100644 internal/file/stats.go create mode 100644 templates/admin_stats.html diff --git a/cmd/server/main.go b/cmd/server/main.go index 8c42c9a..ae6b710 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -66,6 +66,12 @@ func main() { "add": func(a, b int) int { return a + b }, "sub": func(a, b int) int { return a - b }, "humanSize": util.HumanSize, + "percent": func(used, total int64) int { + if total == 0 { + return 0 + } + return int((float64(used) / float64(total)) * 100) + }, }) r.LoadHTMLGlob("templates/*.html") diff --git a/go.mod b/go.mod index 6283f94..8a71939 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/rs/zerolog v1.33.0 golang.org/x/crypto v0.49.0 + golang.org/x/sys v0.42.0 gorm.io/driver/mysql v1.6.0 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 @@ -23,6 +24,7 @@ require ( github.com/cloudwego/base64x v0.1.6 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect @@ -44,15 +46,17 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect + github.com/shirou/gopsutil/v3 v3.24.5 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect golang.org/x/arch v0.22.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 // indirect google.golang.org/protobuf v1.36.10 // indirect ) diff --git a/go.sum b/go.sum index f8edec4..fcb26ef 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -78,6 +80,8 @@ github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= @@ -85,6 +89,8 @@ github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRC github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -101,6 +107,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= @@ -113,6 +121,8 @@ golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/file/service.go b/internal/file/service.go index aaa89e5..e6c036e 100644 --- a/internal/file/service.go +++ b/internal/file/service.go @@ -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 +} diff --git a/internal/file/stats.go b/internal/file/stats.go new file mode 100644 index 0000000..6516ebe --- /dev/null +++ b/internal/file/stats.go @@ -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 +} diff --git a/internal/web/handler.go b/internal/web/handler.go index d50b685..21d59f9 100644 --- a/internal/web/handler.go +++ b/internal/web/handler.go @@ -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) -} \ No newline at end of file +} + +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, + }) +} diff --git a/internal/web/routes.go b/internal/web/routes.go index af119f7..026e006 100644 --- a/internal/web/routes.go +++ b/internal/web/routes.go @@ -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) diff --git a/templates/admin.html b/templates/admin.html index 24aa258..b313193 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -121,6 +121,7 @@
← BACK_TO_UPLOADER CONFIG_MODULE + STORAGE_MATRIX LOGOUT_SESSION
diff --git a/templates/admin_stats.html b/templates/admin_stats.html new file mode 100644 index 0000000..ffc23c7 --- /dev/null +++ b/templates/admin_stats.html @@ -0,0 +1,309 @@ + + + + + + Storage Matrix + + + + + + + + +
+ +
+
+

+ STORAGE_ANALYTICS +

+ +
+
+
+ + +
+ +
+ +
+
+ TOTAL_STORAGE_USED +
+ +
+ {{humanSize .Stats.TotalBytes}} +
+
+ +
+
+ DISK_FREE_SPACE +
+ +
+ {{humanSize .Stats.DiskFreeBytes}} +
+
+ +
+
+ TEMP_CHUNK_STORAGE +
+ +
+ {{humanSize .Stats.TempBytes}} +
+
+ +
+ + +
+ +
+
+ DISK_CONSUMPTION +
+ +
+ {{humanSize .Stats.DiskUsedBytes}} + / + {{humanSize .Stats.DiskTotalBytes}} +
+
+ +
+
+
+
+ +
+ {{percent .Stats.DiskUsedBytes .Stats.DiskTotalBytes}}% +
+ +
+ + +
+ +
+
+ TOTAL_FILES +
+ +
+ {{.Stats.TotalFiles}} +
+
+ +
+
+ ACTIVE_FILES +
+ +
+ {{.Stats.ActiveFiles}} +
+
+ +
+
+ DELETED_FILES +
+ +
+ {{.Stats.DeletedFiles}} +
+
+ +
+
+ AVERAGE_SIZE +
+ +
+ {{humanSize .Stats.AverageFileSize}} +
+
+ +
+ +
+ +
+ LARGEST_FILES_ON_SYSTEM +
+ + + + + + + + + + + + + + + {{if not .Stats.LargestFiles}} + + + + {{end}} + + {{range .Stats.LargestFiles}} + + + + + + + + + + + + + + {{end}} + + +
FilenameSizeDownloadsCreatedStatus
+ No files detected in storage matrix +
+ {{.Filename}} + + {{humanSize .Size}} + + {{.DownloadCount}} + + {{.CreatedAt.Format "02/01/06 15:04"}} + + {{if .Deleted}} + + DELETED + + {{else}} + + LIVE + + {{end}} +
+ +
+ + + +
+ + + \ No newline at end of file