// Copyright (c) 2024 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package database import ( "context" "database/sql" "fmt" "net/http" "slices" "time" "go.mau.fi/util/dbutil" "go.mau.fi/util/jsontime" "maunium.net/go/mautrix" "maunium.net/go/mautrix/crypto/attachment" "maunium.net/go/mautrix/id" ) const ( insertMediaQuery = ` INSERT INTO media (mxc, enc_file, file_name, mime_type, size, hash, error, thumbnail_size, thumbnail_hash, thumbnail_error) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (mxc) DO NOTHING ` upsertMediaQuery = ` INSERT INTO media (mxc, enc_file, file_name, mime_type, size, hash, error, thumbnail_size, thumbnail_hash, thumbnail_error) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (mxc) DO UPDATE SET enc_file = COALESCE(excluded.enc_file, media.enc_file), file_name = COALESCE(excluded.file_name, media.file_name), mime_type = COALESCE(excluded.mime_type, media.mime_type), size = COALESCE(excluded.size, media.size), hash = COALESCE(excluded.hash, media.hash), error = excluded.error, thumbnail_size = COALESCE(excluded.thumbnail_size, media.thumbnail_size), thumbnail_hash = COALESCE(excluded.thumbnail_hash, media.thumbnail_hash), thumbnail_error = excluded.thumbnail_error WHERE excluded.error IS NULL OR media.hash IS NULL ` getMediaQuery = ` SELECT mxc, enc_file, file_name, mime_type, size, hash, error, thumbnail_size, thumbnail_hash, thumbnail_error FROM media WHERE mxc = $1 ` addMediaReferenceQuery = ` INSERT INTO media_reference (event_rowid, media_mxc) VALUES ($1, $2) ON CONFLICT (event_rowid, media_mxc) DO NOTHING ` ) var mediaReferenceMassInserter = dbutil.NewMassInsertBuilder[*MediaReference, [0]any]( addMediaReferenceQuery, "($%d, $%d)", ) var mediaMassInserter = dbutil.NewMassInsertBuilder[*PlainMedia, [0]any]( "INSERT INTO media (mxc) VALUES ($1) ON CONFLICT (mxc) DO NOTHING", "($%d)", ) type MediaQuery struct { *dbutil.QueryHelper[*Media] } func (mq *MediaQuery) Add(ctx context.Context, cm *Media) error { return mq.Exec(ctx, insertMediaQuery, cm.sqlVariables()...) } func (mq *MediaQuery) AddReference(ctx context.Context, evtRowID EventRowID, mxc id.ContentURI) error { return mq.Exec(ctx, addMediaReferenceQuery, evtRowID, &mxc) } func (mq *MediaQuery) AddMany(ctx context.Context, medias []*PlainMedia) error { for chunk := range slices.Chunk(medias, 8000) { query, params := mediaMassInserter.Build([0]any{}, chunk) err := mq.Exec(ctx, query, params...) if err != nil { return err } } return nil } func (mq *MediaQuery) AddManyReferences(ctx context.Context, refs []*MediaReference) error { for chunk := range slices.Chunk(refs, 4000) { query, params := mediaReferenceMassInserter.Build([0]any{}, chunk) err := mq.Exec(ctx, query, params...) if err != nil { return err } } return nil } func (mq *MediaQuery) Put(ctx context.Context, cm *Media) error { return mq.Exec(ctx, upsertMediaQuery, cm.sqlVariables()...) } func (mq *MediaQuery) Get(ctx context.Context, mxc id.ContentURI) (*Media, error) { return mq.QueryOne(ctx, getMediaQuery, &mxc) } type MediaError struct { Matrix *mautrix.RespError `json:"data"` StatusCode int `json:"status_code"` ReceivedAt jsontime.UnixMilli `json:"received_at"` Attempts int `json:"attempts"` } const MaxMediaBackoff = 7 * 24 * time.Hour func (me *MediaError) backoff() time.Duration { return min(time.Duration(2<