Skip to content

Commit 4c878ac

Browse files
author
unknown
committed
new vesion 2
1 parent 02d32bd commit 4c878ac

File tree

8 files changed

+754
-0
lines changed

8 files changed

+754
-0
lines changed

v2/.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Binaries for programs and plugins
2+
*.exe
3+
*.exe~
4+
*.dll
5+
*.so
6+
*.dylib
7+
8+
# Test binary, built with `go test -c`
9+
*.test
10+
11+
# Output of the go coverage tool, specifically when used with LiteIDE
12+
*.out
13+
14+
# Dependency directories (remove the comment below to include it)
15+
testdata/uploads/*
16+
#

v2/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Toolkit
2+
3+
A simple example of how to create a reusable Go module with commonly used tools.
4+
5+
The included tools are:
6+
7+
- [x] Read JSON
8+
- [x] Write JSON
9+
- [x] Produce a JSON encoded error response
10+
- [X] Upload a file to a specified directory
11+
- [x] Download a static file
12+
- [X] Get a random string of length n
13+
- [x] Post JSON to a remote service
14+
- [x] Create a directory, including all parent directories, if it does not already exist
15+
- [x] Create a URL safe slug from a string
16+
17+
## Installation
18+
19+
`go get -u github.com/fastscripts/toolkit`
20+
`go get -u github.com/tsawler/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/fastscripts/toolkit/v2
2+
3+
go 1.19

v2/license.md

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) 2022 Trevor Sawler
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/testdata/Bee.png

57.5 KB
Loading

v2/testdata/img.png

57.5 KB
Loading

v2/tools.go

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

0 commit comments

Comments
 (0)