feat: load oss media config from sys config

This commit is contained in:
richjohnrambo
2026-09-08 22:19:00 +08:00
parent 0f984b0ab4
commit aae13c9265
10 changed files with 224 additions and 9 deletions
+53 -5
View File
@@ -13,6 +13,7 @@ package main
import (
"context"
"encoding/json"
"errors"
"flag"
"net/http"
"os"
@@ -254,6 +255,52 @@ type mediaConfig struct {
Handlers map[string]json.RawMessage `json:"handlers"`
}
func resolveMediaHandlerConfig(config *mediaConfig) (string, string, error) {
if config == nil {
return "", "", nil
}
handler := strings.TrimSpace(config.UseHandler)
if handler == "sys_config" {
values, err := store.ThirdPartyConfig.Get("im_third_party", "media_handler")
if err != nil {
return "", "", err
}
handler = strings.TrimSpace(values["media_handler"])
}
if handler == "" {
return "", "", errors.New("empty media handler")
}
var conf string
if config.Handlers != nil {
if params := config.Handlers[handler]; params != nil {
conf = string(params)
}
}
if handler == "oss" {
ossConf, err := ensureOSSUsesSysConfig(conf)
if err != nil {
return "", "", err
}
return handler, ossConf, nil
}
return handler, conf, nil
}
func ensureOSSUsesSysConfig(conf string) (string, error) {
params := map[string]any{}
if strings.TrimSpace(conf) != "" {
if err := json.Unmarshal([]byte(conf), &params); err != nil {
return "", err
}
}
params["use_sys_config"] = true
encoded, err := json.Marshal(params)
if err != nil {
return "", err
}
return string(encoded), nil
}
// Contentx of the configuration file
type configType struct {
// HTTP(S) address:port to listen on for websocket and long polling clients. Either a
@@ -640,12 +687,13 @@ func main() {
} else {
globals.maxFileUploadSize = config.Media.MaxFileUploadSize
if config.Media.Handlers != nil {
var conf string
if params := config.Media.Handlers[config.Media.UseHandler]; params != nil {
conf = string(params)
handler, conf, err := resolveMediaHandlerConfig(config.Media)
if err != nil {
logs.Err.Fatalf("Failed to resolve media handler config: %s", err)
}
if err = store.Store.UseMediaHandler(config.Media.UseHandler, conf); err != nil {
logs.Err.Fatalf("Failed to init media handler '%s': %s", config.Media.UseHandler, err)
config.Media.UseHandler = handler
if err = store.Store.UseMediaHandler(handler, conf); err != nil {
logs.Err.Fatalf("Failed to init media handler '%s': %s", handler, err)
}
}
if config.Media.GcPeriod > 0 && config.Media.GcBlockSize > 0 {
+36
View File
@@ -131,6 +131,10 @@ func (oh *ossHandler) loadSysConfig() error {
"oss_access_key_id",
"oss_access_key_secret",
"oss_presign_ttl",
"oss_serve_url",
"oss_cache_control",
"oss_cors_origins",
"oss_verify_bucket",
)
if err != nil {
return errors.New("failed to load OSS config from sys config: " + err.Error())
@@ -146,6 +150,14 @@ func (oh *ossHandler) loadSysConfig() error {
applyString(&oh.conf.RegionID, values["oss_region_id"])
applyString(&oh.conf.AccessKeyID, values["oss_access_key_id"])
applyString(&oh.conf.AccessKeySecret, values["oss_access_key_secret"])
applyString(&oh.conf.ServeURL, values["oss_serve_url"])
applyString(&oh.conf.CacheControl, values["oss_cache_control"])
if origins := parseStringList(values["oss_cors_origins"]); len(origins) > 0 {
oh.conf.CorsOrigins = origins
}
if verify := strings.TrimSpace(values["oss_verify_bucket"]); verify != "" {
oh.conf.VerifyBucket = truthy(verify)
}
if ttl := strings.TrimSpace(values["oss_presign_ttl"]); ttl != "" {
parsed, err := strconv.Atoi(ttl)
if err != nil {
@@ -156,6 +168,30 @@ func (oh *ossHandler) loadSysConfig() error {
return nil
}
func parseStringList(value string) []string {
value = strings.TrimSpace(value)
if value == "" {
return nil
}
var items []string
if strings.HasPrefix(value, "[") {
if err := json.Unmarshal([]byte(value), &items); err == nil {
return compactStrings(items)
}
}
return compactStrings(strings.Split(value, ","))
}
func compactStrings(values []string) []string {
items := make([]string, 0, len(values))
for _, value := range values {
if value = strings.TrimSpace(value); value != "" {
items = append(items, value)
}
}
return items
}
func applyString(target *string, value string) {
if value = strings.TrimSpace(value); value != "" {
*target = value
+87
View File
@@ -0,0 +1,87 @@
package oss
import (
"bytes"
"encoding/json"
"io"
"net/http"
"os"
"testing"
"time"
aliyunoss "github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/tinode/chat/server/media"
)
func TestLiveAliyunOSSUploadPublic(t *testing.T) {
endpoint := os.Getenv("LIVE_OSS_ENDPOINT")
bucket := os.Getenv("LIVE_OSS_BUCKET")
accessKeyID := os.Getenv("LIVE_OSS_ACCESS_KEY_ID")
accessKeySecret := os.Getenv("LIVE_OSS_ACCESS_KEY_SECRET")
if endpoint == "" || bucket == "" || accessKeyID == "" || accessKeySecret == "" {
t.Skip("LIVE_OSS_* env is not configured")
}
conf, err := json.Marshal(ossConfig{
Endpoint: endpoint,
BucketName: bucket,
AccessKeyID: accessKeyID,
AccessKeySecret: accessKeySecret,
PresignTTL: 60,
CacheControl: "no-cache",
CorsOrigins: []string{"*"},
})
if err != nil {
t.Fatal(err)
}
handler := &ossHandler{}
if err = handler.Init(string(conf)); err != nil {
t.Fatalf("oss init failed: %v", err)
}
body := []byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}
file := &media.PublicFileDef{
ID: "codex-live-" + time.Now().UTC().Format("20060102150405"),
Purpose: "codex_live",
MimeType: "image/png",
CreatedAt: time.Now().UTC(),
}
location, size, err := handler.UploadPublic(file, bytes.NewReader(body))
if err != nil {
t.Fatalf("oss upload failed: %v", err)
}
defer func() {
if err := handler.Delete([]string{file.Location}); err != nil {
t.Logf("oss cleanup failed: %v", err)
}
}()
if size != int64(len(body)) || location == "" || file.Location == "" || file.ETag == "" {
t.Fatalf("unexpected upload result: location=%q file=%+v size=%d", location, file, size)
}
got, err := handler.bucket.GetObject(file.Location)
if err != nil {
t.Fatalf("oss get object failed: %v", err)
}
defer got.Close()
downloaded, err := io.ReadAll(got)
if err != nil {
t.Fatalf("oss read object failed: %v", err)
}
if !bytes.Equal(downloaded, body) {
t.Fatalf("oss downloaded bytes mismatch: got %x want %x", downloaded, body)
}
signedURL, err := handler.bucket.SignURL(file.Location, aliyunoss.HTTPGet, 60)
if err != nil {
t.Fatalf("oss sign url failed: %v", err)
}
resp, err := http.Get(signedURL)
if err != nil {
t.Fatalf("oss signed url get failed: %v", err)
}
resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest {
t.Fatalf("oss signed url get status=%d", resp.StatusCode)
}
}
+10
View File
@@ -57,6 +57,10 @@ func TestLoadSysConfig(t *testing.T) {
"oss_access_key_id": "ak",
"oss_access_key_secret": "sk",
"oss_presign_ttl": "300",
"oss_serve_url": "/files/",
"oss_cache_control": "private",
"oss_cors_origins": `["https://example.com"]`,
"oss_verify_bucket": "true",
}
defer func() { store.ThirdPartyConfig = original }()
@@ -70,4 +74,10 @@ func TestLoadSysConfig(t *testing.T) {
if handler.conf.RegionID != "cn-shenzhen" || handler.conf.PresignTTL != 300 {
t.Fatalf("unexpected OSS region or ttl: %+v", handler.conf)
}
if handler.conf.ServeURL != "/files/" || handler.conf.CacheControl != "private" || !handler.conf.VerifyBucket {
t.Fatalf("unexpected OSS serve settings: %+v", handler.conf)
}
if len(handler.conf.CorsOrigins) != 1 || handler.conf.CorsOrigins[0] != "https://example.com" {
t.Fatalf("unexpected OSS cors origins: %+v", handler.conf.CorsOrigins)
}
}
+4 -1
View File
@@ -23,7 +23,7 @@
"alias_tag": "alias",
"media": {
"use_handler": "fs",
"use_handler": "sys_config",
"max_size": 104857600,
"gc_period": 3600,
"gc_block_size": 100,
@@ -32,6 +32,9 @@
"upload_dir": "uploads",
"cache_control": "max-age=86400",
"cors_origins": ["*"]
},
"oss": {
"use_sys_config": true
}
}
},
+4 -1
View File
@@ -23,7 +23,7 @@
"alias_tag": "alias",
"media": {
"use_handler": "fs",
"use_handler": "sys_config",
"max_size": 104857600,
"gc_period": 3600,
"gc_block_size": 100,
@@ -32,6 +32,9 @@
"upload_dir": "uploads",
"cache_control": "max-age=86400",
"cors_origins": ["*"]
},
"oss": {
"use_sys_config": true
}
}
},