fix: deleting an organisation removes its avatar and logo attachments

The Organisations screen's delete left the organisation's system_files
rows and blobs behind. FormBeforeDelete now deletes them in the delete's
transaction (attach.DeleteForOwner) and removes the blobs once it
commits, as the user delete does.
This commit is contained in:
2026-10-23 18:22:01 +02:00
parent f20a8d3b98
commit 25b9f2e35c
2 changed files with 46 additions and 5 deletions

View File

@@ -4,9 +4,13 @@ import (
"context"
"errors"
"regexp"
"strconv"
"git.golem15.com/golem15/sm-user-plugin/models"
"git.golem15.com/golem15/summercms/modules/cabana"
"git.golem15.com/golem15/summercms/modules/lagoon"
"git.golem15.com/golem15/summercms/modules/lagoon/attach"
"gocloud.dev/blob"
"gorm.io/gorm"
)
@@ -105,8 +109,10 @@ func (organisationsAdminController) RelationExtendManageQuery(_ context.Context,
// FormBeforeDelete releases the members of the organisation in the delete's
// transaction: organisation_id and organisation_role are cleared, on
// deactivated users too, so the delete does not fail on the users foreign key
// and no user is deleted.
func (organisationsAdminController) FormBeforeDelete(ctx context.Context, model any) error {
// and no user is deleted. The organisation's attachments (avatar and logo)
// are deleted in the same transaction, and their blobs once it commits, so
// no system_files row outlives the organisation row.
func (c organisationsAdminController) FormBeforeDelete(ctx context.Context, model any) error {
organisation, ok := model.(*models.Organisation)
if !ok || organisation == nil {
return errors.New("organisations admin: form model is not an organisation")
@@ -115,7 +121,42 @@ func (organisationsAdminController) FormBeforeDelete(ctx context.Context, model
if !ok {
return errors.New("organisations admin: a delete needs its transaction")
}
return tx.Session(&gorm.Session{NewDB: true}).WithContext(ctx).Unscoped().
if err := tx.Session(&gorm.Session{NewDB: true}).WithContext(ctx).Unscoped().
Model(&models.User{}).Where("organisation_id = ?", organisation.ID).
Updates(map[string]any{"organisation_id": nil, "organisation_role": nil}).Error
Updates(map[string]any{"organisation_id": nil, "organisation_role": nil}).Error; err != nil {
return err
}
var keys []string
if err := attach.DeleteForOwner(tx.Session(&gorm.Session{NewDB: true}).WithContext(ctx), organisation, strconv.FormatUint(uint64(organisation.ID), 10), func(blobKeys []string) error {
keys = append(keys, blobKeys...)
return nil
}); err != nil {
return err
}
bucket := c.bucket()
if bucket == nil || len(keys) == 0 {
return nil
}
id := organisation.ID
lagoon.AfterCommit(ctx, tx, func(ctx context.Context, _ *gorm.DB) {
// The rows are gone by now, so a blob that cannot be removed is
// orphaned, not lost data: log it and carry on.
if err := attach.DeleteKeys(ctx, bucket, keys); err != nil {
pluginLog.Warn("golem15.user: remove attachment blobs of a deleted organisation", "organisation_id", id, "err", err)
}
})
return nil
}
// bucket is the application's blob bucket, or nil when none is published.
func (c organisationsAdminController) bucket() *blob.Bucket {
app := c.app.application()
if app == nil {
return nil
}
bucket, ok := app.Lookup[*blob.Bucket]()
if !ok {
return nil
}
return bucket
}