• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cinode / go / 6082407089

05 Sep 2023 08:55AM UTC coverage: 90.595% (+0.1%) from 90.484%
6082407089

push

github

byo
Merge branch 'fix-tests'

1782 of 1967 relevant lines covered (90.59%)

1.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.97
/pkg/datastore/webinterface.go
1
/*
2
Copyright © 2022 Bartłomiej Święcki (byo)
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 datastore
18

19
import (
20
        "encoding/json"
21
        "errors"
22
        "io"
23
        "mime/multipart"
24
        "net/http"
25

26
        "github.com/cinode/go/pkg/common"
27
        "golang.org/x/exp/slog"
28
)
29

30
var (
31
        errNoData = errors.New("no upload data")
32
)
33

34
// WebInterface provides simple web interface for given Datastore
35
type webInterface struct {
36
        ds  DS
37
        log *slog.Logger
38
}
39

40
type webInterfaceOption func(i *webInterface)
41

42
func WebInterfaceOptionLogger(log *slog.Logger) webInterfaceOption {
1✔
43
        return func(i *webInterface) { i.log = log }
2✔
44
}
45

46
// WebInterface returns http handler representing web interface to given
47
// Datastore instance
48
func WebInterface(ds DS, opts ...webInterfaceOption) http.Handler {
1✔
49
        ret := &webInterface{
1✔
50
                ds: ds,
1✔
51
        }
1✔
52

1✔
53
        for _, o := range opts {
2✔
54
                o(ret)
1✔
55
        }
1✔
56

57
        if ret.log == nil {
2✔
58
                ret.log = slog.Default()
1✔
59
        }
1✔
60

61
        return ret
1✔
62
}
63

64
func (i *webInterface) ServeHTTP(w http.ResponseWriter, r *http.Request) {
1✔
65
        switch r.Method {
1✔
66
        case http.MethodGet:
1✔
67
                i.serveGet(w, r)
1✔
68
        case http.MethodPut:
1✔
69
                i.servePut(w, r)
1✔
70
        case http.MethodDelete:
1✔
71
                i.serveDelete(w, r)
1✔
72
        case http.MethodHead:
1✔
73
                i.serveHead(w, r)
1✔
74
        default:
1✔
75
                http.Error(w, "Unsupported method", http.StatusMethodNotAllowed)
1✔
76
        }
77
}
78

79
func (i *webInterface) getName(w http.ResponseWriter, r *http.Request) (common.BlobName, error) {
1✔
80
        // Don't allow url queries and require path to start with '/'
1✔
81
        if r.URL.Path[0] != '/' || r.URL.RawQuery != "" {
2✔
82
                return nil, common.ErrInvalidBlobName
1✔
83
        }
1✔
84

85
        bn, err := common.BlobNameFromString(r.URL.Path[1:])
1✔
86
        if err != nil {
1✔
87
                return nil, err
×
88
        }
×
89

90
        return bn, nil
1✔
91
}
92

93
func (i *webInterface) sendName(name common.BlobName, w http.ResponseWriter, r *http.Request) {
1✔
94
        w.Header().Set("Content-type", "application/json")
1✔
95
        json.NewEncoder(w).Encode(&webNameResponse{
1✔
96
                Name: name.String(),
1✔
97
        })
1✔
98
}
1✔
99

100
func (i *webInterface) sendError(w http.ResponseWriter, httpCode int, code string, message string) {
1✔
101
        w.Header().Set("Content-type", "application/json")
1✔
102
        w.WriteHeader(httpCode)
1✔
103
        json.NewEncoder(w).Encode(&webErrResponse{
1✔
104
                Code:    code,
1✔
105
                Message: message,
1✔
106
        })
1✔
107
}
1✔
108
func (i *webInterface) checkErr(err error, w http.ResponseWriter, r *http.Request) bool {
1✔
109
        if err == nil {
2✔
110
                return true
1✔
111
        }
1✔
112

113
        if errors.Is(err, ErrNotFound) {
2✔
114
                http.NotFound(w, r)
1✔
115
                return false
1✔
116
        }
1✔
117

118
        code := webErrToCode(err)
1✔
119
        if code != "" {
2✔
120
                i.sendError(w, http.StatusBadRequest, code, err.Error())
1✔
121
                return false
1✔
122
        }
1✔
123

124
        i.log.Error(
1✔
125
                "Internal error happened while processing the request", err,
1✔
126
                slog.Group("req",
1✔
127
                        slog.String("remoteAddr", r.RemoteAddr),
1✔
128
                        slog.String("method", r.Method),
1✔
129
                        slog.String("url", r.URL.String()),
1✔
130
                ),
1✔
131
        )
1✔
132
        http.Error(w, "Internal server error", http.StatusInternalServerError)
1✔
133
        return false
1✔
134
}
135

136
func (i *webInterface) serveGet(w http.ResponseWriter, r *http.Request) {
1✔
137
        name, err := i.getName(w, r)
1✔
138
        if !i.checkErr(err, w, r) {
2✔
139
                return
1✔
140
        }
1✔
141

142
        rc, err := i.ds.Open(r.Context(), name)
1✔
143
        if !i.checkErr(err, w, r) {
2✔
144
                return
1✔
145
        }
1✔
146

147
        defer rc.Close()
1✔
148
        io.Copy(w, rc)
1✔
149
        // TODO: Log error / drop the connection ? It may be too late to send the error to the user
150
        // thus we have to assume that the blob will be validated on the other side
151
}
152

153
type partReader struct {
154
        p *multipart.Part
155
        b io.Closer
156
}
157

158
func (r *partReader) Read(b []byte) (int, error) {
1✔
159
        return r.p.Read(b)
1✔
160
}
1✔
161

162
func (r *partReader) Close() error {
1✔
163
        err1 := r.p.Close()
1✔
164
        err2 := r.b.Close()
1✔
165
        if err1 != nil {
1✔
166
                return err1
×
167
        }
×
168
        return err2
1✔
169
}
170

171
func (i *webInterface) getUploadReader(r *http.Request) (io.ReadCloser, error) {
1✔
172

1✔
173
        mpr, err := r.MultipartReader()
1✔
174
        if err == http.ErrNotMultipart {
2✔
175
                // Not multipart, read raw body data
1✔
176
                return r.Body, nil
1✔
177
        }
1✔
178
        if err != nil {
1✔
179
                return nil, err
×
180
        }
×
181

182
        for {
2✔
183
                // Get next part of the upload
1✔
184
                part, err := mpr.NextPart()
1✔
185
                if err == io.EOF {
2✔
186
                        return nil, errNoData
1✔
187
                }
1✔
188
                if err != nil {
1✔
189
                        return nil, err
×
190
                }
×
191

192
                // Search for first file input
193
                fn := part.FileName()
1✔
194
                if fn != "" {
2✔
195
                        return &partReader{
1✔
196
                                p: part,
1✔
197
                                b: r.Body,
1✔
198
                        }, nil
1✔
199
                }
1✔
200
        }
201
}
202

203
func (i *webInterface) servePut(w http.ResponseWriter, r *http.Request) {
1✔
204
        name, err := i.getName(w, r)
1✔
205
        if !i.checkErr(err, w, r) {
2✔
206
                return
1✔
207
        }
1✔
208

209
        reader, err := i.getUploadReader(r)
1✔
210
        if !i.checkErr(err, w, r) {
2✔
211
                return
1✔
212
        }
1✔
213
        defer reader.Close()
1✔
214

1✔
215
        err = i.ds.Update(r.Context(), name, reader)
1✔
216
        if !i.checkErr(err, w, r) {
2✔
217
                return
1✔
218
        }
1✔
219

220
        i.sendName(name, w, r)
1✔
221
}
222

223
func (i *webInterface) serveDelete(w http.ResponseWriter, r *http.Request) {
1✔
224

1✔
225
        name, err := i.getName(w, r)
1✔
226
        if !i.checkErr(err, w, r) {
2✔
227
                return
1✔
228
        }
1✔
229

230
        err = i.ds.Delete(r.Context(), name)
1✔
231
        if !i.checkErr(err, w, r) {
2✔
232
                return
1✔
233
        }
1✔
234

235
        i.sendName(name, w, r)
1✔
236
}
237

238
func (i *webInterface) serveHead(w http.ResponseWriter, r *http.Request) {
1✔
239
        name, err := i.getName(w, r)
1✔
240
        if !i.checkErr(err, w, r) {
2✔
241
                return
1✔
242
        }
1✔
243

244
        exists, err := i.ds.Exists(r.Context(), name)
1✔
245
        if !i.checkErr(err, w, r) {
2✔
246
                return
1✔
247
        }
1✔
248

249
        if !exists {
2✔
250
                http.NotFound(w, r)
1✔
251
        }
1✔
252
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc