| 1 | diff --git a/internal/configport/configport.go b/internal/configport/configport.go |
| 2 | index e0dc5bf..68d89a4 100644 |
| 3 | --- a/internal/configport/configport.go |
| 4 | +++ b/internal/configport/configport.go |
| 5 | @@ -159,7 +159,7 @@ func warnPolicy(c *Config) { |
| 6 | c.Warnings = append(c.Warnings, "policy.lease: legacy lease imported disabled; explicit owner mapping required") |
| 7 | } else if key == "owner" { |
| 8 | c.Warnings = append(c.Warnings, "policy.owner: never imported; tenant owner must be assigned by the operator") |
| 9 | - } else if key == "succession" || key == "customHosts" || key == "fuel" || strings.Contains(strings.ToLower(key), "limit") || strings.HasPrefix(strings.ToLower(key), "free") || strings.HasPrefix(strings.ToLower(key), "sats") { |
| 10 | + } else if key != "fileLimits" && (key == "succession" || key == "customHosts" || key == "fuel" || strings.Contains(strings.ToLower(key), "limit") || strings.HasPrefix(strings.ToLower(key), "free") || strings.HasPrefix(strings.ToLower(key), "sats")) { |
| 11 | c.Warnings = append(c.Warnings, "policy."+key+": removed from self-hosted runtime") |
| 12 | } |
| 13 | } |
| 14 | diff --git a/internal/configport/file_limits_test.go b/internal/configport/file_limits_test.go |
| 15 | new file mode 100644 |
| 16 | index 0000000..6a154f2 |
| 17 | --- /dev/null |
| 18 | +++ b/internal/configport/file_limits_test.go |
| 19 | @@ -0,0 +1,24 @@ |
| 20 | +package configport |
| 21 | + |
| 22 | +import ( |
| 23 | + "testing" |
| 24 | + |
| 25 | + "github.com/FelineStateMachine/tinyrelay/internal/policy" |
| 26 | +) |
| 27 | + |
| 28 | +func TestImportFileLimits(t *testing.T) { |
| 29 | + doc, err := Parse([]byte(`{"format":"bind.ws/relay-config/2","policy":{"fileLimits":{"maxFileBytes":1024,"userStorageBytes":4096}}}`)) |
| 30 | + if err != nil { |
| 31 | + t.Fatal(err) |
| 32 | + } |
| 33 | + if len(doc.Warnings) != 0 { |
| 34 | + t.Fatalf("file limits should be portable: %v", doc.Warnings) |
| 35 | + } |
| 36 | + got, err := finalPolicy(policy.Defaults("owner"), doc, "") |
| 37 | + if err != nil { |
| 38 | + t.Fatal(err) |
| 39 | + } |
| 40 | + if got.FileLimits.MaxFileBytes != 1024 || got.FileLimits.UserStorageBytes != 4096 { |
| 41 | + t.Fatalf("file limits lost during import: %+v", got.FileLimits) |
| 42 | + } |
| 43 | +} |
| 44 | diff --git a/internal/policy/file_limits_test.go b/internal/policy/file_limits_test.go |
| 45 | new file mode 100644 |
| 46 | index 0000000..0bb2027 |
| 47 | --- /dev/null |
| 48 | +++ b/internal/policy/file_limits_test.go |
| 49 | @@ -0,0 +1,27 @@ |
| 50 | +package policy |
| 51 | + |
| 52 | +import ( |
| 53 | + "encoding/json" |
| 54 | + "testing" |
| 55 | +) |
| 56 | + |
| 57 | +func TestFileLimitsPatchAndValidation(t *testing.T) { |
| 58 | + p := Defaults("owner") |
| 59 | + p.FileLimits = FileLimits{MaxFileBytes: 1024, UserStorageBytes: 4096} |
| 60 | + updated, err := Patch(p, map[string]json.RawMessage{"fileLimits": json.RawMessage(`{"maxFileBytes":2048}`)}) |
| 61 | + if err != nil { |
| 62 | + t.Fatal(err) |
| 63 | + } |
| 64 | + if updated.FileLimits.MaxFileBytes != 2048 || updated.FileLimits.UserStorageBytes != 4096 { |
| 65 | + t.Fatalf("partial limit patch lost settings: %+v", updated.FileLimits) |
| 66 | + } |
| 67 | + for _, patch := range []string{`{"maxFileBytes":-1}`, `{"userStorageBytes":-1}`} { |
| 68 | + if _, err := Patch(p, map[string]json.RawMessage{"fileLimits": json.RawMessage(patch)}); err == nil { |
| 69 | + t.Fatalf("accepted negative limit: %s", patch) |
| 70 | + } |
| 71 | + } |
| 72 | + updated, err = Patch(p, map[string]json.RawMessage{"fileLimits": json.RawMessage(`{"maxFileBytes":0,"userStorageBytes":0}`)}) |
| 73 | + if err != nil || updated.FileLimits != (FileLimits{}) { |
| 74 | + t.Fatalf("zero should disable limits: %+v, %v", updated.FileLimits, err) |
| 75 | + } |
| 76 | +} |
| 77 | diff --git a/internal/policy/policy.go b/internal/policy/policy.go |
| 78 | index 1bc0f8c..49599ad 100644 |
| 79 | --- a/internal/policy/policy.go |
| 80 | +++ b/internal/policy/policy.go |
| 81 | @@ -62,6 +62,12 @@ type Inbox struct { |
| 82 | Targeted bool `json:"targeted"` |
| 83 | } |
| 84 | |
| 85 | +// FileLimits controls a tenant's file storage allowances. Zero is unlimited. |
| 86 | +type FileLimits struct { |
| 87 | + MaxFileBytes int64 `json:"maxFileBytes"` |
| 88 | + UserStorageBytes int64 `json:"userStorageBytes"` |
| 89 | +} |
| 90 | + |
| 91 | type CustomHost struct { |
| 92 | Host string `json:"host"` |
| 93 | ID string `json:"id"` |
| 94 | @@ -109,6 +115,7 @@ type Policy struct { |
| 95 | Features Features `json:"features"` |
| 96 | PushCallbacks []string `json:"pushCallbacks"` |
| 97 | PrivatePeers []string `json:"privatePeers,omitempty"` |
| 98 | + FileLimits FileLimits `json:"fileLimits"` |
| 99 | LetteredNips bool `json:"letteredNips"` |
| 100 | Delivery Delivery `json:"delivery"` |
| 101 | Inbox Inbox `json:"inbox"` |
| 102 | @@ -126,6 +133,9 @@ func Defaults(owner string) Policy { |
| 103 | } |
| 104 | |
| 105 | func Validate(p Policy) error { |
| 106 | + if p.FileLimits.MaxFileBytes < 0 || p.FileLimits.UserStorageBytes < 0 { |
| 107 | + return errors.New("fileLimits: byte allowances cannot be negative") |
| 108 | + } |
| 109 | if p.Writes != "open" && p.Writes != "allowlist" && p.Writes != "wot" && p.Writes != "owner" { |
| 110 | return fmt.Errorf("writes: invalid rule %q", p.Writes) |
| 111 | } |
| 112 | @@ -203,7 +213,7 @@ func Patch(cur Policy, raw map[string]json.RawMessage) (Policy, error) { |
| 113 | if _, ok := merged[key]; !ok { |
| 114 | continue |
| 115 | } |
| 116 | - if key == "features" || key == "notify" || key == "memberInvites" || key == "delivery" || key == "inbox" { |
| 117 | + if key == "features" || key == "notify" || key == "memberInvites" || key == "delivery" || key == "inbox" || key == "fileLimits" { |
| 118 | var patchMap, currentMap map[string]json.RawMessage |
| 119 | if json.Unmarshal(value, &patchMap) == nil && json.Unmarshal(merged[key], ¤tMap) == nil { |
| 120 | if key == "features" { |