/* GoToSocial Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package migrations import ( "context" "database/sql" "time" previousgtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20211113114307_init" newgtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20220214175650_media_cleanup" "github.com/uptrace/bun" ) func init() { const batchSize = 100 up := func(ctx context.Context, db *bun.DB) error { // we need to migrate media attachments into a new table // see section 6 here: https://www.sqlite.org/lang_altertable.html return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { // create the new media attachments table if _, err := tx. NewCreateTable(). ModelTableExpr("new_media_attachments"). Model(&newgtsmodel.MediaAttachment{}). IfNotExists(). Exec(ctx); err != nil { return err } offset := time.Now() // migrate existing media attachments into new table migrateLoop: for { oldAttachments := []*previousgtsmodel.MediaAttachment{} err := tx. NewSelect(). Model(&oldAttachments). // subtract a millisecond from the offset just to make sure we're not getting double entries (this happens sometimes) Where("media_attachment.created_at < ?", offset.Add(-1*time.Millisecond)). Order("media_attachment.created_at DESC"). Limit(batchSize). Scan(ctx) if err != nil && err != sql.ErrNoRows { // there's been a real error return err } if err == sql.ErrNoRows || len(oldAttachments) == 0 { // we're finished migrating break migrateLoop } // update the offset to the createdAt time of the oldest media attachment in the slice offset = oldAttachments[len(oldAttachments)-1].CreatedAt // for every old attachment, we need to make a new attachment out of it by taking the same values newAttachments := []*newgtsmodel.MediaAttachment{} for _, old := range oldAttachments { new := &newgtsmodel.MediaAttachment{ ID: old.ID, CreatedAt: old.CreatedAt, UpdatedAt: old.UpdatedAt, StatusID: old.StatusID, URL: old.URL, RemoteURL: old.RemoteURL, Type: newgtsmodel.FileType(old.Type), FileMeta: newgtsmodel.FileMeta{ Original: newgtsmodel.Original{ Width: old.FileMeta.Original.Width, Height: old.FileMeta.Original.Height, Size: old.FileMeta.Original.Size, Aspect: old.FileMeta.Original.Aspect, }, Small: newgtsmodel.Small{ Width: old.FileMeta.Small.Width, Height: old.FileMeta.Small.Height, Size: old.FileMeta.Small.Size, Aspect: old.FileMeta.Small.Aspect, }, Focus: newgtsmodel.Focus{ X: old.FileMeta.Focus.X, Y: old.FileMeta.Focus.Y, }, }, AccountID: old.AccountID, Description: old.Description, ScheduledStatusID: old.ScheduledStatusID, Blurhash: old.Blurhash, Processing: newgtsmodel.ProcessingStatus(old.Processing), File: newgtsmodel.File{ Path: old.File.Path, ContentType: old.File.ContentType, FileSize: old.File.FileSize, UpdatedAt: old.File.UpdatedAt, }, Thumbnail: newgtsmodel.Thumbnail{ Path: old.Thumbnail.Path, ContentType: old.Thumbnail.ContentType, FileSize: old.Thumbnail.FileSize, UpdatedAt: old.Thumbnail.UpdatedAt, URL: old.Thumbnail.URL, RemoteURL: old.Thumbnail.RemoteURL, }, Avatar: old.Avatar, Header: old.Header, Cached: true, } newAttachments = append(newAttachments, new) } // insert this batch of new attachments, and then continue the loop if _, err := tx. NewInsert(). Model(&newAttachments). ModelTableExpr("new_media_attachments"). Exec(ctx); err != nil { return err } } // we have all the data we need from the old table, so we can safely drop it now if _, err := tx.NewDropTable().Model(&previousgtsmodel.MediaAttachment{}).Exec(ctx); err != nil { return err } // rename the new table to the same name as the old table was if _, err := tx.ExecContext(ctx, "ALTER TABLE new_media_attachments RENAME TO media_attachments;"); err != nil { return err } // add an index to the new table if _, err := tx. NewCreateIndex(). Model(&newgtsmodel.MediaAttachment{}). Index("media_attachments_id_idx"). Column("id"). Exec(ctx); err != nil { return err } return nil }) } down := func(ctx context.Context, db *bun.DB) error { return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { return nil }) } if err := Migrations.Register(up, down); err != nil { panic(err) } }