Files
zooqueen dd74d3e6d6 Replace AGPL MinIO fork with Apache-2.0 SeaweedFS fork; rebrand CLI weed->s3
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.
2026-06-18 23:41:25 -07:00

134 lines
4.2 KiB
Go

package engine
import (
"bytes"
"sort"
"sync/atomic"
"time"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3lifecycle"
)
var snapshotIDSeq atomic.Uint64
// CompileInput: at most one entry per Bucket. Duplicates would overwrite
// earlier entries' BucketIndex.
type CompileInput struct {
Bucket string
Rules []*s3lifecycle.Rule
Versioned bool
}
// PriorState carries durable per-action state into Compile. Missing keys
// are treated as bootstrap_complete=false.
type PriorState struct {
BootstrapComplete bool
Mode RuleMode
}
// MetaLogRetention=0 means unbounded; the retention gate doesn't trip.
type CompileOptions struct {
MetaLogRetention time.Duration
BootstrapLookbackMin time.Duration
PriorStates map[s3lifecycle.ActionKey]PriorState
}
const defaultBootstrapLookbackMin = 5 * s3lifecycle.SmallDelay
func (e *Engine) Compile(inputs []CompileInput, opts CompileOptions) *Snapshot {
if opts.BootstrapLookbackMin == 0 {
opts.BootstrapLookbackMin = defaultBootstrapLookbackMin
}
snap := &Snapshot{
id: snapshotIDSeq.Add(1),
buckets: make(map[string]*BucketIndex),
actions: make(map[s3lifecycle.ActionKey]*CompiledAction),
originalDelayGroups: make(map[time.Duration][]s3lifecycle.ActionKey),
dateActions: make(map[s3lifecycle.ActionKey]time.Time),
}
for _, in := range inputs {
bi := &BucketIndex{bucket: in.Bucket, versioned: in.Versioned}
snap.buckets[in.Bucket] = bi
for _, rule := range in.Rules {
ruleHash := s3lifecycle.RuleHash(rule)
for _, kind := range s3lifecycle.RuleActionKinds(rule) {
key := s3lifecycle.ActionKey{Bucket: in.Bucket, RuleHash: ruleHash, ActionKind: kind}
// Durable Mode wins; decideMode is only the seed for new
// or legacy-zero actions. Otherwise lag-fallback / operator
// SCAN_ONLY would re-promote on every rebuild.
prior, hasPrior := opts.PriorStates[key]
mode := prior.Mode
if !hasPrior || mode == ModeUnspecified {
mode = decideMode(rule, kind, opts.MetaLogRetention, opts.BootstrapLookbackMin)
}
// Active gates routing: MatchPath / MatchOriginalWrite skip
// !IsActive actions. ScanAtDate's only dispatch path is the
// bootstrap walk's MatchPath call, so the action must be
// considered active there or its rule is silently a no-op.
// Bootstrap-completion state is per-action and event-driven-
// shaped; date-based actions don't need a bootstrap rendezvous.
active := mode == ModeScanAtDate ||
(prior.BootstrapComplete && mode == ModeEventDriven)
ca := &CompiledAction{
Rule: rule,
Bucket: in.Bucket,
Key: key,
Delay: s3lifecycle.MinTriggerAge(rule, kind),
PredicateSensitive: rulePredicateSensitive(rule),
Mode: mode,
}
if active {
ca.markActive()
}
snap.actions[key] = ca
bi.actionKeys = append(bi.actionKeys, key)
// Index every action regardless of `active`; routing
// re-filters on IsActive() so MarkActive flips are visible
// without a recompile.
if mode == ModeScanAtDate {
snap.dateActions[key] = rule.ExpirationDate
}
if mode == ModeEventDriven {
snap.originalDelayGroups[ca.Delay] = append(snap.originalDelayGroups[ca.Delay], key)
if ca.PredicateSensitive {
snap.predicateActions = append(snap.predicateActions, key)
}
}
}
}
}
snap.allActionsSorted = make([]*CompiledAction, 0, len(snap.actions))
for _, a := range snap.actions {
snap.allActionsSorted = append(snap.allActionsSorted, a)
}
sort.Slice(snap.allActionsSorted, func(i, j int) bool {
a, b := snap.allActionsSorted[i], snap.allActionsSorted[j]
if a.Bucket != b.Bucket {
return a.Bucket < b.Bucket
}
if c := bytes.Compare(a.Key.RuleHash[:], b.Key.RuleHash[:]); c != 0 {
return c < 0
}
return a.Key.ActionKind < b.Key.ActionKind
})
e.current.Store(snap)
return snap
}
// rulePredicateSensitive: only tag filters can flip post-PUT. Size is
// immutable once written; a size change is a fresh write through the
// original-write stream.
func rulePredicateSensitive(rule *s3lifecycle.Rule) bool {
if rule == nil {
return false
}
return len(rule.FilterTags) > 0
}