mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-27 14:57:15 +00:00
4f277f836a
* fix(apps): an app upload is charged against the plan's storage cap (BUG-3653) App uploads were capped only per install (2,000 files, 1 GiB), so on Pad Cloud an app could add bytes past the workspace owner's storage_bytes limit that a human upload would be refused for. - FenceSpec.StorageLimit makes BeginFenced take the owner's storage plan-limit lock before its install read, as the lock's ordering rule requires. FencedTx.CheckStorageLimit charges bytes under it and returns ErrAppStorageLimit, which carries no totals, like the item cap. - Admission checks the cap at the declared size before any byte is read, so the common refusal writes no blob. The row insert charges it authoritatively, and an owner change mid-check is retried (ErrStorageOwnerMoved, now exported for this caller). - The server answers 403 storage_limit_reached. - appstore sets the flag from Options.PlanLimit, so self-hosted is unchanged. - The boundary allow-list takes the read-only storage helpers and the advisory lock, each with what it writes. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * docs(apps): the fence's lock-order comments name the storage lock an app upload takes first (BUG-3653) Codex round 1: two comments said the install FOR SHARE (fenced_tx.go) and the seq lock (fenced_attachments.go) come first, which a StorageLimit spec now precedes. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * fix(apps): an app upload whose row never commits reclaims its blob (BUG-3652, BUG-3653) With #2118 merged, the app door gets the same reclaim as the human doors. PendingUpload.Close, when the row never committed (its insert was refused, as by the storage cap after an admission another upload raced, or the caller's re-check refused it before Insert), hands the canonical blob to Options.Reclaimer while the hash guard is still held. The server implements it with reclaimRefusedBlob on the FS backend, which keeps the blob when another row or writer shares the hash. Options.Reclaimer is an interface, not a func field: the appstore boundary guard refuses function values in app code outright (rule 9), and refuses interface calls only where an internal/store type could answer them (rule 4). The server's implementation reads row existence for the hash and deletes a blob file; it writes no row. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * fix(apps): a pending upload attempts its blob reclaim once, cleared before the call (BUG-3652) Codex round: the storage key was cleared after the reclaimer returned. It is now captured and cleared first, so a second Close can never retry the reclaim after the hash guard was released, whatever the first attempt did. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
24 lines
738 B
Go
24 lines
738 B
Go
package server
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/PerpetualSoftware/pad/internal/store"
|
|
)
|
|
|
|
// BUG-3653 part B: an app upload past the plan's storage cap answers 403
|
|
// storage_limit_reached, with no totals (DOC-3371 §4).
|
|
func TestBUG3653_AppStorageLimitIs403WithoutTotals(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
writeAppStoreError(rr, store.ErrAppStorageLimit)
|
|
if rr.Code != http.StatusForbidden || !strings.Contains(rr.Body.String(), `"storage_limit_reached"`) {
|
|
t.Fatalf("response = %d %s, want 403 storage_limit_reached", rr.Code, rr.Body.String())
|
|
}
|
|
if strings.ContainsAny(rr.Body.String(), "0123456789") {
|
|
t.Errorf("the refusal carries a number: %s", rr.Body.String())
|
|
}
|
|
}
|