Skip to content

Commit 39bbe61

Browse files
authored
Merge pull request #69 from michaelcoll/feat/update-pagination
feat : Update pagination handling
2 parents e07228b + 7b99574 commit 39bbe61

File tree

16 files changed

+397
-107
lines changed

16 files changed

+397
-107
lines changed

.editorconfig

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
# https://editorconfig.org/
22
root = true
33

4-
[*]
5-
indent_style = space
6-
indent_size = 2
7-
charset = utf-8
8-
trim_trailing_whitespace = true
9-
end_of_line = lf
10-
114
# Tab indentation (no size specified)
125
[Makefile]
136
indent_style = tab

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ require (
1111
github.com/google/uuid v1.3.0
1212
github.com/gwatts/gin-adapter v1.0.0
1313
github.com/joeig/gin-cachecontrol v1.1.1
14-
github.com/michaelcoll/gallery-proto v0.9.1
14+
github.com/michaelcoll/gallery-proto v0.11.0
1515
github.com/spf13/cobra v1.6.1
1616
github.com/stretchr/testify v1.8.1
17-
google.golang.org/grpc v1.52.0
17+
google.golang.org/grpc v1.52.3
1818
)
1919

2020
require (
@@ -41,7 +41,7 @@ require (
4141
golang.org/x/net v0.5.0 // indirect
4242
golang.org/x/sys v0.4.0 // indirect
4343
golang.org/x/text v0.6.0 // indirect
44-
google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2 // indirect
44+
google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa // indirect
4545
google.golang.org/protobuf v1.28.1 // indirect
4646
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
4747
gopkg.in/yaml.v2 v2.4.0 // indirect

go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
6262
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
6363
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
6464
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
65-
github.com/michaelcoll/gallery-proto v0.9.1 h1:vGbT8+DdS2T6QYoUYes48EPa2J/LAAKgE6/Y/r7pqIg=
66-
github.com/michaelcoll/gallery-proto v0.9.1/go.mod h1:u+8HDPcdiLSkZgArkWvFTD3fsGI7sddH6iZRH0An0aE=
65+
github.com/michaelcoll/gallery-proto v0.11.0 h1:xY2lWD/TTN476F20/Ys4+RgXkXUOvCHhQoAFKRIWTYs=
66+
github.com/michaelcoll/gallery-proto v0.11.0/go.mod h1:u+8HDPcdiLSkZgArkWvFTD3fsGI7sddH6iZRH0An0aE=
6767
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
6868
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
6969
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -123,10 +123,10 @@ golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
123123
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
124124
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
125125
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
126-
google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2 h1:O97sLx/Xmb/KIZHB/2/BzofxBs5QmmR0LcihPtllmbc=
127-
google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
128-
google.golang.org/grpc v1.52.0 h1:kd48UiU7EHsV4rnLyOJRuP/Il/UHE7gdDAQ+SZI7nZk=
129-
google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY=
126+
google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa h1:qQPhfbPO23fwm/9lQr91L1u62Zo6cm+zI+slZT+uf+o=
127+
google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
128+
google.golang.org/grpc v1.52.3 h1:pf7sOysg4LdgBqduXveGKrcEwbStiK2rtfghdzlUYDQ=
129+
google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY=
130130
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
131131
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
132132
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=

internal/photo/domain/service/photoService.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ func NewPhotoService(c PhotoServiceCaller) PhotoService {
3030
return PhotoService{c: c}
3131
}
3232

33-
func (s *PhotoService) List(ctx context.Context, daemon *model.Daemon, page uint32, pageSize uint32) ([]*model.Photo, error) {
34-
return s.c.List(ctx, daemon, page, pageSize)
33+
func (s *PhotoService) List(ctx context.Context, daemon *model.Daemon, offset uint32, limit uint32) ([]*model.Photo, uint32, error) {
34+
return s.c.List(ctx, daemon, offset, limit)
3535
}
3636

3737
func (s *PhotoService) ContentByHash(ctx context.Context, daemon *model.Daemon, hash string) ([]byte, string, error) {

internal/photo/domain/service/photoServiceCaller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
)
2424

2525
type PhotoServiceCaller interface {
26-
List(ctx context.Context, d *model.Daemon, page uint32, pageSize uint32) ([]*model.Photo, error)
26+
List(ctx context.Context, d *model.Daemon, page uint32, pageSize uint32) ([]*model.Photo, uint32, error)
2727
Exists(ctx context.Context, d *model.Daemon, hash string) (bool, error)
2828
ContentByHash(ctx context.Context, d *model.Daemon, hash string) ([]byte, string, error)
2929
ThumbnailByHash(ctx context.Context, d *model.Daemon, hash string, width uint32, height uint32) ([]byte, string, error)

internal/photo/infrastructure/caller/daemonGrpcCaller.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,29 +39,29 @@ func New() *PhotoServiceGrpcCaller {
3939
return &PhotoServiceGrpcCaller{}
4040
}
4141

42-
func (c *PhotoServiceGrpcCaller) List(ctx context.Context, d *model.Daemon, page uint32, pageSize uint32) ([]*model.Photo, error) {
42+
func (c *PhotoServiceGrpcCaller) List(ctx context.Context, d *model.Daemon, offset uint32, limit uint32) ([]*model.Photo, uint32, error) {
4343

4444
client, conn, err := createClient(d)
4545
if err != nil {
46-
return nil, status.Errorf(codes.Unavailable, "can't connect to the daemon (%v)", err)
46+
return nil, 0, status.Errorf(codes.Unavailable, "can't connect to the daemon (%v)", err)
4747
}
4848
defer closeConnection(conn)
4949

5050
photoResponse, err := client.GetPhotos(ctx, &photov1.GetPhotosRequest{
51-
Page: page,
52-
PageSize: pageSize,
51+
Offset: offset,
52+
Limit: limit,
5353
})
5454
if err != nil {
55-
return nil, err
55+
return nil, 0, err
5656
}
5757

58-
photos := make([]*model.Photo, len(photoResponse.GetPhotos()))
58+
photos := make([]*model.Photo, len(photoResponse.Photos))
5959

60-
for i, photo := range photoResponse.GetPhotos() {
60+
for i, photo := range photoResponse.Photos {
6161
photos[i] = toDomain(photo)
6262
}
6363

64-
return photos, nil
64+
return photos, photoResponse.Total, nil
6565
}
6666

6767
func (c *PhotoServiceGrpcCaller) Exists(ctx context.Context, d *model.Daemon, hash string) (bool, error) {

internal/photo/presentation/baseApiController.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525

2626
"github.com/fatih/color"
2727
"github.com/gin-gonic/gin"
28-
2928
"github.com/michaelcoll/gallery-web/internal/photo/domain/service"
3029
)
3130

@@ -70,7 +69,7 @@ func (c *ApiController) Serve() {
7069
mediaGroup.GET("/daemon/:id/thumbnail/:hash", c.thumbnailByHash)
7170

7271
// Listen and serve on 0.0.0.0:8080
73-
fmt.Printf("%s Listening API on 0.0.0.0%s\n", color.GreenString("✓"), color.GreenString(apiPort))
72+
fmt.Printf("%s Listening API on http://0.0.0.0%s\n", color.GreenString("✓"), color.GreenString(apiPort))
7473
err := router.Run(apiPort)
7574
if err != nil {
7675
log.Fatalf("Error starting server : %v", err)

internal/photo/presentation/errors.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright (c) 2023 Michaël COLL.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package presentation
18+
19+
import "fmt"
20+
21+
type HttpStatusError struct {
22+
status int
23+
message string
24+
}
25+
26+
func (e *HttpStatusError) HTTPStatus() int {
27+
return e.status
28+
}
29+
30+
func (e *HttpStatusError) Error() string {
31+
return e.message
32+
}
33+
34+
func Errorf(status int, format string, a ...interface{}) error {
35+
return &HttpStatusError{status, fmt.Sprintf(format, a...)}
36+
}
37+
38+
func FromError(err error) (int, bool) {
39+
if se, ok := err.(interface {
40+
HTTPStatus() int
41+
}); ok {
42+
return se.HTTPStatus(), true
43+
}
44+
45+
return 0, false
46+
}

internal/photo/presentation/middleware.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ func addCommonMiddlewares(group *gin.Engine) {
4242
group.Use(cors.New(cors.Config{
4343
AllowOrigins: []string{"http://localhost:4040"},
4444
AllowMethods: []string{"GET"},
45-
AllowHeaders: []string{"Content-Type", "Content-Length", "Accept-Encoding", "Authorization", "Cache-Control"},
46-
ExposeHeaders: []string{"Content-Length"},
45+
AllowHeaders: []string{"Content-Type", "Content-Length", "Accept-Encoding", "Authorization", "Cache-Control", "Range"},
46+
ExposeHeaders: []string{"Content-Length", "Content-Range"},
4747
AllowCredentials: true,
4848
MaxAge: 12 * time.Hour,
4949
}))

internal/photo/presentation/photoApiController.go

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,31 +17,73 @@
1717
package presentation
1818

1919
import (
20+
"fmt"
2021
"net/http"
22+
"regexp"
2123
"strconv"
2224

2325
"github.com/gin-gonic/gin"
2426
)
2527

28+
var rangeRxp = regexp.MustCompile(`(?P<Unit>.*)=(?P<Start>[0-9]+)-(?P<End>[0-9]*)`)
29+
2630
func (c *ApiController) mediaList(ctx *gin.Context) {
2731
daemon, err := c.getDaemonById(ctx, true)
2832
if err != nil {
2933
handleError(ctx, err)
3034
return
3135
}
3236

33-
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "0"))
34-
pageSize, _ := strconv.Atoi(ctx.DefaultQuery("pageSize", "25"))
37+
start, end, err := extractRangeHeader(ctx.GetHeader("Range"))
38+
if err != nil {
39+
handleError(ctx, err)
40+
return
41+
}
3542

36-
photos, err := c.photoService.List(ctx.Request.Context(), daemon, uint32(page), uint32(pageSize))
43+
photos, total, err := c.photoService.List(ctx.Request.Context(), daemon, uint32(start), uint32(end-start))
3744
if err != nil {
3845
handleError(ctx, err)
3946
return
4047
}
4148

49+
ctx.Header("Content-Range", fmt.Sprintf("%s %d-%d/%d", "photo", start, start+len(photos), total))
4250
ctx.JSON(http.StatusOK, photos)
4351
}
4452

53+
func extractRangeHeader(rangeHeader string) (int, int, error) {
54+
r := rangeRxp.FindStringSubmatch(rangeHeader)
55+
st := http.StatusRequestedRangeNotSatisfiable
56+
57+
if len(r) < 4 {
58+
return 0, 0, Errorf(st, "Range is not valid, supported format : photo=0-25")
59+
}
60+
61+
if r[1] != "photo" {
62+
return 0, 0, Errorf(st, "Unit in range is not valid, supported unit : photo")
63+
}
64+
65+
start, errStart := strconv.Atoi(r[2])
66+
end, errEnd := strconv.Atoi(r[3])
67+
68+
if len(r[3]) == 0 {
69+
end = 0
70+
}
71+
72+
if errStart != nil {
73+
return 0, 0, Errorf(st, "Start range is not valid")
74+
}
75+
76+
if len(r[3]) != 0 && errEnd != nil {
77+
return 0, 0, Errorf(st, "End range is not valid")
78+
}
79+
80+
if end != 0 && start >= end {
81+
return 0, 0, Errorf(st, "Range is not valid, start > end")
82+
}
83+
84+
return start, end, nil
85+
}
86+
4587
func (c *ApiController) contentByHash(ctx *gin.Context) {
4688

4789
daemon, err := c.getDaemonById(ctx, false)

0 commit comments

Comments
 (0)