196 lines
4.1 KiB
Go
196 lines
4.1 KiB
Go
package web
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"log"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/go-echarts/go-echarts/v2/charts"
|
|
"github.com/go-echarts/go-echarts/v2/opts"
|
|
)
|
|
|
|
func loadTemplates(name string) (*template.Template, error) {
|
|
|
|
funcs := template.FuncMap{
|
|
"safeHTML": func(s string) interface{} { return template.HTML(s) },
|
|
}
|
|
|
|
templates, err := loadTemplatesFromDir("internal/web/templates")
|
|
if err != nil {
|
|
log.Printf("Error loading templates: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
return template.New(name).Funcs(funcs).ParseFiles(templates...)
|
|
}
|
|
|
|
func ConvertAndSort[K comparable](data map[K]int, limit int) []CommandStat {
|
|
var result []CommandStat
|
|
for key, count := range data {
|
|
result = append(result, CommandStat{
|
|
Command: fmt.Sprintf("%v", key),
|
|
Count: count,
|
|
})
|
|
}
|
|
|
|
sort.Slice(result, func(i, j int) bool {
|
|
return result[i].Count > result[j].Count
|
|
})
|
|
if len(result) > limit {
|
|
result = result[:limit]
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func parseHistoryFile(file http.File) (map[string]int, error) {
|
|
commandCounts := make(map[string]int)
|
|
scanner := bufio.NewScanner(file)
|
|
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
|
|
command := strings.Fields(line)[0]
|
|
commandCounts[command]++
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return commandCounts, nil
|
|
}
|
|
|
|
func generateTopCommands(commandCounts map[string]int, topN int) []CommandStat {
|
|
stats := make([]CommandStat, 0, len(commandCounts))
|
|
for cmd, count := range commandCounts {
|
|
stats = append(stats, CommandStat{Command: cmd, Count: count})
|
|
}
|
|
|
|
sort.Slice(stats, func(i, j int) bool {
|
|
return stats[i].Count > stats[j].Count
|
|
})
|
|
|
|
if topN < len(stats) {
|
|
stats = stats[:topN]
|
|
}
|
|
return stats
|
|
}
|
|
|
|
func createBarChart(stats []CommandStat) *charts.Bar {
|
|
bar := charts.NewBar()
|
|
bar.SetGlobalOptions(
|
|
charts.WithTitleOpts(opts.Title{
|
|
Title: "Top Commands",
|
|
Subtitle: "Most used commands",
|
|
}),
|
|
charts.WithXAxisOpts(opts.XAxis{
|
|
AxisLabel: &opts.AxisLabel{
|
|
Show: opts.Bool(true),
|
|
Interval: "0",
|
|
Rotate: 45,
|
|
},
|
|
}),
|
|
charts.WithLegendOpts(opts.Legend{
|
|
TextStyle: &opts.TextStyle{
|
|
Color: "#888",
|
|
},
|
|
}),
|
|
)
|
|
|
|
commands := make([]string, len(stats))
|
|
counts := make([]int, len(stats))
|
|
for i, stat := range stats {
|
|
commands[i] = stat.Command
|
|
counts[i] = stat.Count
|
|
}
|
|
|
|
bar.SetXAxis(commands).
|
|
AddSeries("Frequency", generateBarItems(counts))
|
|
|
|
return bar
|
|
}
|
|
|
|
func generateBarItems(data []int) []opts.BarData {
|
|
items := make([]opts.BarData, len(data))
|
|
for i, v := range data {
|
|
items[i] = opts.BarData{Value: v}
|
|
}
|
|
return items
|
|
}
|
|
|
|
func saveResults(uniqueID string, stats map[string]interface{}) {
|
|
filename := "results/" + uniqueID + ".json"
|
|
file, err := os.Create(filename)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer file.Close()
|
|
json.NewEncoder(file).Encode(stats)
|
|
}
|
|
|
|
func loadResults(uniqueID string) (map[string]interface{}, error) {
|
|
filename := "results/" + uniqueID + ".json"
|
|
file, err := os.Open(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
|
|
var stats map[string]interface{}
|
|
err = json.NewDecoder(file).Decode(&stats)
|
|
return stats, err
|
|
}
|
|
|
|
func loadTemplatesFromDir(dir string) ([]string, error) {
|
|
var templates []string
|
|
|
|
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.IsDir() && filepath.Ext(path) == ".html" {
|
|
templates = append(templates, path)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return templates, err
|
|
}
|
|
|
|
func isTextFileAndSizeOk(file multipart.File, size int64) (bool, error) {
|
|
if size > 1*1024*1024 {
|
|
return false, fmt.Errorf("File size exceeds 1MB")
|
|
}
|
|
|
|
buffer := make([]byte, 512)
|
|
_, err := file.Read(buffer)
|
|
if err != nil && err != io.EOF {
|
|
return false, fmt.Errorf("Error reading file: %v", err)
|
|
}
|
|
|
|
mimeType := http.DetectContentType(buffer)
|
|
|
|
if !strings.HasPrefix(mimeType, "text/") {
|
|
return false, fmt.Errorf("File is not a text file (mimetype: %s)", mimeType)
|
|
}
|
|
|
|
_, err = file.Seek(0, 0)
|
|
if err != nil {
|
|
return false, fmt.Errorf("Error seeking file: %v", err)
|
|
}
|
|
|
|
return true, nil
|
|
}
|