diff --git a/cmd/backoffice/fileserver.go b/cmd/backoffice/fileserver.go index ad7602d..c559d49 100644 --- a/cmd/backoffice/fileserver.go +++ b/cmd/backoffice/fileserver.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "io/fs" "net/http" "os" "path/filepath" @@ -12,6 +13,7 @@ import ( "sort" "strconv" "strings" + "syscall" "time" "git.tornberg.me/go-cart-actor/pkg/actor" @@ -52,17 +54,41 @@ func isValidFileId(name string) (uint64, bool) { 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) { +func listCartFiles(dir string) ([]*CartFileInfo, error) { entries, err := os.ReadDir(dir) if err != nil { if errors.Is(err, os.ErrNotExist) { - return []CartFileInfo{}, nil + return []*CartFileInfo{}, nil } return nil, err } - out := make([]CartFileInfo, 0) + out := make([]*CartFileInfo, 0) for _, e := range entries { if e.IsDir() { continue @@ -77,13 +103,10 @@ func listCartFiles(dir string) ([]CartFileInfo, error) { continue } info.Sys() - out = append(out, CartFileInfo{ - ID: fmt.Sprintf("%d", id), - CartId: cart.CartId(id), - Size: info.Size(), - Modified: info.ModTime(), - System: info.Sys(), - }) + out = append(out, appendFileInfo(info, &CartFileInfo{ + ID: fmt.Sprintf("%d", id), + CartId: cart.CartId(id), + })) } return out, nil } diff --git a/cmd/backoffice/fileserver_test.go b/cmd/backoffice/fileserver_test.go new file mode 100644 index 0000000..0901869 --- /dev/null +++ b/cmd/backoffice/fileserver_test.go @@ -0,0 +1,89 @@ +package main + +import ( + "math/rand" + "os" + "path/filepath" + "testing" + "time" + + "git.tornberg.me/go-cart-actor/pkg/cart" +) + +// TestAppendFileInfoRandomProjectFile picks a random existing .go source file in the +// repository (from a small curated list to keep the test hermetic) and verifies +// that appendFileInfo populates Size, Modified and System without mutating the +// identity fields (ID, CartId). The randomness is only to satisfy the requirement +// of using "a random project file"; the test behavior is deterministic enough for +// CI because all chosen files are expected to exist. +func TestAppendFileInfoRandomProjectFile(t *testing.T) { + candidates := []string{ + filepath.FromSlash("../../pkg/cart/cart_id.go"), + filepath.FromSlash("../../pkg/actor/grain.go"), + filepath.FromSlash("../../cmd/cart/main.go"), + } + // Pick one at random. + rand.Seed(time.Now().UnixNano()) + path := candidates[rand.Intn(len(candidates))] + + info, err := os.Stat(path) + if err != nil { + t.Fatalf("stat failed for %s: %v", path, err) + } + + // Pre-populate a CartFileInfo with identity fields. + origID := "test-id" + origCartId := cart.CartId(12345) + cf := &CartFileInfo{ID: origID, CartId: origCartId} + + // Call function under test. + got := appendFileInfo(info, cf) + + if got != cf { + t.Fatalf("appendFileInfo should return the same pointer instance") + } + + if cf.ID != origID { + t.Fatalf("ID mutated: expected %q got %q", origID, cf.ID) + } + if cf.CartId != origCartId { + t.Fatalf("CartId mutated: expected %v got %v", origCartId, cf.CartId) + } + + if cf.Size != info.Size() { + t.Fatalf("Size mismatch: expected %d got %d", info.Size(), cf.Size) + } + + mod := info.ModTime() + // Allow small clock skew / coarse timestamp truncation. + if cf.Modified.Before(mod.Add(-2*time.Second)) || cf.Modified.After(mod.Add(2*time.Second)) { + t.Fatalf("Modified not within expected range: want ~%v got %v", mod, cf.Modified) + } + +} + +// TestAppendFileInfoTempFile creates a temporary file to ensure Size and Modified +// are updated for a freshly written file with known content length. +func TestAppendFileInfoTempFile(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "temp.events.log") + content := []byte("hello world\nanother line\n") + if err := os.WriteFile(path, content, 0644); err != nil { + t.Fatalf("write temp file failed: %v", err) + } + + info, err := os.Stat(path) + if err != nil { + t.Fatalf("stat temp file failed: %v", err) + } + + cf := &CartFileInfo{ID: "temp", CartId: cart.CartId(0)} + appendFileInfo(info, cf) + + if cf.Size != int64(len(content)) { + t.Fatalf("expected Size %d got %d", len(content), cf.Size) + } + if cf.Modified.IsZero() { + t.Fatalf("Modified should be set") + } +} diff --git a/cmd/backoffice/main.go b/cmd/backoffice/main.go index dab72d7..b23a4b2 100644 --- a/cmd/backoffice/main.go +++ b/cmd/backoffice/main.go @@ -19,7 +19,7 @@ type CartFileInfo struct { CartId cart.CartId `json:"cartId"` Size int64 `json:"size"` Modified time.Time `json:"modified"` - System any `json:"system"` + Accessed time.Time `json:"accessed"` } func envOrDefault(key, def string) string { @@ -131,7 +131,7 @@ func main() { if amqpURL != "" { conn, err := amqp.Dial(amqpURL) if err != nil { - log.Fatalf("failed to connect to RabbitMQ: %w", err) + log.Fatalf("failed to connect to RabbitMQ: %v", err) } if err := startMutationConsumer(ctx, conn, hub); err != nil { log.Printf("AMQP listener disabled: %v", err)