more fancy
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.tornberg.me/go-cart-actor/pkg/actor"
|
"git.tornberg.me/go-cart-actor/pkg/actor"
|
||||||
@@ -52,17 +54,41 @@ func isValidFileId(name string) (uint64, bool) {
|
|||||||
return 0, false
|
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$`)
|
var cartFileRe = regexp.MustCompile(`^(\d+)\.events\.log$`)
|
||||||
|
|
||||||
func listCartFiles(dir string) ([]CartFileInfo, error) {
|
func listCartFiles(dir string) ([]*CartFileInfo, error) {
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
return []CartFileInfo{}, nil
|
return []*CartFileInfo{}, nil
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
out := make([]CartFileInfo, 0)
|
out := make([]*CartFileInfo, 0)
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
if e.IsDir() {
|
if e.IsDir() {
|
||||||
continue
|
continue
|
||||||
@@ -77,13 +103,10 @@ func listCartFiles(dir string) ([]CartFileInfo, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
info.Sys()
|
info.Sys()
|
||||||
out = append(out, CartFileInfo{
|
out = append(out, appendFileInfo(info, &CartFileInfo{
|
||||||
ID: fmt.Sprintf("%d", id),
|
ID: fmt.Sprintf("%d", id),
|
||||||
CartId: cart.CartId(id),
|
CartId: cart.CartId(id),
|
||||||
Size: info.Size(),
|
}))
|
||||||
Modified: info.ModTime(),
|
|
||||||
System: info.Sys(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|||||||
89
cmd/backoffice/fileserver_test.go
Normal file
89
cmd/backoffice/fileserver_test.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ type CartFileInfo struct {
|
|||||||
CartId cart.CartId `json:"cartId"`
|
CartId cart.CartId `json:"cartId"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
Modified time.Time `json:"modified"`
|
Modified time.Time `json:"modified"`
|
||||||
System any `json:"system"`
|
Accessed time.Time `json:"accessed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func envOrDefault(key, def string) string {
|
func envOrDefault(key, def string) string {
|
||||||
@@ -131,7 +131,7 @@ func main() {
|
|||||||
if amqpURL != "" {
|
if amqpURL != "" {
|
||||||
conn, err := amqp.Dial(amqpURL)
|
conn, err := amqp.Dial(amqpURL)
|
||||||
if err != nil {
|
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 {
|
if err := startMutationConsumer(ctx, conn, hub); err != nil {
|
||||||
log.Printf("AMQP listener disabled: %v", err)
|
log.Printf("AMQP listener disabled: %v", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user