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
15 changes: 11 additions & 4 deletions repository/gorm/stamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/traPtitech/traQ/event"
"github.com/traPtitech/traQ/model"
"github.com/traPtitech/traQ/repository"
"github.com/traPtitech/traQ/utils/etag"
"github.com/traPtitech/traQ/utils/gormutil"
"github.com/traPtitech/traQ/utils/validator"
)
Expand All @@ -25,7 +26,7 @@ type stampRepository struct {
db *gorm.DB
hub *hub.Hub
stamps *sc.Cache[struct{}, map[uuid.UUID]*model.Stamp]
perType *sc.Cache[repository.StampType, []*model.StampWithThumbnail]
perType *sc.Cache[repository.StampType, *etag.Entity[[]*model.StampWithThumbnail]]
}

func makeStampRepository(db *gorm.DB, hub *hub.Hub) *stampRepository {
Expand All @@ -48,7 +49,7 @@ func (r *stampRepository) loadStamps(_ context.Context, _ struct{}) (map[uuid.UU
return stampsMap, nil
}

func (r *stampRepository) loadFilteredStamps(ctx context.Context, stampType repository.StampType) ([]*model.StampWithThumbnail, error) {
func (r *stampRepository) loadFilteredStamps(ctx context.Context, stampType repository.StampType) (*etag.Entity[[]*model.StampWithThumbnail], error) {
stamps, err := r.stamps.Get(ctx, struct{}{})
if err != nil {
return nil, err
Expand Down Expand Up @@ -106,7 +107,13 @@ func (r *stampRepository) loadFilteredStamps(ctx context.Context, stampType repo
}

sort.Slice(arr, func(i, j int) bool { return arr[i].ID.String() < arr[j].ID.String() })
return arr, nil

stampsWithEtag, err := etag.New(arr)
if err != nil {
return nil, err
}

return stampsWithEtag, nil
}

func (r *stampRepository) purgeCache() {
Expand Down Expand Up @@ -304,7 +311,7 @@ func (r *stampRepository) DeleteStamp(ctx context.Context, id uuid.UUID) (err er
}

// GetAllStampsWithThumbnail implements StampRepository interface.
func (r *stampRepository) GetAllStampsWithThumbnail(ctx context.Context, stampType repository.StampType) (stampsWithThumbnail []*model.StampWithThumbnail, err error) {
func (r *stampRepository) GetAllStampsWithThumbnail(ctx context.Context, stampType repository.StampType) (*etag.Entity[[]*model.StampWithThumbnail], error) {
return r.perType.Get(ctx, stampType)
}

Expand Down
12 changes: 6 additions & 6 deletions repository/gorm/stamp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,13 +248,13 @@ func TestRepositoryImpl_GetAllStampsWithThumbnail(t *testing.T) {

t.Run("without thumbnail", func(t *testing.T) {
t.Parallel()
arr, err := repo.GetAllStampsWithThumbnail(context.TODO(), repository.StampTypeAll)
stamps, err := repo.GetAllStampsWithThumbnail(context.TODO(), repository.StampTypeAll)
if !assert.NoError(err) {
t.FailNow()
}
assert.Len(arr, n*2)
assert.Len(stamps.Value(), n*2)
cnt := 0
for _, s := range arr {
for _, s := range stamps.Value() {
if !s.HasThumbnail {
cnt++
}
Expand All @@ -263,13 +263,13 @@ func TestRepositoryImpl_GetAllStampsWithThumbnail(t *testing.T) {
})
t.Run("with thumbnail", func(t *testing.T) {
t.Parallel()
arr, err := repo.GetAllStampsWithThumbnail(context.TODO(), repository.StampTypeAll)
stamps, err := repo.GetAllStampsWithThumbnail(context.TODO(), repository.StampTypeAll)
if !assert.NoError(err) {
t.FailNow()
}
assert.Len(arr, n*2)
assert.Len(stamps.Value(), n*2)
cnt := 0
for _, s := range arr {
for _, s := range stamps.Value() {
if s.HasThumbnail {
cnt++
}
Expand Down
3 changes: 2 additions & 1 deletion repository/stamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/gofrs/uuid"

"github.com/traPtitech/traQ/model"
"github.com/traPtitech/traQ/utils/etag"
"github.com/traPtitech/traQ/utils/optional"
)

Expand Down Expand Up @@ -96,7 +97,7 @@ type StampRepository interface {
//
// 成功した場合、スタンプのIDでソートされた配列とnilを返します。
// DBによるエラーを返すことがあります。
GetAllStampsWithThumbnail(ctx context.Context, stampType StampType) (stamps []*model.StampWithThumbnail, err error)
GetAllStampsWithThumbnail(ctx context.Context, stampType StampType) (stampsWithETag *etag.Entity[[]*model.StampWithThumbnail], err error)
// StampExists 指定したIDのスタンプが存在するかどうかを返します
//
// 存在する場合、trueとnilを返します。
Expand Down
21 changes: 21 additions & 0 deletions router/extension/precond.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/labstack/echo/v4"

"github.com/traPtitech/traQ/router/consts"
"github.com/traPtitech/traQ/utils/etag"
)

func scanETag(s string) (eTag string, remain string) {
Expand Down Expand Up @@ -223,3 +224,23 @@ func ServeWithETag(c echo.Context, contentType string, bytes []byte) error {
}
return c.Blob(http.StatusOK, contentType, bytes)
}

// ServeJSONWithPrecomputedETag 事前に計算されたEtagを付与してJSONを返します。リクエストの条件に合うときは304を返します。
func ServeJSONWithPrecomputedETag[T any](c echo.Context, e *etag.Entity[T]) error {
// NOTE: prettyクエリがあるときはEtagを無視して整形されたJSONを返す
if _, pretty := c.QueryParams()["pretty"]; pretty {
return c.JSONPretty(http.StatusOK, e.Value(), " ")
}

c.Response().Header().Set(consts.HeaderETag, "\""+e.ETag()+"\"")

done, err := CheckPreconditions(c, time.Time{})
if err != nil {
return err
}
if done {
return nil
}

return c.JSON(http.StatusOK, e.Value())
}
6 changes: 3 additions & 3 deletions router/v1/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@
return nil, err
}

stampNames := make([]string, len(stamps))
for i, stamp := range stamps {
stampNames := make([]string, len(stamps.Value()))
for i, stamp := range stamps.Value() {
stampNames[i] = stamp.Name
}

Expand All @@ -151,8 +151,8 @@
buf.WriteString(".s16{width:16px;height:16px}")
buf.WriteString(".s24{width:24px;height:24px}")
buf.WriteString(".s32{width:32px;height:32px}")
for _, stamp := range stamps {
for _, stamp := range stamps.Value() {
buf.WriteString(fmt.Sprintf(".emoji.e_%s{background-image:url(/api/1.0/public/emoji/%s)}", stamp.Name, stamp.ID))

Check failure on line 155 in router/v1/public.go

View workflow job for this annotation

GitHub Actions / Lint

QF1012: Use fmt.Fprintf(...) instead of WriteString(fmt.Sprintf(...)) (staticcheck)
}
return buf.Bytes(), nil
}
Expand Down
2 changes: 1 addition & 1 deletion router/v3/stamps.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func (h *Handlers) GetStamps(c echo.Context) error {
return herror.InternalServerError(err)
}

return extension.ServeJSONWithETag(c, stamps)
return extension.ServeJSONWithPrecomputedETag(c, stamps)
}

// CreateStamp POST /stamps
Expand Down
43 changes: 43 additions & 0 deletions utils/etag/etag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package etag

import (
"crypto/md5"
"encoding/hex"

jsonIter "github.com/json-iterator/go"
)

type Entity[T any] struct {
value T
etag string
}

var jsonIterForETag = jsonIter.Config{
EscapeHTML: false,
MarshalFloatWith6Digits: true,
ObjectFieldMustBeSimpleString: true,
// ここより上はjsonIter.ConfigFastestと同様
SortMapKeys: true, // 順番が一致しないとEtagが一致しないのでソートを有効にする
}.Froze()

func New[T any](data T) (*Entity[T], error) {
b, err := jsonIterForETag.Marshal(data)
if err != nil {
return nil, err
}

md5Res := md5.Sum(b)

return &Entity[T]{
value: data,
etag: hex.EncodeToString(md5Res[:]),
}, nil
}

func (e Entity[T]) Value() T {
return e.value
}

func (e Entity[T]) ETag() string {
return e.etag
}
Loading