Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -876,10 +876,25 @@ func (c *Config) GetAvatarPath(uid string) string {
return fmt.Sprintf("users/%s/avatar", uid)
}

// GetGroupAvatarFilePath 获取群头像上传路径
// GetGroupAvatarFilePath returns the object path for a group's avatar image.
//
// The path is routed under the "avatar" bucket so it lands in the publicly
// readable bucket used by all avatars (user, group, community). Before this
// fix (GH#21 / octo-server#103) the path started with "group/", which
// routed the file into the private "group" bucket (intended for group
// exports, not publicly served images). The MinIO / OSS splitBucketAndObject
// helper in octo-server selects the destination bucket from the first path
// segment when it appears in the allow-list, so "avatar/..." → avatar
// bucket (anonymous-readable) and "group/..." → group bucket (private).
//
// Deployments upgrading from a version that stored group avatars under the
// old "group/" path should either:
// - re-generate group avatars (trigger a group-member change), or
// - copy the existing objects from the "group" bucket to "avatar" bucket
// under the new key layout.
func (c *Config) GetGroupAvatarFilePath(groupNo string) string {
avatarID := crc32.ChecksumIEEE([]byte(groupNo)) % uint32(c.Avatar.Partition)
return fmt.Sprintf("group/%d/%s.png", avatarID, groupNo)
return fmt.Sprintf("avatar/group/%d/%s.png", avatarID, groupNo)
}

// GetCommunityAvatarFilePath 获取社区头像上传路径
Expand Down
70 changes: 70 additions & 0 deletions config/config_avatar_path_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package config

import (
"strings"
"testing"

"github.com/spf13/viper"
)

// TestGetGroupAvatarFilePath_BucketPrefix verifies that the group avatar path
// lands in the "avatar" bucket (publicly readable), not the "group" bucket
// (private). This is the regression guard for GH#21 / octo-server#103.
func TestGetGroupAvatarFilePath_BucketPrefix(t *testing.T) {
cfg := New()
vp := viper.New()
cfg.ConfigureWithViper(vp)

path := cfg.GetGroupAvatarFilePath("g_test_12345")

// Must start with "avatar/" so splitBucketAndObject routes to the avatar bucket.
if !strings.HasPrefix(path, "avatar/") {
t.Errorf("GetGroupAvatarFilePath should start with 'avatar/' to route to the public avatar bucket, got %q", path)
}

// Must NOT start with "group/" — that routes to the private group bucket.
if strings.HasPrefix(path, "group/") {
t.Errorf("GetGroupAvatarFilePath must not start with 'group/' (private bucket), got %q", path)
}

// Must end with ".png" and contain the group number.
if !strings.HasSuffix(path, ".png") {
t.Errorf("GetGroupAvatarFilePath should end with '.png', got %q", path)
}
if !strings.Contains(path, "g_test_12345") {
t.Errorf("GetGroupAvatarFilePath should contain the group number, got %q", path)
}

// Must contain a "group/" sub-path to namespace group avatars within the avatar bucket.
if !strings.Contains(path, "/group/") {
t.Errorf("GetGroupAvatarFilePath should contain '/group/' namespace, got %q", path)
}
}

// TestGetGroupAvatarFilePath_Deterministic verifies the same groupNo always
// produces the same path (partition is deterministic via CRC32).
func TestGetGroupAvatarFilePath_Deterministic(t *testing.T) {
cfg := New()
vp := viper.New()
cfg.ConfigureWithViper(vp)

p1 := cfg.GetGroupAvatarFilePath("g_abc")
p2 := cfg.GetGroupAvatarFilePath("g_abc")
if p1 != p2 {
t.Errorf("GetGroupAvatarFilePath should be deterministic: %q != %q", p1, p2)
}
}

// TestGetGroupAvatarFilePath_DifferentGroups verifies distinct groups get
// distinct paths (collision-free at the groupNo level).
func TestGetGroupAvatarFilePath_DifferentGroups(t *testing.T) {
cfg := New()
vp := viper.New()
cfg.ConfigureWithViper(vp)

p1 := cfg.GetGroupAvatarFilePath("g_alice_group")
p2 := cfg.GetGroupAvatarFilePath("g_bob_group")
if p1 == p2 {
t.Errorf("different groups should produce different paths: both %q", p1)
}
}
Loading