mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-19 18:13:41 -05:00
media: add support for generating avatar thumbnails
This commit is contained in:
parent
66c850717a
commit
1b5467cf0e
8 changed files with 189 additions and 40 deletions
|
@ -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
|
||||
|
|
|
@ -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=
|
||||
|
|
2
go.mod
2
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
|
||||
|
|
6
go.sum
6
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=
|
||||
|
|
|
@ -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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
@ -218,7 +218,11 @@ CREATE TABLE media (
|
|||
mime_type TEXT,
|
||||
size INTEGER,
|
||||
hash BLOB,
|
||||
error TEXT
|
||||
error TEXT,
|
||||
|
||||
thumbnail_size INTEGER,
|
||||
thumbnail_hash BLOB,
|
||||
thumbnail_error TEXT
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE media_reference (
|
||||
|
|
4
pkg/hicli/database/upgrades/13-media-thumbnails.sql
Normal file
4
pkg/hicli/database/upgrades/13-media-thumbnails.sql
Normal file
|
@ -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;
|
Loading…
Add table
Reference in a new issue