fix(12.2): close code-review blockers on uploads, JSON caps, and pivot fill
Keep form save behind in-flight uploads, make retries idempotent via X-Upload-Id, cap remaining JSON bodies, and surface pending pivot type errors instead of zeroing them. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -364,20 +364,21 @@ func compileFileFields(pluginID string, cc *CompiledController) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkFileLimits refuses a maxFilesize above http.body_limits.upload_bytes,
|
||||
// as WinterCMS refuses one above upload_max_filesize.
|
||||
// checkFileLimits refuses a maxFilesize whose file plus multipart framing
|
||||
// cannot fit in http.body_limits.upload_bytes. Equality is not enough:
|
||||
// the request cap is the whole multipart body.
|
||||
func checkFileLimits(reg *Registry, uploadBytes int64) error {
|
||||
if reg == nil || uploadBytes <= 0 {
|
||||
return nil
|
||||
}
|
||||
for _, cc := range reg.byID {
|
||||
for _, cf := range cc.files {
|
||||
if cf.maxBytes > uploadBytes {
|
||||
if cf.maxBytes > 0 && cf.maxBytes+multipartOverhead > uploadBytes {
|
||||
path := ""
|
||||
if cc.Form != nil {
|
||||
path = cc.Form.fieldsPath
|
||||
}
|
||||
return bootErr(cc.PluginID, controllerID(cc), path, fmt.Errorf("field %s: maxFilesize exceeds http.body_limits.upload_bytes", cf.name))
|
||||
return bootErr(cc.PluginID, controllerID(cc), path, fmt.Errorf("field %s: maxFilesize plus multipart overhead exceeds http.body_limits.upload_bytes", cf.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -510,6 +511,10 @@ func parentFileScope(ctx context.Context, tx *gorm.DB, r *http.Request, cc *Comp
|
||||
// attached to the owner minus the session's pending removals, plus the
|
||||
// session's pending uploads, in sort_order then id order.
|
||||
func (sc *fileScope) visibleFiles(tx *gorm.DB) ([]attach.File, map[uint]bool, error) {
|
||||
return sc.visibleFilesLocked(tx, false)
|
||||
}
|
||||
|
||||
func (sc *fileScope) visibleFilesLocked(tx *gorm.DB, lock bool) ([]attach.File, map[uint]bool, error) {
|
||||
q := tx.Session(&gorm.Session{NewDB: true}).Model(&attach.File{})
|
||||
var attached *gorm.DB
|
||||
if sc.ownerID > 0 {
|
||||
@@ -534,6 +539,9 @@ func (sc *fileScope) visibleFiles(tx *gorm.DB) ([]attach.File, map[uint]bool, er
|
||||
default:
|
||||
return nil, nil, nil
|
||||
}
|
||||
if lock {
|
||||
q = q.Clauses(clause.Locking{Strength: "UPDATE"})
|
||||
}
|
||||
var files []attach.File
|
||||
if err := q.Order("sort_order").Order("id").Find(&files).Error; err != nil {
|
||||
return nil, nil, err
|
||||
@@ -547,6 +555,60 @@ func (sc *fileScope) visibleFiles(tx *gorm.DB) ([]attach.File, map[uint]bool, er
|
||||
return files, isPending, nil
|
||||
}
|
||||
|
||||
const uploadIDHeader = "X-Upload-Id"
|
||||
const maxUploadIDLen = 64
|
||||
|
||||
func parseUploadID(r *http.Request) string {
|
||||
raw := strings.TrimSpace(r.Header.Get(uploadIDHeader))
|
||||
if raw == "" || len(raw) > maxUploadIDLen {
|
||||
return ""
|
||||
}
|
||||
for _, c := range raw {
|
||||
if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '-' && c != '_' {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// fileForUploadID returns the pending file already stored for this
|
||||
// session, field and client upload id, or nil when none exists.
|
||||
func (sc *fileScope) fileForUploadID(ctx context.Context, tx *gorm.DB, uploadID string) (*attach.File, error) {
|
||||
if uploadID == "" || !sc.hasKey {
|
||||
return nil, nil
|
||||
}
|
||||
rows, err := lagoon.DeferredBindings(ctx, tx, sc.key, []string{sc.file.name})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range rows {
|
||||
if !row.IsBind || row.SlaveType != lagoon.DeferredFileType {
|
||||
continue
|
||||
}
|
||||
env, err := row.Envelope()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if env.UploadID != uploadID {
|
||||
continue
|
||||
}
|
||||
id, err := strconv.ParseUint(row.SlaveID, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var f attach.File
|
||||
err = tx.Session(&gorm.Session{NewDB: true, Context: ctx}).Where("id = ?", uint(id)).Take(&f).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &f, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// fileItem projects a stored file. Public URLs are emitted only for a
|
||||
// public relation (never for a protected file, D-10).
|
||||
func fileItem(ctx context.Context, bucket *blob.Bucket, cf *compiledFile, f *attach.File, pending bool) FileItem {
|
||||
@@ -643,6 +705,7 @@ func (s *service) fileUploadOn(w http.ResponseWriter, r *http.Request, cc *Compi
|
||||
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
|
||||
return
|
||||
}
|
||||
uploadID := parseUploadID(r)
|
||||
body := &bodyReader{r: http.MaxBytesReader(w, r.Body, s.uploadCap(cf))}
|
||||
r.Body = io.NopCloser(body)
|
||||
mr, err := r.MultipartReader()
|
||||
@@ -667,6 +730,12 @@ func (s *service) fileUploadOn(w http.ResponseWriter, r *http.Request, cc *Compi
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing, err := sc.fileForUploadID(ctx, tx, uploadID); err != nil {
|
||||
return lifecycleFailure(cc, err)
|
||||
} else if existing != nil {
|
||||
item = fileItem(ctx, bucket, cf, existing, true)
|
||||
return nil
|
||||
}
|
||||
if cf.relation.Many && cf.maxFiles > 0 {
|
||||
files, _, err := sc.visibleFiles(tx)
|
||||
if err != nil {
|
||||
@@ -688,7 +757,11 @@ func (s *service) fileUploadOn(w http.ResponseWriter, r *http.Request, cc *Compi
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := lagoon.DeferredBind(ctx, tx, sc.key, cf.name, lagoon.DeferredFileType, uitoa(f.ID), nil); err != nil {
|
||||
var env *lagoon.DeferredEnvelope
|
||||
if uploadID != "" {
|
||||
env = &lagoon.DeferredEnvelope{UploadID: uploadID}
|
||||
}
|
||||
if err := lagoon.DeferredBind(ctx, tx, sc.key, cf.name, lagoon.DeferredFileType, uitoa(f.ID), env); err != nil {
|
||||
return lifecycleFailure(cc, err)
|
||||
}
|
||||
item = fileItem(ctx, bucket, cf, f, true)
|
||||
@@ -1245,7 +1318,7 @@ func (s *service) fileReorderOn(w http.ResponseWriter, r *http.Request, cc *Comp
|
||||
bucket := s.bucket()
|
||||
var items []FileItem
|
||||
ok := s.withFileScope(w, r, cc, fr, func(ctx context.Context, tx *gorm.DB, sc *fileScope) error {
|
||||
files, _, err := sc.visibleFiles(tx)
|
||||
files, _, err := sc.visibleFilesLocked(tx, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user