package main import ( "bufio" "encoding/json" "errors" "fmt" "net/http" "os" "path/filepath" "sort" "strconv" "strings" "time" "git.tornberg.me/go-cart-actor/pkg/cart" ) type FileServer struct { // Define fields here dataDir string } func NewFileServer(dataDir string) *FileServer { return &FileServer{ dataDir: dataDir, } } func listCartFiles(dir string) ([]CartFileInfo, error) { entries, err := os.ReadDir(dir) if err != nil { if errors.Is(err, os.ErrNotExist) { return []CartFileInfo{}, nil } return nil, err } out := make([]CartFileInfo, 0) for _, e := range entries { if e.IsDir() { continue } name := e.Name() var id uint64 var parseErr error parts := strings.Split(name, ".") if len(parts) == 2 && parts[1] == "events.log" { idStr := parts[0] id, parseErr = strconv.ParseUint(idStr, 10, 64) } else if len(parts) == 3 && parts[1] == "events" && parts[2] == "log" { idStr := parts[0] id, parseErr = strconv.ParseUint(idStr, 10, 64) } else { continue } if parseErr != nil { continue } p := filepath.Join(dir, name) info, err := e.Info() if err != nil { continue } out = append(out, CartFileInfo{ ID: id, CartId: cart.CartId(id), Path: p, Size: info.Size(), Modified: info.ModTime(), }) } return out, nil } func readRawLogLines(path string) ([]string, error) { fh, err := os.Open(path) if err != nil { return nil, err } defer fh.Close() lines := make([]string, 0, 64) s := bufio.NewScanner(fh) // increase buffer to handle larger JSON lines buf := make([]byte, 0, 1024*1024) s.Buffer(buf, 1024*1024) for s.Scan() { line := strings.TrimSpace(s.Text()) if line == "" { continue } lines = append(lines, line) } if err := s.Err(); err != nil { return nil, err } return lines, nil } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-cache") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } func (fs *FileServer) CartsHandler(w http.ResponseWriter, r *http.Request) { list, err := listCartFiles(fs.dataDir) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } // sort by modified desc sort.Slice(list, func(i, j int) bool { return list[i].Modified.After(list[j].Modified) }) carts := make([]map[string]any, 0, len(list)) for _, it := range list { carts = append(carts, map[string]any{ "id": it.ID, "cartId": cart.CartId(it.ID).String(), "filename": filepath.Base(it.Path), "path": it.Path, "size": it.Size, "modified": it.Modified, }) } writeJSON(w, http.StatusOK, map[string]any{ "count": len(carts), "carts": carts, }) } func (fs *FileServer) CartHandler(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") if idStr == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing id"}) return } id, err := strconv.ParseUint(idStr, 10, 64) if err != nil { if cartId, ok := cart.ParseCartId(idStr); !ok { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"}) return } else { id = uint64(cartId) } } // reconstruct state from event log if present grain := cart.NewCartGrain(id, time.Now()) if globalDisk != nil { _ = globalDisk.LoadEvents(id, grain) } path := filepath.Join(fs.dataDir, fmt.Sprintf("%d.events.log", id)) info, err := os.Stat(path) if err != nil && errors.Is(err, os.ErrNotExist) { writeJSON(w, http.StatusNotFound, map[string]string{"error": "cart not found"}) return } else if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } lines, err := readRawLogLines(path) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]any{ "id": id, "cartId": cart.CartId(id).String(), "state": grain, "rawLog": lines, "meta": map[string]any{ "size": info.Size(), "modified": info.ModTime(), "path": path, }, }) }