From 1b5467cf0e745773b5c8f5ff976c8be0b735b502 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 27 Jan 2025 23:11:22 +0200 Subject: [PATCH] media: add support for generating avatar thumbnails --- desktop/go.mod | 2 + desktop/go.sum | 5 + go.mod | 2 + go.sum | 6 + pkg/gomuks/media.go | 128 ++++++++++++++++-- pkg/hicli/database/media.go | 60 +++++--- .../database/upgrades/00-latest-revision.sql | 22 +-- .../database/upgrades/13-media-thumbnails.sql | 4 + 8 files changed, 189 insertions(+), 40 deletions(-) create mode 100644 pkg/hicli/database/upgrades/13-media-thumbnails.sql diff --git a/desktop/go.mod b/desktop/go.mod index 5dfcb56..62024b3 100644 --- a/desktop/go.mod +++ b/desktop/go.mod @@ -25,6 +25,7 @@ require ( github.com/coder/websocket v1.8.12 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cyphar/filepath-securejoin v0.2.5 // indirect + github.com/disintegration/imaging v1.6.2 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect github.com/ebitengine/purego v0.4.0-alpha.4 // indirect github.com/emirpasic/gods v1.18.1 // indirect @@ -63,6 +64,7 @@ require ( github.com/wailsapp/mimetype v1.4.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/yuin/goldmark v1.7.8 // indirect + go.mau.fi/webp v0.2.0 // indirect go.mau.fi/zeroconfig v0.1.3 // indirect golang.org/x/crypto v0.32.0 // indirect golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect diff --git a/desktop/go.sum b/desktop/go.sum index 2611697..6c747cd 100644 --- a/desktop/go.sum +++ b/desktop/go.sum @@ -44,6 +44,8 @@ github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxG github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/ebitengine/purego v0.4.0-alpha.4 h1:Y7yIV06Yo5M2BAdD7EVPhfp6LZ0tEcQo5770OhYUVes= @@ -166,6 +168,8 @@ github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= go.mau.fi/util v0.8.4 h1:mVKlJcXWfVo8ZW3f4vqtjGpqtZqJvX4ETekxawt2vnQ= go.mau.fi/util v0.8.4/go.mod h1:MOfGTs1CBuK6ERTcSL4lb5YU7/ujz09eOPVEDckuazY= +go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg= +go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q= go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM= go.mau.fi/zeroconfig v0.1.3/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -177,6 +181,7 @@ golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= diff --git a/go.mod b/go.mod index 404b444..2eb7d06 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/buckket/go-blurhash v1.1.0 github.com/chzyer/readline v1.5.1 github.com/coder/websocket v1.8.12 + github.com/disintegration/imaging v1.6.2 github.com/gabriel-vasile/mimetype v1.4.8 github.com/lucasb-eyer/go-colorful v1.2.0 github.com/mattn/go-sqlite3 v1.14.24 @@ -18,6 +19,7 @@ require ( github.com/tidwall/sjson v1.2.5 github.com/yuin/goldmark v1.7.8 go.mau.fi/util v0.8.4 + go.mau.fi/webp v0.2.0 go.mau.fi/zeroconfig v0.1.3 golang.org/x/crypto v0.32.0 golang.org/x/image v0.23.0 diff --git a/go.sum b/go.sum index 3e4d434..a1b722a 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= @@ -68,12 +70,15 @@ github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= go.mau.fi/util v0.8.4 h1:mVKlJcXWfVo8ZW3f4vqtjGpqtZqJvX4ETekxawt2vnQ= go.mau.fi/util v0.8.4/go.mod h1:MOfGTs1CBuK6ERTcSL4lb5YU7/ujz09eOPVEDckuazY= +go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg= +go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q= go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM= go.mau.fi/zeroconfig v0.1.3/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= @@ -84,6 +89,7 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/pkg/gomuks/media.go b/pkg/gomuks/media.go index fe03c96..79d922c 100644 --- a/pkg/gomuks/media.go +++ b/pkg/gomuks/media.go @@ -36,16 +36,17 @@ import ( "strings" "github.com/buckket/go-blurhash" + "github.com/disintegration/imaging" "github.com/gabriel-vasile/mimetype" "github.com/rs/zerolog" "github.com/rs/zerolog/hlog" - _ "golang.org/x/image/webp" - "go.mau.fi/util/exhttp" "go.mau.fi/util/ffmpeg" "go.mau.fi/util/jsontime" "go.mau.fi/util/ptr" "go.mau.fi/util/random" + cwebp "go.mau.fi/webp" + _ "golang.org/x/image/webp" "maunium.net/go/mautrix" "maunium.net/go/mautrix/crypto/attachment" "maunium.net/go/mautrix/event" @@ -59,7 +60,7 @@ var ErrBadGateway = mautrix.RespError{ StatusCode: http.StatusBadGateway, } -func (gmx *Gomuks) downloadMediaFromCache(ctx context.Context, w http.ResponseWriter, r *http.Request, entry *database.Media, force bool) bool { +func (gmx *Gomuks) downloadMediaFromCache(ctx context.Context, w http.ResponseWriter, r *http.Request, entry *database.Media, force, useThumbnail bool) bool { if !entry.UseCache() { if force { mautrix.MNotFound.WithMessage("Media not found in cache").Write(w) @@ -67,11 +68,12 @@ func (gmx *Gomuks) downloadMediaFromCache(ctx context.Context, w http.ResponseWr } return false } + etag := entry.ETag(useThumbnail) if entry.Error != nil { w.Header().Set("Mau-Cached-Error", "true") entry.Error.Write(w) return true - } else if r.Header.Get("If-None-Match") == entry.ETag() { + } else if etag != "" && r.Header.Get("If-None-Match") == etag { w.WriteHeader(http.StatusNotModified) return true } else if entry.MimeType != "" && r.URL.Query().Has("fallback") && !isAllowedAvatarMime(entry.MimeType) { @@ -79,7 +81,36 @@ func (gmx *Gomuks) downloadMediaFromCache(ctx context.Context, w http.ResponseWr return true } log := zerolog.Ctx(ctx) - cacheFile, err := os.Open(gmx.cacheEntryToPath(entry.Hash[:])) + hash := entry.Hash + if useThumbnail { + if entry.ThumbnailError != "" { + log.Debug().Str(zerolog.ErrorFieldName, entry.ThumbnailError).Msg("Returning cached thumbnail error") + w.WriteHeader(http.StatusInternalServerError) + return true + } + if entry.ThumbnailHash == nil { + err := gmx.generateAvatarThumbnail(entry, thumbnailMaxSize) + if err != nil { + log.Err(err).Msg("Failed to generate avatar thumbnail") + w.WriteHeader(http.StatusInternalServerError) + return true + } + } + hash = entry.ThumbnailHash + } + cacheFile, err := os.Open(gmx.cacheEntryToPath(hash[:])) + if useThumbnail && errors.Is(err, os.ErrNotExist) { + err = gmx.generateAvatarThumbnail(entry, thumbnailMaxSize) + if errors.Is(err, os.ErrNotExist) { + // Fall through to next error handler + } else if err != nil { + log.Err(err).Msg("Failed to generate avatar thumbnail") + w.WriteHeader(http.StatusInternalServerError) + return true + } else { + cacheFile, err = os.Open(gmx.cacheEntryToPath(hash[:])) + } + } if err != nil { if errors.Is(err, os.ErrNotExist) && !force { return false @@ -91,7 +122,7 @@ func (gmx *Gomuks) downloadMediaFromCache(ctx context.Context, w http.ResponseWr defer func() { _ = cacheFile.Close() }() - cacheEntryToHeaders(w, entry) + cacheEntryToHeaders(w, entry, useThumbnail) w.WriteHeader(http.StatusOK) _, err = io.Copy(w, cacheFile) if err != nil { @@ -105,13 +136,79 @@ func (gmx *Gomuks) cacheEntryToPath(hash []byte) string { return filepath.Join(gmx.CacheDir, "media", hashPath[0:2], hashPath[2:4], hashPath[4:]) } -func cacheEntryToHeaders(w http.ResponseWriter, entry *database.Media) { - w.Header().Set("Content-Type", entry.MimeType) - w.Header().Set("Content-Length", strconv.FormatInt(entry.Size, 10)) - w.Header().Set("Content-Disposition", mime.FormatMediaType(entry.ContentDisposition(), map[string]string{"filename": entry.FileName})) +func cacheEntryToHeaders(w http.ResponseWriter, entry *database.Media, thumbnail bool) { + if thumbnail { + w.Header().Set("Content-Type", "image/webp") + w.Header().Set("Content-Length", strconv.FormatInt(entry.ThumbnailSize, 10)) + w.Header().Set("Content-Disposition", "inline; filename=thumbnail.webp") + } else { + w.Header().Set("Content-Type", entry.MimeType) + w.Header().Set("Content-Length", strconv.FormatInt(entry.Size, 10)) + w.Header().Set("Content-Disposition", mime.FormatMediaType(entry.ContentDisposition(), map[string]string{"filename": entry.FileName})) + } w.Header().Set("Content-Security-Policy", "sandbox; default-src 'none'; script-src 'none'; media-src 'self';") w.Header().Set("Cache-Control", "max-age=2592000, immutable") - w.Header().Set("ETag", entry.ETag()) + w.Header().Set("ETag", entry.ETag(thumbnail)) +} + +const thumbnailMaxSize = 80 + +func (gmx *Gomuks) generateAvatarThumbnail(entry *database.Media, size int) error { + cacheFile, err := os.Open(gmx.cacheEntryToPath(entry.Hash[:])) + if err != nil { + return fmt.Errorf("failed to open full file: %w", err) + } + img, _, err := image.Decode(cacheFile) + if err != nil { + return fmt.Errorf("failed to decode image: %w", err) + } + //bounds := img.Bounds() + //origWidth := bounds.Dx() + //origHeight := bounds.Dy() + //var width, height int + //if origWidth == origHeight { + // width = size + // height = size + //} else if origWidth > origHeight { + // width = size + // height = origHeight * size / origWidth + //} else { + // width = origWidth * size / origHeight + // height = size + //} + + tempFile, err := os.CreateTemp(gmx.TempDir, "thumbnail-*") + if err != nil { + return fmt.Errorf("failed to create temporary file: %w", err) + } + defer func() { + _ = tempFile.Close() + _ = os.Remove(tempFile.Name()) + }() + thumbnailImage := imaging.Thumbnail(img, size, size, imaging.Lanczos) + fileHasher := sha256.New() + wrappedWriter := io.MultiWriter(fileHasher, tempFile) + err = cwebp.Encode(wrappedWriter, thumbnailImage, &cwebp.Options{Quality: 80}) + if err != nil { + return fmt.Errorf("failed to encode thumbnail: %w", err) + } + fileInfo, err := tempFile.Stat() + if err != nil { + return fmt.Errorf("failed to stat temporary file: %w", err) + } + entry.ThumbnailHash = (*[32]byte)(fileHasher.Sum(nil)) + entry.ThumbnailError = "" + entry.ThumbnailSize = fileInfo.Size() + cachePath := gmx.cacheEntryToPath(entry.ThumbnailHash[:]) + err = os.MkdirAll(filepath.Dir(cachePath), 0700) + if err != nil { + return fmt.Errorf("failed to create cache directory: %w", err) + } + err = os.Rename(tempFile.Name(), cachePath) + if err != nil { + return fmt.Errorf("failed to rename temporary file: %w", err) + } + return nil } type noErrorWriter struct { @@ -191,6 +288,7 @@ func (gmx *Gomuks) DownloadMedia(w http.ResponseWriter, r *http.Request) { } encrypted, _ := strconv.ParseBool(query.Get("encrypted")) + useThumbnail := query.Get("thumbnail") == "avatar" logVal := zerolog.Ctx(r.Context()).With(). Stringer("mxc_uri", mxc). @@ -211,7 +309,7 @@ func (gmx *Gomuks) DownloadMedia(w http.ResponseWriter, r *http.Request) { return } - if gmx.downloadMediaFromCache(ctx, w, r, cacheEntry, false) { + if gmx.downloadMediaFromCache(ctx, w, r, cacheEntry, false, useThumbnail) { return } @@ -301,8 +399,8 @@ func (gmx *Gomuks) DownloadMedia(w http.ResponseWriter, r *http.Request) { cacheEntry.Size = resp.ContentLength fileHasher := sha256.New() wrappedReader := io.TeeReader(reader, fileHasher) - if cacheEntry.Size > 0 && cacheEntry.EncFile == nil { - cacheEntryToHeaders(w, cacheEntry) + if cacheEntry.Size > 0 && cacheEntry.EncFile == nil && !useThumbnail { + cacheEntryToHeaders(w, cacheEntry, useThumbnail) w.WriteHeader(http.StatusOK) wrappedReader = io.TeeReader(wrappedReader, &noErrorWriter{w}) w = nil @@ -342,7 +440,7 @@ func (gmx *Gomuks) DownloadMedia(w http.ResponseWriter, r *http.Request) { return } if w != nil { - gmx.downloadMediaFromCache(ctx, w, r, cacheEntry, true) + gmx.downloadMediaFromCache(ctx, w, r, cacheEntry, true, useThumbnail) } } diff --git a/pkg/hicli/database/media.go b/pkg/hicli/database/media.go index edc6698..e603025 100644 --- a/pkg/hicli/database/media.go +++ b/pkg/hicli/database/media.go @@ -23,24 +23,27 @@ import ( const ( insertMediaQuery = ` - INSERT INTO media (mxc, enc_file, file_name, mime_type, size, hash, error) - VALUES ($1, $2, $3, $4, $5, $6, $7) + 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) - VALUES ($1, $2, $3, $4, $5, $6, $7) + 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 + 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 + SELECT mxc, enc_file, file_name, mime_type, size, hash, error, thumbnail_size, thumbnail_hash, thumbnail_error FROM media WHERE mxc = $1 ` @@ -137,9 +140,22 @@ type Media struct { Size int64 Hash *[32]byte Error *MediaError + + ThumbnailError string + ThumbnailSize int64 + ThumbnailHash *[32]byte } -func (m *Media) ETag() string { +func (m *Media) ETag(thumbnail bool) string { + if m == nil { + return "" + } + if thumbnail { + if m.ThumbnailHash == nil { + return "" + } + return fmt.Sprintf(`"%x"`, m.ThumbnailHash) + } if m.Hash == nil { return "" } @@ -151,14 +167,18 @@ func (m *Media) UseCache() bool { } func (m *Media) sqlVariables() []any { - var hash []byte + var hash, thumbnailHash []byte if m.Hash != nil { hash = m.Hash[:] } + if m.ThumbnailHash != nil { + thumbnailHash = m.ThumbnailHash[:] + } return []any{ &m.MXC, dbutil.JSONPtr(m.EncFile), dbutil.StrPtr(m.FileName), dbutil.StrPtr(m.MimeType), dbutil.NumPtr(m.Size), hash, dbutil.JSONPtr(m.Error), + dbutil.NumPtr(m.ThumbnailSize), thumbnailHash, dbutil.StrPtr(m.ThumbnailError), } } @@ -172,19 +192,27 @@ var safeMimes = []string{ } func (m *Media) Scan(row dbutil.Scannable) (*Media, error) { - var mimeType, fileName sql.NullString - var size sql.NullInt64 - var hash []byte - err := row.Scan(&m.MXC, dbutil.JSON{Data: &m.EncFile}, &fileName, &mimeType, &size, &hash, dbutil.JSON{Data: &m.Error}) + var mimeType, fileName, thumbnailError sql.NullString + var size, thumbnailSize sql.NullInt64 + var hash, thumbnailHash []byte + err := row.Scan( + &m.MXC, dbutil.JSON{Data: &m.EncFile}, &fileName, &mimeType, &size, + &hash, dbutil.JSON{Data: &m.Error}, &thumbnailSize, &thumbnailHash, &thumbnailError, + ) if err != nil { return nil, err } m.MimeType = mimeType.String m.FileName = fileName.String m.Size = size.Int64 + m.ThumbnailSize = thumbnailSize.Int64 + m.ThumbnailError = thumbnailError.String if len(hash) == 32 { m.Hash = (*[32]byte)(hash) } + if len(thumbnailHash) == 32 { + m.ThumbnailHash = (*[32]byte)(thumbnailHash) + } return m, nil } diff --git a/pkg/hicli/database/upgrades/00-latest-revision.sql b/pkg/hicli/database/upgrades/00-latest-revision.sql index 847afae..9b87844 100644 --- a/pkg/hicli/database/upgrades/00-latest-revision.sql +++ b/pkg/hicli/database/upgrades/00-latest-revision.sql @@ -1,4 +1,4 @@ --- v0 -> v12 (compatible with v10+): Latest revision +-- v0 -> v13 (compatible with v10+): Latest revision CREATE TABLE account ( user_id TEXT NOT NULL PRIMARY KEY, device_id TEXT NOT NULL, @@ -18,7 +18,7 @@ CREATE TABLE room ( name_quality INTEGER NOT NULL DEFAULT 0, avatar TEXT, explicit_avatar INTEGER NOT NULL DEFAULT 0, - dm_user_id TEXT, + dm_user_id TEXT, topic TEXT, canonical_alias TEXT, lazy_load_summary TEXT, @@ -212,13 +212,17 @@ BEGIN END; CREATE TABLE media ( - mxc TEXT NOT NULL PRIMARY KEY, - enc_file TEXT, - file_name TEXT, - mime_type TEXT, - size INTEGER, - hash BLOB, - error TEXT + mxc TEXT NOT NULL PRIMARY KEY, + enc_file TEXT, + file_name TEXT, + mime_type TEXT, + size INTEGER, + hash BLOB, + error TEXT, + + thumbnail_size INTEGER, + thumbnail_hash BLOB, + thumbnail_error TEXT ) STRICT; CREATE TABLE media_reference ( diff --git a/pkg/hicli/database/upgrades/13-media-thumbnails.sql b/pkg/hicli/database/upgrades/13-media-thumbnails.sql new file mode 100644 index 0000000..ff9d88c --- /dev/null +++ b/pkg/hicli/database/upgrades/13-media-thumbnails.sql @@ -0,0 +1,4 @@ +-- v13 (compatible with v10+): Add columns for media thumbnails +ALTER TABLE media ADD COLUMN thumbnail_size INTEGER; +ALTER TABLE media ADD COLUMN thumbnail_hash BLOB; +ALTER TABLE media ADD COLUMN thumbnail_error TEXT;