package main import ( "bufio" "encoding/json" "errors" "fmt" "io" "io/fs" "net/http" "os" "path/filepath" "sort" "strconv" "strings" "time" "git.tornberg.me/go-cart-actor/pkg/actor" "git.tornberg.me/go-cart-actor/pkg/cart" ) type FileServer struct { // Define fields here dataDir string storage actor.LogStorage[cart.CartGrain] } func NewFileServer(dataDir string, storage actor.LogStorage[cart.CartGrain]) *FileServer { return &FileServer{ dataDir: dataDir, storage: storage, } } func isValidId(id string) (uint64, bool) { if nr, err := strconv.ParseUint(id, 10, 64); err == nil { return nr, true } if nr, ok := cart.ParseCartId(id); ok { return uint64(nr), true } return 0, false } func isValidFileId(name string) (uint64, bool) { parts := strings.Split(name, ".") if len(parts) > 1 && parts[1] == "events" { idStr := parts[0] return isValidId(idStr) } return 0, false } // func AccessTime(info os.FileInfo) (time.Time, bool) { // switch stat := info.Sys().(type) { // case *syscall.Stat_t: // // Linux: Atim; macOS/BSD: Atimespec // // Use reflection or build tags if naming differs. // // Linux: // if stat.Atim.Sec != 0 || stat.Atim.Nsec != 0 { // return time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)), true // } // // macOS/BSD example (uncomment if needed): // //return time.Unix(int64(stat.Atimespec.Sec), int64(stat.Atimespec.Nsec)), true // } // return time.Time{}, false // } func appendFileInfo(info fs.FileInfo, out *CartFileInfo) *CartFileInfo { //sys := info.Sys() //fmt.Printf("sys type %T", sys) out.Size = info.Size() out.Modified = info.ModTime() //out.Accessed, _ = AccessTime(info) return out } // var cartFileRe = regexp.MustCompile(`^(\d+)\.events\.log$`) 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 } id, valid := isValidFileId(e.Name()) if !valid { continue } info, err := e.Info() if err != nil { continue } info.Sys() out = append(out, appendFileInfo(info, &CartFileInfo{ ID: fmt.Sprintf("%d", id), CartId: cart.CartId(id), })) } return out, nil } func readRawLogLines(path string) ([]json.RawMessage, error) { fh, err := os.Open(path) if err != nil { return nil, err } defer fh.Close() lines := make([]json.RawMessage, 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 := s.Bytes() if line == nil { 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) }) writeJSON(w, http.StatusOK, map[string]any{ "count": len(list), "carts": list, }) } func (fs *FileServer) PromotionsHandler(w http.ResponseWriter, r *http.Request) { fileName := filepath.Join(fs.dataDir, "promotions.json") if r.Method == http.MethodGet { file, err := os.Open(fileName) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } defer file.Close() io.Copy(w, file) return } if r.Method == http.MethodPost { file, err := os.Create(fileName) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } defer file.Close() io.Copy(file, r.Body) return } w.WriteHeader(http.StatusMethodNotAllowed) } func (fs *FileServer) VoucherHandler(w http.ResponseWriter, r *http.Request) { fileName := filepath.Join(fs.dataDir, "vouchers.json") if r.Method == http.MethodGet { file, err := os.Open(fileName) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } defer file.Close() io.Copy(w, file) return } if r.Method == http.MethodPost { file, err := os.Create(fileName) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } defer file.Close() io.Copy(file, r.Body) return } w.WriteHeader(http.StatusMethodNotAllowed) } func (fs *FileServer) PromotionPartHandler(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") if idStr == "" { w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, "missing id") return } _, ok := isValidId(idStr) if !ok { w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, "invalid id %s", idStr) return } w.WriteHeader(http.StatusNotImplemented) } type JsonError struct { Error string `json:"error"` } func (fs *FileServer) CartHandler(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") if idStr == "" { writeJSON(w, http.StatusBadRequest, JsonError{Error: "missing id"}) return } id, ok := isValidId(idStr) if !ok { writeJSON(w, http.StatusBadRequest, JsonError{Error: "invalid id"}) return } // reconstruct state from event log if present grain := cart.NewCartGrain(id, time.Now()) err := fs.storage.LoadEvents(r.Context(), id, grain) if err != nil { writeJSON(w, http.StatusInternalServerError, JsonError{Error: err.Error()}) return } 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, JsonError{Error: "cart not found"}) return } else if err != nil { writeJSON(w, http.StatusInternalServerError, JsonError{Error: err.Error()}) return } lines, err := readRawLogLines(path) if err != nil { writeJSON(w, http.StatusInternalServerError, JsonError{Error: err.Error()}) return } writeJSON(w, http.StatusOK, map[string]any{ "id": id, "cartId": cart.CartId(id).String(), "state": grain, "mutations": lines, "meta": map[string]any{ "size": info.Size(), "modified": info.ModTime(), "path": path, }, }) }