feat(12.2-01): add deferred bindings, guarded upload store and purge

- deferred_bindings migration set under summercms.deferred with backend_user_id
- lagoon.DeferredBind/Unbind/Bindings/Forget/Slaves scoped by DeferredKey
- lagoon.PurgeDeferred with SKIP LOCKED batches and after-commit blob deletes
- attach.Store with the ported image guard, extension and MIME limits
- attach.Relation, attach.HasRelations, attach.BlobKeys, File.ThumbKey
- lagoon README and attachments docs
This commit is contained in:
Jakub Zych
2026-10-02 17:36:43 +02:00
parent 79e2a43095
commit 19f4cf8232
14 changed files with 1280 additions and 15 deletions

View File

@@ -126,6 +126,19 @@ var encodeImage = defaultEncodeImage
// Thumb returns the public URL of a lazily generated thumbnail. The second
// call for the same dimensions hits the existing blob and does not resize.
// It is PublicURL of ThumbKey; see ThumbKey for the generation rules.
func (f *File) Thumb(ctx context.Context, bucket *blob.Bucket, w, h int, mode string) (string, error) {
key, err := f.ThumbKey(ctx, bucket, w, h, mode)
if err != nil {
return "", err
}
return PublicURL(key), nil
}
// ThumbKey returns the blob key of a lazily generated thumbnail, generating
// it on first use, so a caller that must not emit a public URL (a protected
// file) can stream the thumbnail itself. The second call for the same
// dimensions hits the existing blob and does not resize.
//
// As WinterCMS's File::makeThumb does, an original that is missing from
// the bucket, does not decode, or declares more than 4096 by 4096 pixels
@@ -133,7 +146,7 @@ var encodeImage = defaultEncodeImage
// thumbnail, and the failure is logged at warn level instead of returned:
// one unusable upload never fails the listings that show it. An invalid
// mode or size, a storage error and an encode failure are still errors.
func (f *File) Thumb(ctx context.Context, bucket *blob.Bucket, w, h int, mode string) (string, error) {
func (f *File) ThumbKey(ctx context.Context, bucket *blob.Bucket, w, h int, mode string) (string, error) {
if f == nil {
return "", fmt.Errorf("attach: file is nil")
}
@@ -162,7 +175,7 @@ func (f *File) Thumb(ctx context.Context, bucket *blob.Bucket, w, h int, mode st
return "", fmt.Errorf("attach: thumb exists: %w", err)
}
if exists {
return PublicURL(thumbKey), nil
return thumbKey, nil
}
origKey := part + f.DiskName
r, err := bucket.NewReader(ctx, origKey, nil)
@@ -219,7 +232,7 @@ func (f *File) Thumb(ctx context.Context, bucket *blob.Bucket, w, h int, mode st
}
return "", closeErr
}
return PublicURL(thumbKey), nil
return thumbKey, nil
}
var errOriginalTooLarge = errors.New("original image is too large")
@@ -239,12 +252,12 @@ var BrokenImagePNG = func() []byte {
}()
// brokenThumb is the catch branch of WinterCMS's File::makeThumb: log the
// reason, store BrokenImagePNG under the thumbnail key and return its URL.
// reason, store BrokenImagePNG under the thumbnail key and return that key.
func brokenThumb(ctx context.Context, bucket *blob.Bucket, f *File, thumbKey string, reason error) (string, error) {
slog.Default().WarnContext(ctx, "attach: thumbnail original is unusable, storing the broken-image picture",
slog.Uint64("file_id", uint64(f.ID)), slog.String("error", reason.Error()))
if err := bucket.WriteAll(ctx, thumbKey, BrokenImagePNG, &blob.WriterOptions{ContentType: "image/png"}); err != nil {
return "", fmt.Errorf("attach: broken-image thumb: %w", err)
}
return PublicURL(thumbKey), nil
return thumbKey, nil
}