fix(matrix): stream inbound media downloads to disk (#1436)

This commit is contained in:
Horsley Lee 2026-03-12 23:48:26 +08:00 committed by GitHub
parent d18a319b0c
commit 8f49af99f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 74 additions and 7 deletions

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"html" "html"
"io"
"mime" "mime"
"net/url" "net/url"
"os" "os"
@ -726,17 +727,23 @@ func (c *MatrixChannel) downloadMedia(
reqCtx, cancel := context.WithTimeout(dlCtx, 20*time.Second) reqCtx, cancel := context.WithTimeout(dlCtx, 20*time.Second)
defer cancel() defer cancel()
data, err := c.client.DownloadBytes(reqCtx, parsed) resp, err := c.client.Download(reqCtx, parsed)
if err != nil { if err != nil {
return "", err return "", err
} }
defer resp.Body.Close()
reader := resp.Body
readerClose := func() error { return nil }
// Encrypted attachments put URL in msgEvt.File and require client-side decryption. // Encrypted attachments put URL in msgEvt.File and require client-side decryption.
if msgEvt != nil && msgEvt.File != nil && msgEvt.URL == "" { if msgEvt != nil && msgEvt.File != nil && msgEvt.URL == "" {
err = msgEvt.File.DecryptInPlace(data) if err = msgEvt.File.PrepareForDecryption(); err != nil {
if err != nil {
return "", fmt.Errorf("decrypt matrix media: %w", err) return "", fmt.Errorf("decrypt matrix media: %w", err)
} }
decryptReader := msgEvt.File.DecryptStream(resp.Body)
reader = decryptReader
readerClose = decryptReader.Close
} }
label := matrixMediaLabel(msgEvt, mediaKind) label := matrixMediaLabel(msgEvt, mediaKind)
@ -749,14 +756,28 @@ func (c *MatrixChannel) downloadMedia(
if err != nil { if err != nil {
return "", err return "", err
} }
defer tmp.Close() tmpPath := tmp.Name()
cleanup := true
defer func() {
_ = tmp.Close()
if cleanup {
_ = os.Remove(tmpPath)
}
}()
if _, err = tmp.Write(data); err != nil { _, err = io.Copy(tmp, reader)
_ = os.Remove(tmp.Name()) if err != nil {
return "", err
}
if err = readerClose(); err != nil {
return "", fmt.Errorf("decrypt matrix media: %w", err)
}
if err = tmp.Close(); err != nil {
return "", err return "", err
} }
return tmp.Name(), nil cleanup = false
return tmpPath, nil
} }
func matrixContentType(msgEvt *event.MessageEventContent) string { func matrixContentType(msgEvt *event.MessageEventContent) string {

View file

@ -2,6 +2,8 @@ package matrix
import ( import (
"context" "context"
"net/http"
"net/http/httptest"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -197,6 +199,50 @@ func TestMatrixMediaExt(t *testing.T) {
} }
} }
func TestDownloadMedia_WritesResponseToTempFile(t *testing.T) {
const wantBody = "matrix-media-payload"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasSuffix(r.URL.Path, "/_matrix/client/v1/media/download/matrix.test/abc123") {
t.Fatalf("unexpected download path: %s", r.URL.Path)
}
w.Header().Set("Content-Type", "image/png")
_, _ = w.Write([]byte(wantBody))
}))
defer server.Close()
client, err := mautrix.NewClient(server.URL, id.UserID("@picoclaw:matrix.test"), "")
if err != nil {
t.Fatalf("NewClient: %v", err)
}
ch := &MatrixChannel{client: client}
msg := &event.MessageEventContent{
MsgType: event.MsgImage,
Body: "image.png",
URL: id.ContentURIString("mxc://matrix.test/abc123"),
Info: &event.FileInfo{MimeType: "image/png"},
}
path, err := ch.downloadMedia(context.Background(), msg, "image")
if err != nil {
t.Fatalf("downloadMedia: %v", err)
}
defer os.Remove(path)
if ext := filepath.Ext(path); ext != ".png" {
t.Fatalf("temp file extension=%q want=.png", ext)
}
got, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if string(got) != wantBody {
t.Fatalf("file contents=%q want=%q", string(got), wantBody)
}
}
func TestExtractInboundContent_ImageNoURLFallback(t *testing.T) { func TestExtractInboundContent_ImageNoURLFallback(t *testing.T) {
ch := &MatrixChannel{} ch := &MatrixChannel{}
msg := &event.MessageEventContent{ msg := &event.MessageEventContent{