mirror of
https://github.com/hanzoai/s3.git
synced 2026-10-30 14:59:09 +00:00
hanzoai/s3 was an AGPL-3.0 MinIO fork whose network-copyleft blocks embedding
and reselling as a managed service. Replace it wholesale with a fork of
SeaweedFS (github.com/seaweedfs/seaweedfs, Apache-2.0, Go) which ships a
built-in S3 gateway + Filer.
- License: AGPL-3.0 -> Apache-2.0 (upstream LICENSE kept verbatim, incl.
"Copyright 2025 Chris Lu"); NOTICE added crediting SeaweedFS. No AGPL/BUSL/
SSPL/GPL or enterprise ("ee") files remain.
- Module: go.mod path set to github.com/hanzoai/s3 with a self-replace
(replace github.com/seaweedfs/seaweedfs => ./) so the ~1800 internal Go files
keep their upstream import paths with zero churn. Internal identifiers and
import paths are intentionally NOT renamed.
- CLI rebrand: the binary is built as `s3` (was `weed`); root usage, per-command
help/usage/flag strings, shell autocomplete program name, and the product name
in CLI output now read "Hanzo S3" / `s3`. Users never see `weed`.
(e.g. `s3 server`, `s3 filer`, `s3 master`, `s3 volume`, `s3 s3`, `s3 mount`.)
- Build: `make` (or `go build -o s3 ./weed`) builds the `s3` binary green;
`cmd/weed-db` and `cmd/weed-sql` also build; `go vet ./weed/command/` clean.
- README/LLM.md rewritten for Hanzo S3; AGENTS.md/CLAUDE.md symlinks preserved.
- Dropped a 33MB prebuilt Mach-O `stress` binary upstream had committed; `.gitignore`
updated to ignore the `s3` build output.
195 lines
7.8 KiB
Go
195 lines
7.8 KiB
Go
package engine
|
|
|
|
import (
|
|
"reflect"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3lifecycle"
|
|
)
|
|
|
|
// activeAll returns a PriorStates map that activates every action of the
|
|
// given canonical rules under bucket.
|
|
func activeAll(bucket string, rules []*s3lifecycle.Rule) map[s3lifecycle.ActionKey]PriorState {
|
|
out := map[s3lifecycle.ActionKey]PriorState{}
|
|
for _, r := range rules {
|
|
rh := s3lifecycle.RuleHash(r)
|
|
for _, k := range s3lifecycle.RuleActionKinds(r) {
|
|
out[s3lifecycle.ActionKey{Bucket: bucket, RuleHash: rh, ActionKind: k}] = PriorState{BootstrapComplete: true}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func sortedKeys(keys []s3lifecycle.ActionKey) []s3lifecycle.ActionKey {
|
|
out := append([]s3lifecycle.ActionKey(nil), keys...)
|
|
sort.Slice(out, func(i, j int) bool {
|
|
return out[i].ActionKind < out[j].ActionKind
|
|
})
|
|
return out
|
|
}
|
|
|
|
func TestMatchOriginalWrite_DelayGroupRoutes(t *testing.T) {
|
|
r30 := ruleExpDays("a", "x/", 30)
|
|
r60 := ruleExpDays("b", "x/", 60)
|
|
rules := []*s3lifecycle.Rule{r30, r60}
|
|
e := New()
|
|
snap := e.Compile([]CompileInput{{Bucket: "bk", Rules: rules}}, CompileOptions{PriorStates: activeAll("bk", rules)})
|
|
|
|
ev := &Event{Shape: EventShapeOriginalWrite, Bucket: "bk", Path: "x/o"}
|
|
got30 := snap.MatchOriginalWrite(ev, s3lifecycle.DaysToDuration(30))
|
|
if len(got30) != 1 || got30[0].ActionKind != s3lifecycle.ActionKindExpirationDays {
|
|
t.Fatalf("30d sweep should match r30, got %v", got30)
|
|
}
|
|
got60 := snap.MatchOriginalWrite(ev, s3lifecycle.DaysToDuration(60))
|
|
if len(got60) != 1 {
|
|
t.Fatalf("60d sweep should match r60, got %v", got60)
|
|
}
|
|
}
|
|
|
|
func TestMatchOriginalWrite_PrefixFilter(t *testing.T) {
|
|
r := ruleExpDays("a", "logs/", 30)
|
|
e := New()
|
|
snap := e.Compile([]CompileInput{{Bucket: "bk", Rules: []*s3lifecycle.Rule{r}}}, CompileOptions{PriorStates: activeAll("bk", []*s3lifecycle.Rule{r})})
|
|
|
|
if got := snap.MatchOriginalWrite(&Event{Shape: EventShapeOriginalWrite, Bucket: "bk", Path: "data/x"}, s3lifecycle.DaysToDuration(30)); len(got) != 0 {
|
|
t.Fatalf("non-matching prefix should reject, got %v", got)
|
|
}
|
|
if got := snap.MatchOriginalWrite(&Event{Shape: EventShapeOriginalWrite, Bucket: "bk", Path: "logs/x"}, s3lifecycle.DaysToDuration(30)); len(got) != 1 {
|
|
t.Fatalf("matching prefix should fire, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestMatchOriginalWrite_MarkActiveBecomesRoutable(t *testing.T) {
|
|
// pending_bootstrap actions are indexed but inactive: MatchOriginalWrite
|
|
// returns nothing. After MarkActive flips engineState, the same key is
|
|
// routable in subsequent matches without a recompile.
|
|
r := ruleExpDays("a", "x/", 30)
|
|
e := New()
|
|
snap := e.Compile([]CompileInput{{Bucket: "bk", Rules: []*s3lifecycle.Rule{r}}}, CompileOptions{})
|
|
ev := &Event{Shape: EventShapeOriginalWrite, Bucket: "bk", Path: "x/o"}
|
|
if got := snap.MatchOriginalWrite(ev, s3lifecycle.DaysToDuration(30)); len(got) != 0 {
|
|
t.Fatalf("inactive action should not match, got %v", got)
|
|
}
|
|
snap.MarkActive(s3lifecycle.ActionKey{Bucket: "bk", RuleHash: s3lifecycle.RuleHash(r), ActionKind: s3lifecycle.ActionKindExpirationDays})
|
|
if got := snap.MatchOriginalWrite(ev, s3lifecycle.DaysToDuration(30)); len(got) != 1 {
|
|
t.Fatalf("post-markActive should be routable, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestMatchOriginalWrite_AbortMPUOnlyOnMPUInit(t *testing.T) {
|
|
r := &s3lifecycle.Rule{
|
|
ID: "mpu",
|
|
Status: s3lifecycle.StatusEnabled,
|
|
Prefix: ".uploads/",
|
|
AbortMPUDaysAfterInitiation: 7,
|
|
}
|
|
e := New()
|
|
snap := e.Compile([]CompileInput{{Bucket: "bk", Rules: []*s3lifecycle.Rule{r}}}, CompileOptions{PriorStates: activeAll("bk", []*s3lifecycle.Rule{r})})
|
|
|
|
// Non-MPU event under .uploads/ prefix: filtered out by shape gating.
|
|
got := snap.MatchOriginalWrite(&Event{Shape: EventShapeOriginalWrite, Bucket: "bk", Path: ".uploads/u1/", IsMPUInit: false}, s3lifecycle.DaysToDuration(7))
|
|
if len(got) != 0 {
|
|
t.Fatalf("non-MPU event should be filtered, got %v", got)
|
|
}
|
|
got = snap.MatchOriginalWrite(&Event{Shape: EventShapeOriginalWrite, Bucket: "bk", Path: ".uploads/u1/", IsMPUInit: true}, s3lifecycle.DaysToDuration(7))
|
|
if len(got) != 1 {
|
|
t.Fatalf("MPU init should fire, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestMatchPredicateChange_OnlyTagSensitiveActions(t *testing.T) {
|
|
rTag := &s3lifecycle.Rule{
|
|
ID: "tag",
|
|
Status: s3lifecycle.StatusEnabled,
|
|
Prefix: "x/",
|
|
ExpirationDays: 30,
|
|
FilterTags: map[string]string{"env": "prod"},
|
|
}
|
|
rPlain := ruleExpDays("plain", "x/", 30)
|
|
rules := []*s3lifecycle.Rule{rTag, rPlain}
|
|
e := New()
|
|
snap := e.Compile([]CompileInput{{Bucket: "bk", Rules: rules}}, CompileOptions{PriorStates: activeAll("bk", rules)})
|
|
|
|
// Predicate-change event: should match only the predicate-sensitive rule.
|
|
ev := &Event{Shape: EventShapePredicateChange, Bucket: "bk", Path: "x/o", Tags: map[string]string{"env": "prod"}}
|
|
got := snap.MatchPredicateChange(ev)
|
|
if len(got) != 1 {
|
|
t.Fatalf("only the tag-sensitive rule should match, got %v", got)
|
|
}
|
|
rh := s3lifecycle.RuleHash(rTag)
|
|
if got[0].RuleHash != rh {
|
|
t.Fatalf("expected match on tag-rule")
|
|
}
|
|
|
|
// Wrong shape: never matches.
|
|
if got := snap.MatchPredicateChange(&Event{Shape: EventShapeOriginalWrite, Bucket: "bk", Path: "x/o"}); len(got) != 0 {
|
|
t.Fatalf("wrong shape should be empty, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestMatchPath_BootstrapWalkerSeesAllActiveActions(t *testing.T) {
|
|
// One rule with three actions (90d Expiration, 30d NoncurrentDays,
|
|
// 7d AbortMPU). MatchPath returns every active action whose filter
|
|
// matches the path; the bootstrap walker iterates these and calls
|
|
// EvaluateAction per kind for the entry.
|
|
rule := &s3lifecycle.Rule{
|
|
ID: "multi",
|
|
Status: s3lifecycle.StatusEnabled,
|
|
Prefix: "x/",
|
|
ExpirationDays: 90,
|
|
NoncurrentVersionExpirationDays: 30,
|
|
AbortMPUDaysAfterInitiation: 7,
|
|
}
|
|
e := New()
|
|
snap := e.Compile([]CompileInput{{Bucket: "bk", Rules: []*s3lifecycle.Rule{rule}}}, CompileOptions{PriorStates: activeAll("bk", []*s3lifecycle.Rule{rule})})
|
|
|
|
got := snap.MatchPath("bk", "x/obj", nil)
|
|
if len(got) != 3 {
|
|
t.Fatalf("want all 3 active actions, got %v", got)
|
|
}
|
|
wantKinds := []s3lifecycle.ActionKind{
|
|
s3lifecycle.ActionKindExpirationDays,
|
|
s3lifecycle.ActionKindNoncurrentDays,
|
|
s3lifecycle.ActionKindAbortMPU,
|
|
}
|
|
gotKinds := []s3lifecycle.ActionKind{}
|
|
for _, k := range sortedKeys(got) {
|
|
gotKinds = append(gotKinds, k.ActionKind)
|
|
}
|
|
sort.Slice(wantKinds, func(i, j int) bool { return wantKinds[i] < wantKinds[j] })
|
|
if !reflect.DeepEqual(gotKinds, wantKinds) {
|
|
t.Fatalf("want kinds %v, got %v", wantKinds, gotKinds)
|
|
}
|
|
}
|
|
|
|
func TestMatchPath_PrefixMismatchExcludes(t *testing.T) {
|
|
rule := ruleExpDays("a", "logs/", 30)
|
|
e := New()
|
|
snap := e.Compile([]CompileInput{{Bucket: "bk", Rules: []*s3lifecycle.Rule{rule}}}, CompileOptions{PriorStates: activeAll("bk", []*s3lifecycle.Rule{rule})})
|
|
if got := snap.MatchPath("bk", "data/x", nil); len(got) != 0 {
|
|
t.Fatalf("prefix mismatch should reject, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestMatchPath_FilterRejectsByTag(t *testing.T) {
|
|
rule := &s3lifecycle.Rule{
|
|
ID: "tag",
|
|
Status: s3lifecycle.StatusEnabled,
|
|
Prefix: "x/",
|
|
ExpirationDays: 30,
|
|
FilterTags: map[string]string{"env": "prod"},
|
|
}
|
|
e := New()
|
|
snap := e.Compile([]CompileInput{{Bucket: "bk", Rules: []*s3lifecycle.Rule{rule}}}, CompileOptions{PriorStates: activeAll("bk", []*s3lifecycle.Rule{rule})})
|
|
// With ev passed, tag mismatch rejects.
|
|
ev := &Event{Bucket: "bk", Path: "x/obj", Tags: map[string]string{"env": "dev"}}
|
|
if got := snap.MatchPath("bk", "x/obj", ev); len(got) != 0 {
|
|
t.Fatalf("tag mismatch should reject, got %v", got)
|
|
}
|
|
// With ev nil (caller will fetch live state): prefix-only, returns the action.
|
|
if got := snap.MatchPath("bk", "x/obj", nil); len(got) != 1 {
|
|
t.Fatalf("ev=nil prefix-only path should return action, got %v", got)
|
|
}
|
|
}
|