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 }