Skip to content
This repository was archived by the owner on Feb 4, 2021. It is now read-only.

Commit 54e65c2

Browse files
authored
Merge pull request #75 from gedorinku/achievement_image_url
実績の画像の更新
2 parents 5a0f830 + 7158a6c commit 54e65c2

File tree

9 files changed

+181
-55
lines changed

9 files changed

+181
-55
lines changed

app/di/store_component.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
feedstore "github.com/ProgrammingLab/prolab-accounts/infra/store/feed"
1919
githubstore "github.com/ProgrammingLab/prolab-accounts/infra/store/github"
2020
heartbeatstore "github.com/ProgrammingLab/prolab-accounts/infra/store/heartbeat"
21+
imagestore "github.com/ProgrammingLab/prolab-accounts/infra/store/image"
2122
invitationstore "github.com/ProgrammingLab/prolab-accounts/infra/store/invitation"
2223
resetstore "github.com/ProgrammingLab/prolab-accounts/infra/store/password_reset"
2324
profilestore "github.com/ProgrammingLab/prolab-accounts/infra/store/profile"
@@ -44,6 +45,7 @@ type StoreComponent interface {
4445
EmailConfirmationStore(ctx context.Context) store.EmailConfirmationStore
4546
PasswordResetStore(ctx context.Context) store.PasswordResetStore
4647
AchievementStore(ctx context.Context) store.AchievementStore
48+
ImageStore(ctx context.Context) store.ImageStore
4749
}
4850

4951
// NewStoreComponent returns new store component
@@ -146,7 +148,7 @@ type storeComponentImpl struct {
146148
}
147149

148150
func (s *storeComponentImpl) UserStore(ctx context.Context) store.UserStore {
149-
return userstore.NewUserStore(ctx, s.db, s.minioCli, s.cfg.MinioBucketName)
151+
return userstore.NewUserStore(ctx, s.db)
150152
}
151153

152154
func (s *storeComponentImpl) SessionStore(ctx context.Context) store.SessionStore {
@@ -200,3 +202,7 @@ func (s *storeComponentImpl) PasswordResetStore(ctx context.Context) store.Passw
200202
func (s *storeComponentImpl) AchievementStore(ctx context.Context) store.AchievementStore {
201203
return achievementstore.NewAchievementStore(ctx, s.db)
202204
}
205+
206+
func (s *storeComponentImpl) ImageStore(ctx context.Context) store.ImageStore {
207+
return imagestore.NewImageStore(ctx, s.minioCli, s.cfg.MinioBucketName)
208+
}

app/server/achievements_server.go

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/izumin5210/grapi/pkg/grapiserver"
1111
"github.com/pkg/errors"
1212
"google.golang.org/grpc/codes"
13+
"google.golang.org/grpc/grpclog"
1314
"google.golang.org/grpc/status"
1415

1516
api_pb "github.com/ProgrammingLab/prolab-accounts/api"
@@ -43,12 +44,16 @@ type achievementServiceServerImpl struct {
4344
}
4445

4546
const (
47+
// MaxImageSize represents max of image size
48+
MaxImageSize = 1024 * 1024 * 3
4649
pageTokenTimeLayout = time.RFC3339
4750
)
4851

4952
var (
5053
// ErrInvalidPageToken is returned when page token is invalid
5154
ErrInvalidPageToken = status.Error(codes.InvalidArgument, "invalid page token")
55+
// ErrImageSizeTooLarge will be returned when the image is too large
56+
ErrImageSizeTooLarge = status.Error(codes.InvalidArgument, "image must be smaller than 3MiB")
5257
)
5358

5459
func (s *achievementServiceServerImpl) ListAchievements(ctx context.Context, req *api_pb.ListAchievementsRequest) (*api_pb.ListAchievementsResponse, error) {
@@ -148,9 +153,45 @@ func (s *achievementServiceServerImpl) UpdateAchievement(ctx context.Context, re
148153
return achievementToResponse(rec, true, s.cfg), nil
149154
}
150155

151-
func (s *achievementServiceServerImpl) UpdateAchievementImage(context.Context, *api_pb.UpdateAchievementImageRequest) (*api_pb.Achievement, error) {
152-
// TODO: Not yet implemented.
153-
return nil, status.Error(codes.Unimplemented, "TODO: You should implement it!")
156+
func (s *achievementServiceServerImpl) UpdateAchievementImage(ctx context.Context, req *api_pb.UpdateAchievementImageRequest) (*api_pb.Achievement, error) {
157+
_, ok := interceptor.GetCurrentUserID(ctx)
158+
if !ok {
159+
return nil, util.ErrUnauthenticated
160+
}
161+
162+
image := req.GetImage()
163+
if MaxImageSize < len(image) {
164+
return nil, ErrImageSizeTooLarge
165+
}
166+
167+
is := s.ImageStore(ctx)
168+
name, err := is.CreateImage(image)
169+
if err != nil {
170+
return nil, err
171+
}
172+
173+
as := s.AchievementStore(ctx)
174+
ach, old, err := as.UpdateAchievementImage(int64(req.GetAchievementId()), name)
175+
if err != nil {
176+
if errors.Cause(err) == sql.ErrNoRows {
177+
return nil, util.ErrNotFound
178+
}
179+
return nil, err
180+
}
181+
182+
go func() {
183+
if old == "" {
184+
return
185+
}
186+
187+
is := s.ImageStore(context.Background())
188+
err := is.DeleteImage(old)
189+
if err != nil {
190+
grpclog.Errorf("failed to delete old user icon: %+v", err)
191+
}
192+
}()
193+
194+
return achievementToResponse(ach, true, s.cfg), nil
154195
}
155196

156197
func (s *achievementServiceServerImpl) DeleteAchievement(ctx context.Context, req *api_pb.DeleteAchievementRequest) (*empty.Empty, error) {
@@ -190,7 +231,7 @@ func achievementToResponse(ach *record.Achievement, includePrivate bool, cfg *co
190231
Award: ach.Award,
191232
Url: ach.URL,
192233
Description: ach.Description,
193-
ImageUrl: ach.ImageFilename.String, // TODO
234+
ImageUrl: cfg.MinioPublicURL + "/" + cfg.MinioBucketName + "/" + ach.ImageFilename.String,
194235
HappenedAt: timeToResponse(ach.HappenedAt),
195236
}
196237

app/server/users_server.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/volatiletech/null"
1313
"golang.org/x/crypto/bcrypt"
1414
"google.golang.org/grpc/codes"
15+
"google.golang.org/grpc/grpclog"
1516
"google.golang.org/grpc/status"
1617

1718
api_pb "github.com/ProgrammingLab/prolab-accounts/api"
@@ -288,15 +289,33 @@ func (s *userServiceServerImpl) UpdateUserIcon(ctx context.Context, req *api_pb.
288289
return nil, ErrIconSizeTooLarge
289290
}
290291

292+
is := s.ImageStore(ctx)
293+
name, err := is.CreateImage(icon)
294+
if err != nil {
295+
return nil, err
296+
}
297+
291298
us := s.UserStore(ctx)
292-
u, err := us.UpdateIcon(id, icon)
299+
u, old, err := us.UpdateIcon(id, name)
293300
if err != nil {
294301
if err := errors.Cause(err); err == image.ErrFormat {
295302
return nil, ErrInvalidImageFormat
296303
}
297304
return nil, err
298305
}
299306

307+
go func() {
308+
if old == "" {
309+
return
310+
}
311+
312+
is := s.ImageStore(context.Background())
313+
err := is.DeleteImage(old)
314+
if err != nil {
315+
grpclog.Errorf("failed to delete old user icon: %+v", err)
316+
}
317+
}()
318+
300319
return userToResponse(u, true, s.cfg), nil
301320
}
302321

infra/store/achievement/achievement_store.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"time"
77

88
"github.com/pkg/errors"
9+
"github.com/volatiletech/null"
910
"github.com/volatiletech/sqlboiler/boil"
1011
"github.com/volatiletech/sqlboiler/queries/qm"
1112

@@ -121,6 +122,23 @@ func (s *achievementStoreImpl) UpdateAchievement(ach *record.Achievement, member
121122
return ach, err
122123
}
123124

125+
func (s *achievementStoreImpl) UpdateAchievementImage(id int64, filename string) (ach *record.Achievement, old string, err error) {
126+
err = s.db.Watch(s.ctx, func(ctx context.Context, tx *sql.Tx) error {
127+
ach, err = record.Achievements(qm.Where("id = ?", id), qm.Load("AchievementUsers.User")).One(ctx, tx)
128+
if err != nil {
129+
return errors.WithStack(err)
130+
}
131+
132+
old = ach.ImageFilename.String
133+
134+
ach.ImageFilename = null.StringFrom(filename)
135+
_, err = ach.Update(ctx, tx, boil.Whitelist("image_filename", "updated_at"))
136+
return errors.WithStack(err)
137+
})
138+
139+
return ach, old, err
140+
}
141+
124142
func (s *achievementStoreImpl) DeleteAchievement(id int64) error {
125143
err := s.db.Watch(s.ctx, func(ctx context.Context, tx *sql.Tx) error {
126144
_, err := record.AchievementUsers(record.AchievementUserWhere.AchievementID.EQ(id)).DeleteAll(s.ctx, tx)

infra/store/achievement_store.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ type AchievementStore interface {
1313
GetAchievement(id int64) (*record.Achievement, error)
1414
ListAchievements(before time.Time, limit int) (aches []*record.Achievement, next time.Time, err error)
1515
UpdateAchievement(ach *record.Achievement, memberIDs []model.UserID) (*record.Achievement, error)
16+
UpdateAchievementImage(id int64, filename string) (ach *record.Achievement, old string, err error)
1617
DeleteAchievement(id int64) error
1718
}

infra/store/image/image_store.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package imagestore
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"crypto/rand"
7+
"encoding/base64"
8+
"image"
9+
_ "image/gif" // for image
10+
_ "image/jpeg"
11+
_ "image/png"
12+
13+
"github.com/minio/minio-go"
14+
"github.com/pkg/errors"
15+
16+
"github.com/ProgrammingLab/prolab-accounts/infra/store"
17+
)
18+
19+
type imageStoreImpl struct {
20+
ctx context.Context
21+
cli *minio.Client
22+
bucketName string
23+
}
24+
25+
// NewImageStore returns new image store
26+
func NewImageStore(ctx context.Context, cli *minio.Client, bucket string) store.ImageStore {
27+
return &imageStoreImpl{
28+
ctx: ctx,
29+
cli: cli,
30+
bucketName: bucket,
31+
}
32+
}
33+
34+
func (s *imageStoreImpl) CreateImage(img []byte) (filename string, err error) {
35+
r := bytes.NewReader(img)
36+
_, ext, err := image.DecodeConfig(r)
37+
if err != nil {
38+
return "", errors.WithStack(err)
39+
}
40+
name, err := generateFilename(ext)
41+
if err != nil {
42+
return "", errors.WithStack(err)
43+
}
44+
45+
opt := minio.PutObjectOptions{
46+
ContentType: "image/" + ext,
47+
}
48+
_, err = s.cli.PutObjectWithContext(s.ctx, s.bucketName, name, r, r.Size(), opt)
49+
if err != nil {
50+
return "", errors.WithStack(err)
51+
}
52+
53+
return name, nil
54+
}
55+
56+
func (s *imageStoreImpl) DeleteImage(filename string) error {
57+
err := s.cli.RemoveObject(s.bucketName, filename)
58+
return errors.WithStack(err)
59+
}
60+
61+
func generateFilename(ext string) (string, error) {
62+
b := make([]byte, 32)
63+
_, err := rand.Read(b)
64+
if err != nil {
65+
return "", errors.WithStack(err)
66+
}
67+
68+
res := base64.RawURLEncoding.EncodeToString(b)
69+
70+
return string(res) + "." + ext, nil
71+
}

infra/store/image_store.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package store
2+
3+
// ImageStore provides images
4+
type ImageStore interface {
5+
CreateImage(image []byte) (filename string, err error)
6+
DeleteImage(filename string) error
7+
}

infra/store/user/user_store.go

Lines changed: 11 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
package userstore
22

33
import (
4-
"bytes"
54
"context"
65
"crypto/rand"
76
"database/sql"
87
"encoding/base64"
9-
"image"
10-
_ "image/gif" // for image
11-
_ "image/jpeg"
12-
_ "image/png"
138

14-
minio "github.com/minio/minio-go"
159
"github.com/pkg/errors"
1610
"github.com/volatiletech/null"
1711
"github.com/volatiletech/sqlboiler/boil"
@@ -24,19 +18,15 @@ import (
2418
)
2519

2620
type userStoreImpl struct {
27-
ctx context.Context
28-
db *sqlutil.DB
29-
cli *minio.Client
30-
bucketName string
21+
ctx context.Context
22+
db *sqlutil.DB
3123
}
3224

3325
// NewUserStore returns new user store
34-
func NewUserStore(ctx context.Context, db *sqlutil.DB, cli *minio.Client, bucket string) store.UserStore {
26+
func NewUserStore(ctx context.Context, db *sqlutil.DB) store.UserStore {
3527
return &userStoreImpl{
36-
ctx: ctx,
37-
db: db,
38-
cli: cli,
39-
bucketName: bucket,
28+
ctx: ctx,
29+
db: db,
4030
}
4131
}
4232

@@ -167,52 +157,25 @@ func (s *userStoreImpl) UpdateFullName(userID model.UserID, fullName string) (*r
167157
return u, nil
168158
}
169159

170-
func (s *userStoreImpl) UpdateIcon(userID model.UserID, icon []byte) (*record.User, error) {
171-
r := bytes.NewReader(icon)
172-
_, ext, err := image.DecodeConfig(r)
173-
if err != nil {
174-
return nil, errors.WithStack(err)
175-
}
176-
name, err := generateFilename(ext)
177-
if err != nil {
178-
return nil, errors.WithStack(err)
179-
}
180-
160+
func (s *userStoreImpl) UpdateIcon(userID model.UserID, filename string) (*record.User, string, error) {
181161
mods := []qm.QueryMod{
182162
qm.Load("Profile.Role"),
183163
qm.Load("Profile.Department"),
184164
qm.Where("id = ?", userID),
185165
}
186166
u, err := record.Users(mods...).One(s.ctx, s.db)
187167
if err != nil {
188-
return nil, errors.WithStack(err)
168+
return nil, "", errors.WithStack(err)
189169
}
190170

191-
opt := minio.PutObjectOptions{
192-
ContentType: "image/" + ext,
193-
}
194-
_, err = s.cli.PutObjectWithContext(s.ctx, s.bucketName, name, r, r.Size(), opt)
195-
if err != nil {
196-
return nil, errors.WithStack(err)
197-
}
198-
199-
old := u.AvatarFilename
200-
u.AvatarFilename = null.StringFrom(name)
171+
old := u.AvatarFilename.String
172+
u.AvatarFilename = null.StringFrom(filename)
201173
_, err = u.Update(s.ctx, s.db, boil.Whitelist(record.UserColumns.AvatarFilename, record.UserColumns.UpdatedAt))
202174
if err != nil {
203-
return nil, errors.WithStack(err)
204-
}
205-
206-
if !old.Valid {
207-
return u, nil
208-
}
209-
210-
err = s.cli.RemoveObject(s.bucketName, old.String)
211-
if err != nil {
212-
return nil, errors.WithStack(err)
175+
return nil, "", errors.WithStack(err)
213176
}
214177

215-
return u, nil
178+
return u, old, nil
216179
}
217180

218181
func generateFilename(ext string) (string, error) {

infra/store/user_store.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ type UserStore interface {
1515
ListPublicUsers(minUserID model.UserID, limit int) ([]*record.User, model.UserID, error)
1616
ListPrivateUsers(minUserID model.UserID, limit int) ([]*record.User, model.UserID, error)
1717
UpdateFullName(userID model.UserID, fullName string) (*record.User, error)
18-
UpdateIcon(userID model.UserID, icon []byte) (*record.User, error)
18+
UpdateIcon(userID model.UserID, filename string) (u *record.User, old string, err error)
1919
}

0 commit comments

Comments
 (0)