Skip to content

Commit 4cbfb2b

Browse files
thru sec 8
1 parent f36687d commit 4cbfb2b

File tree

4 files changed

+403
-10
lines changed

4 files changed

+403
-10
lines changed

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ Made as apart of [Trevor Sawler's course on Udemy](https://www.udemy.com/course/
66

77
The included tools are:
88

9-
- [ ] Read JSON
10-
- [ ] Write JSON
11-
- [ ] Produce a JSON encoded error response
9+
- [X] Read JSON
10+
- [X] Write JSON
11+
- [X] Produce a JSON encoded error response
1212
- [X] Upload a file to a specified directory
13-
- [ ] Download a static file
13+
- [X] Download a static file
1414
- [X] Get a random string of length n
15-
- [ ] Post JSON to a remote service
16-
- [ ] Create a directory, including all parent directories, if it does not already exist
17-
- [ ] Create a URL safe slug from a string
15+
- [X] Post JSON to a remote service
16+
- [X] Create a directory, including all parent directories, if it does not already exist
17+
- [X] Create a URL safe slug from a string
1818

1919
## Installation
2020

testdata/test.jpg

5.06 MB
Loading

tools.go

Lines changed: 187 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package toolkit
22

33
import (
4+
"bytes"
45
"crypto/rand"
6+
"encoding/json"
57
"errors"
68
"fmt"
79
"io"
810
"net/http"
911
"os"
12+
"path"
1013
"path/filepath"
14+
"regexp"
1115
"strings"
1216
)
1317

@@ -16,8 +20,10 @@ const randomStringSource = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop
1620
// Tools is the type used to instantiate this module. Any variable of this type
1721
// will have access to all the methods with the receiver Tools.
1822
type Tools struct {
19-
MaxFileSize int
20-
AllowedFileTypes []string
23+
MaxFileSize int
24+
AllowedFileTypes []string
25+
MaxJSONSize int
26+
AllowUnknownFields bool
2127
}
2228

2329
// RandomString generates a random string of length n
@@ -38,6 +44,7 @@ type UploadedFile struct {
3844
FileSize int64
3945
}
4046

47+
// UploadOneFile uploads one file
4148
func (t *Tools) UploadOneFile(r *http.Request, uploadDir string, rename ...bool) (*UploadedFile, error) {
4249
renameFile := true
4350
if len(rename) > 0 {
@@ -51,6 +58,7 @@ func (t *Tools) UploadOneFile(r *http.Request, uploadDir string, rename ...bool)
5158
return files[0], nil
5259
}
5360

61+
// UploadFiles uploads multiple files
5462
func (t *Tools) UploadFiles(r *http.Request, uploadDir string, rename ...bool) ([]*UploadedFile, error) {
5563
renameFile := true
5664
if len(rename) > 0 {
@@ -63,7 +71,12 @@ func (t *Tools) UploadFiles(r *http.Request, uploadDir string, rename ...bool) (
6371
t.MaxFileSize = 1024 * 1024 * 1024
6472
}
6573

66-
err := r.ParseMultipartForm(int64(t.MaxFileSize))
74+
err := t.CreateDirIfNotExist(uploadDir)
75+
if err != nil {
76+
return nil, err
77+
}
78+
79+
err = r.ParseMultipartForm(int64(t.MaxFileSize))
6780
if err != nil {
6881
return nil, errors.New("the uploaded file size is too big")
6982
}
@@ -138,3 +151,174 @@ func (t *Tools) UploadFiles(r *http.Request, uploadDir string, rename ...bool) (
138151
}
139152
return uploadedFiles, nil
140153
}
154+
155+
// CreateDirIfNotExist creates a directory and all necessary parents if it does not exist
156+
func (t *Tools) CreateDirIfNotExist(path string) error {
157+
const mode = 0755
158+
if _, err := os.Stat(path); os.IsNotExist(err) {
159+
err := os.MkdirAll(path, mode)
160+
if err != nil {
161+
return err
162+
}
163+
}
164+
return nil
165+
}
166+
167+
// Slugify is a very simple slug generator
168+
func (t *Tools) Slugify(s string) (string, error) {
169+
if s == "" {
170+
return "", errors.New("empty string not permitted")
171+
}
172+
173+
var re = regexp.MustCompile(`[^a-z\d]+`)
174+
slug := strings.Trim(re.ReplaceAllString(strings.ToLower(s), "-"), "-")
175+
if len(slug) == 0 {
176+
return "", errors.New("after removing characters, slug is zero length")
177+
}
178+
return slug, nil
179+
}
180+
181+
// DownloadStaticFile downloads a file without displaying it in the browser
182+
func (t *Tools) DownloadStaticFile(w http.ResponseWriter, r *http.Request, p, file, displayName string) {
183+
fp := path.Join(p, file)
184+
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", displayName))
185+
186+
http.ServeFile(w, r, fp)
187+
}
188+
189+
// JSONResponse is the type used for sending JSON around
190+
type JSONResponse struct {
191+
Error bool `json:"error"`
192+
Message string `json:"message"`
193+
Data interface{} `json:"data,omitempty"`
194+
}
195+
196+
// ReadJSON tries to read the body of a request and converts from json into a go data variable
197+
func (t *Tools) ReadJSON(w http.ResponseWriter, r *http.Request, data interface{}) error {
198+
maxBytes := 1024 * 1024
199+
if t.MaxJSONSize != 0 {
200+
maxBytes = t.MaxJSONSize
201+
}
202+
203+
r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))
204+
205+
dec := json.NewDecoder(r.Body)
206+
207+
if !t.AllowUnknownFields {
208+
dec.DisallowUnknownFields()
209+
}
210+
211+
err := dec.Decode(data)
212+
if err != nil {
213+
var syntaxError *json.SyntaxError
214+
var unmarshalTypeError *json.UnmarshalTypeError
215+
var invalidUnmarshalError *json.InvalidUnmarshalError
216+
217+
switch {
218+
case errors.As(err, &syntaxError):
219+
return fmt.Errorf("body contains badly-formed JSON (at character %d)", syntaxError.Offset)
220+
221+
case errors.Is(err, io.ErrUnexpectedEOF):
222+
return errors.New("body contains badly-formed JSON")
223+
224+
case errors.As(err, &unmarshalTypeError):
225+
if unmarshalTypeError.Field != "" {
226+
return fmt.Errorf("body contains incorrect JSON type for field %q", unmarshalTypeError.Field)
227+
}
228+
return fmt.Errorf("body contains incorrect JSON type (at character %d)", unmarshalTypeError.Offset)
229+
230+
case errors.Is(err, io.EOF):
231+
return errors.New("body must not be empty")
232+
233+
case strings.HasPrefix(err.Error(), "json: unknown field"):
234+
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field")
235+
return fmt.Errorf("body contains unknown key %s", fieldName)
236+
237+
case err.Error() == "http: request body too large":
238+
return fmt.Errorf("body must not be larger than %d bytes", maxBytes)
239+
240+
case errors.As(err, &invalidUnmarshalError):
241+
return fmt.Errorf("error unmarshalling JSON: %s", err.Error())
242+
243+
default:
244+
return err
245+
}
246+
}
247+
248+
err = dec.Decode(&struct{}{})
249+
if err != io.EOF {
250+
return errors.New("body must contain only one JSON value")
251+
}
252+
253+
return nil
254+
}
255+
256+
// WriteJSON takes a response status code and arbitrary data and writes json to the client
257+
func (t *Tools) WriteJSON(w http.ResponseWriter, status int, data interface{}, headers ...http.Header) error {
258+
out, err := json.Marshal(data)
259+
if err != nil {
260+
return err
261+
}
262+
263+
if len(headers) > 0 {
264+
for key, value := range headers[0] {
265+
w.Header()[key] = value
266+
}
267+
}
268+
269+
w.Header().Set("Content-Type", "application/json")
270+
w.WriteHeader(status)
271+
_, err = w.Write(out)
272+
if err != nil {
273+
return err
274+
}
275+
return nil
276+
}
277+
278+
// ErrorJSON takes an error, & optionally a status code, and generates and sends a JSON error message
279+
func (t *Tools) ErrorJSON(w http.ResponseWriter, err error, status ...int) error {
280+
statusCode := http.StatusBadRequest
281+
282+
if len(status) > 0 {
283+
statusCode = status[0]
284+
}
285+
286+
var payload JSONResponse
287+
payload.Error = true
288+
payload.Message = err.Error()
289+
290+
return t.WriteJSON(w, statusCode, payload)
291+
}
292+
293+
// PushJSONToRemote posts arbitrary data to some URL as JSON, and returns the response, status code, and error, if any.
294+
// The final parameter, client, is optional. If none is specified, we use the standard http.Client.
295+
func (t *Tools) PushJSONToRemote(uri string, data interface{}, client ...*http.Client) (*http.Response, int, error) {
296+
// create json
297+
jsonData, err := json.Marshal(data)
298+
if err != nil {
299+
return nil, 0, err
300+
}
301+
302+
// check for custom http client
303+
httpClient := &http.Client{}
304+
if len(client) > 0 {
305+
httpClient = client[0]
306+
}
307+
308+
// build the request and set the header
309+
request, err := http.NewRequest("POST", uri, bytes.NewBuffer(jsonData))
310+
if err != nil {
311+
return nil, 0, err
312+
}
313+
request.Header.Set("Content-Type", "application/json")
314+
315+
// call the remote uri
316+
response, err := httpClient.Do(request)
317+
if err != nil {
318+
return nil, 0, err
319+
}
320+
defer response.Body.Close()
321+
322+
// send response back
323+
return response, response.StatusCode, nil
324+
}

0 commit comments

Comments
 (0)