cluster

Infrastructure files for Nordgedanken and Midnightthoughts.
git clone git://archive.git.mtrnord.blog/MTRNord/cluster.git
Log | Files | Refs | README

media.go (2624B)


      1 // media.go — Matrix media download and S3 storage.
      2 package main
      3 
      4 import (
      5 	"context"
      6 	"fmt"
      7 	"io"
      8 	"log/slog"
      9 	"net/http"
     10 	"net/url"
     11 	"strings"
     12 	"time"
     13 
     14 	"maunium.net/go/mautrix"
     15 )
     16 
     17 // mediaHTTPClient is used for all media downloads; the 5-minute timeout
     18 // prevents a single large file from stalling the entire backup run.
     19 var mediaHTTPClient = &http.Client{Timeout: 5 * time.Minute}
     20 
     21 var mediaMsgTypes = map[string]bool{
     22 	"m.image": true,
     23 	"m.file":  true,
     24 	"m.video": true,
     25 	"m.audio": true,
     26 }
     27 
     28 // downloadAndStoreMedia downloads a single mxc:// URL and stores it under
     29 // prefix/label/server/mediaID in S3, skipping if already present.
     30 func downloadAndStoreMedia(ctx context.Context, client *mautrix.Client, mxcURL, prefix, label string) error {
     31 	if !strings.HasPrefix(mxcURL, "mxc://") {
     32 		return nil
     33 	}
     34 	rest := mxcURL[len("mxc://"):]
     35 	slashIdx := strings.Index(rest, "/")
     36 	if slashIdx < 0 {
     37 		return nil
     38 	}
     39 	server := rest[:slashIdx]
     40 	mediaID := rest[slashIdx+1:]
     41 	if server == "" || mediaID == "" {
     42 		return nil
     43 	}
     44 
     45 	s3Key := prefix + "/" + label + "/" + server + "/" + mediaID
     46 	if s3Exists(ctx, s3Key) {
     47 		return nil
     48 	}
     49 
     50 	base := strings.TrimRight(client.HomeserverURL.String(), "/")
     51 	downloadURL := base + "/_matrix/media/v3/download/" +
     52 		url.PathEscape(server) + "/" + url.PathEscape(mediaID)
     53 
     54 	req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, nil)
     55 	if err != nil {
     56 		return err
     57 	}
     58 	req.Header.Set("Authorization", "Bearer "+client.AccessToken)
     59 	resp, err := mediaHTTPClient.Do(req)
     60 	if err != nil {
     61 		return err
     62 	}
     63 	defer resp.Body.Close()
     64 
     65 	if resp.StatusCode != http.StatusOK {
     66 		return fmt.Errorf("media download HTTP %d for %s", resp.StatusCode, mxcURL)
     67 	}
     68 
     69 	data, err := io.ReadAll(resp.Body)
     70 	if err != nil {
     71 		return err
     72 	}
     73 
     74 	ct := resp.Header.Get("Content-Type")
     75 	if ct == "" {
     76 		ct = "application/octet-stream"
     77 	}
     78 
     79 	finalKey := s3Key
     80 	if ext := extFromContentType(ct); ext != "" {
     81 		finalKey = s3Key + ext
     82 	}
     83 	if err := s3Put(ctx, finalKey, data, ct); err != nil {
     84 		return err
     85 	}
     86 	slog.Info("Stored media", "key", finalKey, "bytes", len(data))
     87 	return nil
     88 }
     89 
     90 func extFromContentType(ct string) string {
     91 	if i := strings.Index(ct, ";"); i >= 0 {
     92 		ct = ct[:i]
     93 	}
     94 	switch strings.TrimSpace(ct) {
     95 	case "image/jpeg":
     96 		return ".jpg"
     97 	case "image/png":
     98 		return ".png"
     99 	case "image/gif":
    100 		return ".gif"
    101 	case "image/webp":
    102 		return ".webp"
    103 	case "video/mp4":
    104 		return ".mp4"
    105 	case "video/webm":
    106 		return ".webm"
    107 	case "audio/mpeg":
    108 		return ".mp3"
    109 	case "audio/ogg":
    110 		return ".ogg"
    111 	case "audio/opus":
    112 		return ".opus"
    113 	case "application/pdf":
    114 		return ".pdf"
    115 	}
    116 	return ""
    117 }