package main import ( "embed" "encoding/json" "fmt" "html/template" "io" "io/fs" "log" "net/http" "os" "path/filepath" "sort" "strings" "time" ) //go:embed index.html //go:embed dashboard.html //go:embed static/* var content embed.FS const ( uploadDir = "./public" logFile = "debug.log" ) type FileInfo struct { Name string Size int64 } type AccessEvent struct { IP string File string Timestamp time.Time Action string // "upload" or "download" } type StatEntry struct { IP string `json:"ip"` File string `json:"file"` Count int `json:"count"` } var logger *log.Logger var accessLog []AccessEvent func main() { // Logger setup logWriter, err := os.OpenFile(logFile, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644) if err != nil { log.Fatalf("Log file error: %v", err) } defer logWriter.Close() logger = log.New(logWriter, "", log.LstdFlags) if _, err := os.Stat(uploadDir); os.IsNotExist(err) { _ = os.Mkdir(uploadDir, 0755) } // Embed FS for static/ directory fsStatic, err := fs.Sub(content, "static") if err != nil { logger.Fatalf("Failed to load static: %v", err) } // Templates tmplIndex := template.Must(template.New("index.html").Funcs(template.FuncMap{ "formatBytes": formatBytes, }).ParseFS(content, "index.html")) tmplDashboard := template.Must(template.ParseFS(content, "dashboard.html")) // Routes http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { files, _ := listFiles(uploadDir) data := struct{ Files []FileInfo }{Files: files} w.Header().Set("Content-Type", "text/html") _ = tmplIndex.Execute(w, data) }) http.HandleFunc("/upload", uploadHandler) http.HandleFunc("/dashboard", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") _ = tmplDashboard.Execute(w, struct{ Events []AccessEvent }{accessLog}) }) http.HandleFunc("/dashboard-data", statsHandler) http.HandleFunc("/files/", func(w http.ResponseWriter, r *http.Request) { file := strings.TrimPrefix(r.URL.Path, "/files/") ip := getIP(r) accessLog = append(accessLog, AccessEvent{ IP: ip, File: file, Action: "download", Timestamp: time.Now(), }) http.ServeFile(w, r, filepath.Join(uploadDir, file)) }) http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(fsStatic)))) log.Println("Server started on http://localhost:80/") _ = http.ListenAndServe(":80", nil) } // ===== Helpers ===== func uploadHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Only POST supported", http.StatusMethodNotAllowed) return } err := r.ParseMultipartForm(10 << 20) if err != nil { http.Error(w, "Failed to parse form", 400) return } file, handler, err := r.FormFile("file") if err != nil { http.Error(w, "No file found", 400) return } defer file.Close() dst, err := os.Create(filepath.Join(uploadDir, handler.Filename)) if err != nil { http.Error(w, "Failed to save file", 500) return } defer dst.Close() _, _ = io.Copy(dst, file) ip := getIP(r) accessLog = append(accessLog, AccessEvent{ IP: ip, File: handler.Filename, Action: "upload", Timestamp: time.Now(), }) http.Redirect(w, r, "/", http.StatusSeeOther) } func listFiles(dir string) ([]FileInfo, error) { entries, err := os.ReadDir(dir) if err != nil { return nil, err } var files []FileInfo for _, e := range entries { info, err := e.Info() if err == nil && !info.IsDir() { files = append(files, FileInfo{Name: info.Name(), Size: info.Size()}) } } sort.Slice(files, func(i, j int) bool { return files[i].Name < files[j].Name }) return files, nil } func formatBytes(b int64) string { const unit = 1024 if b < unit { return fmt.Sprintf("%d B", b) } div, exp := int64(unit), 0 for n := b / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp]) } func getIP(r *http.Request) string { ip := r.RemoteAddr if i := strings.LastIndex(ip, ":"); i != -1 { ip = ip[:i] } return ip } func statsHandler(w http.ResponseWriter, r *http.Request) { counter := map[string]map[string]int{} for _, e := range accessLog { if e.Action != "download" { continue } if counter[e.IP] == nil { counter[e.IP] = map[string]int{} } counter[e.IP][e.File]++ } var result []StatEntry for ip, files := range counter { for file, count := range files { result = append(result, StatEntry{IP: ip, File: file, Count: count}) } } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(result) }