Skip to content

Commit 17c3a45

Browse files
v2
1 parent 4cbfb2b commit 17c3a45

File tree

7 files changed

+729
-0
lines changed

7 files changed

+729
-0
lines changed

v2/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Steve
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

v2/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Go-Toolkit
2+
3+
A simple example of how to create a reusable Go module with commonly used tools.
4+
5+
Made as apart of [Trevor Sawler's course on Udemy](https://www.udemy.com/course/building-a-module-in-go-golang)
6+
7+
The included tools are:
8+
9+
- [X] Read JSON
10+
- [X] Write JSON
11+
- [X] Produce a JSON encoded error response
12+
- [X] Upload a file to a specified directory
13+
- [X] Download a static file
14+
- [X] Get a random string of length n
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
18+
19+
## Installation
20+
21+
`go get -u github.com/msf42/go-toolkit`

v2/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/MSF42/go-toolkit/v2
2+
3+
go 1.22.4

v2/testdata/test.jpg

5.06 MB
Loading

v2/testdata/test.png

412 KB
Loading

v2/tools.go

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
package toolkit
2+
3+
import (
4+
"bytes"
5+
"crypto/rand"
6+
"encoding/json"
7+
"errors"
8+
"fmt"
9+
"io"
10+
"net/http"
11+
"os"
12+
"path/filepath"
13+
"regexp"
14+
"strings"
15+
)
16+
17+
const randomStringSource = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_+"
18+
19+
// Tools is the type used to instantiate this module. Any variable of this type
20+
// will have access to all the methods with the receiver Tools.
21+
type Tools struct {
22+
MaxFileSize int
23+
AllowedFileTypes []string
24+
MaxJSONSize int
25+
AllowUnknownFields bool
26+
}
27+
28+
// RandomString generates a random string of length n
29+
func (t *Tools) RandomString(n int) string {
30+
s, r := make([]rune, n), []rune(randomStringSource)
31+
for i := range s {
32+
p, _ := rand.Prime(rand.Reader, len(r))
33+
x, y := p.Uint64(), uint64(len(r))
34+
s[i] = r[x%y]
35+
}
36+
return string(s)
37+
}
38+
39+
// UploadedFile is a struct used to save information about an uploaded file
40+
type UploadedFile struct {
41+
NewFileName string
42+
OriginalFileName string
43+
FileSize int64
44+
}
45+
46+
// UploadOneFile uploads one file
47+
func (t *Tools) UploadOneFile(r *http.Request, uploadDir string, rename ...bool) (*UploadedFile, error) {
48+
renameFile := true
49+
if len(rename) > 0 {
50+
renameFile = rename[0]
51+
}
52+
files, err := t.UploadFiles(r, uploadDir, renameFile)
53+
if err != nil {
54+
return nil, err
55+
}
56+
57+
return files[0], nil
58+
}
59+
60+
// UploadFiles uploads multiple files
61+
func (t *Tools) UploadFiles(r *http.Request, uploadDir string, rename ...bool) ([]*UploadedFile, error) {
62+
renameFile := true
63+
if len(rename) > 0 {
64+
renameFile = rename[0]
65+
}
66+
67+
var uploadedFiles []*UploadedFile
68+
69+
if t.MaxFileSize == 0 {
70+
t.MaxFileSize = 1024 * 1024 * 1024
71+
}
72+
73+
err := t.CreateDirIfNotExist(uploadDir)
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
err = r.ParseMultipartForm(int64(t.MaxFileSize))
79+
if err != nil {
80+
return nil, errors.New("the uploaded file size is too big")
81+
}
82+
83+
for _, fHeaders := range r.MultipartForm.File {
84+
for _, hdr := range fHeaders {
85+
uploadedFiles, err = func(uploadedFiles []*UploadedFile) ([]*UploadedFile, error) {
86+
var uploadedFile UploadedFile
87+
infile, err := hdr.Open()
88+
if err != nil {
89+
return nil, err
90+
}
91+
defer infile.Close()
92+
93+
buff := make([]byte, 512)
94+
_, err = infile.Read(buff)
95+
if err != nil {
96+
return nil, err
97+
}
98+
99+
allowed := false
100+
fileType := http.DetectContentType(buff)
101+
102+
if len(t.AllowedFileTypes) > 0 {
103+
for _, x := range t.AllowedFileTypes {
104+
if strings.EqualFold(fileType, x) {
105+
allowed = true
106+
}
107+
}
108+
} else {
109+
allowed = true
110+
}
111+
112+
if !allowed {
113+
return nil, errors.New("the uploaded file type is not permitted")
114+
}
115+
116+
_, err = infile.Seek(0, 0)
117+
if err != nil {
118+
return nil, err
119+
}
120+
121+
if renameFile {
122+
uploadedFile.NewFileName = fmt.Sprintf("%s%s", t.RandomString(25), filepath.Ext(hdr.Filename))
123+
} else {
124+
uploadedFile.NewFileName = hdr.Filename
125+
}
126+
uploadedFile.OriginalFileName = hdr.Filename
127+
128+
var outfile *os.File
129+
defer outfile.Close()
130+
131+
if outfile, err = os.Create(filepath.Join(uploadDir, uploadedFile.NewFileName)); err != nil {
132+
return nil, err
133+
} else {
134+
fileSize, err := io.Copy(outfile, infile)
135+
if err != nil {
136+
return nil, err
137+
}
138+
uploadedFile.FileSize = fileSize
139+
}
140+
uploadedFiles = append(uploadedFiles, &uploadedFile)
141+
142+
return uploadedFiles, nil
143+
144+
}(uploadedFiles)
145+
146+
if err != nil {
147+
return uploadedFiles, err
148+
}
149+
}
150+
}
151+
return uploadedFiles, nil
152+
}
153+
154+
// CreateDirIfNotExist creates a directory and all necessary parents if it does not exist
155+
func (t *Tools) CreateDirIfNotExist(path string) error {
156+
const mode = 0755
157+
if _, err := os.Stat(path); os.IsNotExist(err) {
158+
err := os.MkdirAll(path, mode)
159+
if err != nil {
160+
return err
161+
}
162+
}
163+
return nil
164+
}
165+
166+
// Slugify is a very simple slug generator
167+
func (t *Tools) Slugify(s string) (string, error) {
168+
if s == "" {
169+
return "", errors.New("empty string not permitted")
170+
}
171+
172+
var re = regexp.MustCompile(`[^a-z\d]+`)
173+
slug := strings.Trim(re.ReplaceAllString(strings.ToLower(s), "-"), "-")
174+
if len(slug) == 0 {
175+
return "", errors.New("after removing characters, slug is zero length")
176+
}
177+
return slug, nil
178+
}
179+
180+
// DownloadStaticFile downloads a file without displaying it in the browser
181+
func (t *Tools) DownloadStaticFile(w http.ResponseWriter, r *http.Request, pathName, displayName string) {
182+
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", displayName))
183+
184+
http.ServeFile(w, r, pathName)
185+
}
186+
187+
// JSONResponse is the type used for sending JSON around
188+
type JSONResponse struct {
189+
Error bool `json:"error"`
190+
Message string `json:"message"`
191+
Data interface{} `json:"data,omitempty"`
192+
}
193+
194+
// ReadJSON tries to read the body of a request and converts from json into a go data variable
195+
func (t *Tools) ReadJSON(w http.ResponseWriter, r *http.Request, data interface{}) error {
196+
maxBytes := 1024 * 1024
197+
if t.MaxJSONSize != 0 {
198+
maxBytes = t.MaxJSONSize
199+
}
200+
201+
r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))
202+
203+
dec := json.NewDecoder(r.Body)
204+
205+
if !t.AllowUnknownFields {
206+
dec.DisallowUnknownFields()
207+
}
208+
209+
err := dec.Decode(data)
210+
if err != nil {
211+
var syntaxError *json.SyntaxError
212+
var unmarshalTypeError *json.UnmarshalTypeError
213+
var invalidUnmarshalError *json.InvalidUnmarshalError
214+
215+
switch {
216+
case errors.As(err, &syntaxError):
217+
return fmt.Errorf("body contains badly-formed JSON (at character %d)", syntaxError.Offset)
218+
219+
case errors.Is(err, io.ErrUnexpectedEOF):
220+
return errors.New("body contains badly-formed JSON")
221+
222+
case errors.As(err, &unmarshalTypeError):
223+
if unmarshalTypeError.Field != "" {
224+
return fmt.Errorf("body contains incorrect JSON type for field %q", unmarshalTypeError.Field)
225+
}
226+
return fmt.Errorf("body contains incorrect JSON type (at character %d)", unmarshalTypeError.Offset)
227+
228+
case errors.Is(err, io.EOF):
229+
return errors.New("body must not be empty")
230+
231+
case strings.HasPrefix(err.Error(), "json: unknown field"):
232+
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field")
233+
return fmt.Errorf("body contains unknown key %s", fieldName)
234+
235+
case err.Error() == "http: request body too large":
236+
return fmt.Errorf("body must not be larger than %d bytes", maxBytes)
237+
238+
case errors.As(err, &invalidUnmarshalError):
239+
return fmt.Errorf("error unmarshalling JSON: %s", err.Error())
240+
241+
default:
242+
return err
243+
}
244+
}
245+
246+
err = dec.Decode(&struct{}{})
247+
if err != io.EOF {
248+
return errors.New("body must contain only one JSON value")
249+
}
250+
251+
return nil
252+
}
253+
254+
// WriteJSON takes a response status code and arbitrary data and writes json to the client
255+
func (t *Tools) WriteJSON(w http.ResponseWriter, status int, data interface{}, headers ...http.Header) error {
256+
out, err := json.Marshal(data)
257+
if err != nil {
258+
return err
259+
}
260+
261+
if len(headers) > 0 {
262+
for key, value := range headers[0] {
263+
w.Header()[key] = value
264+
}
265+
}
266+
267+
w.Header().Set("Content-Type", "application/json")
268+
w.WriteHeader(status)
269+
_, err = w.Write(out)
270+
if err != nil {
271+
return err
272+
}
273+
return nil
274+
}
275+
276+
// ErrorJSON takes an error, & optionally a status code, and generates and sends a JSON error message
277+
func (t *Tools) ErrorJSON(w http.ResponseWriter, err error, status ...int) error {
278+
statusCode := http.StatusBadRequest
279+
280+
if len(status) > 0 {
281+
statusCode = status[0]
282+
}
283+
284+
var payload JSONResponse
285+
payload.Error = true
286+
payload.Message = err.Error()
287+
288+
return t.WriteJSON(w, statusCode, payload)
289+
}
290+
291+
// PushJSONToRemote posts arbitrary data to some URL as JSON, and returns the response, status code, and error, if any.
292+
// The final parameter, client, is optional. If none is specified, we use the standard http.Client.
293+
func (t *Tools) PushJSONToRemote(uri string, data interface{}, client ...*http.Client) (*http.Response, int, error) {
294+
// create json
295+
jsonData, err := json.Marshal(data)
296+
if err != nil {
297+
return nil, 0, err
298+
}
299+
300+
// check for custom http client
301+
httpClient := &http.Client{}
302+
if len(client) > 0 {
303+
httpClient = client[0]
304+
}
305+
306+
// build the request and set the header
307+
request, err := http.NewRequest("POST", uri, bytes.NewBuffer(jsonData))
308+
if err != nil {
309+
return nil, 0, err
310+
}
311+
request.Header.Set("Content-Type", "application/json")
312+
313+
// call the remote uri
314+
response, err := httpClient.Do(request)
315+
if err != nil {
316+
return nil, 0, err
317+
}
318+
defer response.Body.Close()
319+
320+
// send response back
321+
return response, response.StatusCode, nil
322+
}

0 commit comments

Comments
 (0)