Initial commit
This commit is contained in:
204
main.go
Normal file
204
main.go
Normal file
@@ -0,0 +1,204 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user