-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4cbfb2b
commit 17c3a45
Showing
7 changed files
with
729 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2024 Steve | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# Go-Toolkit | ||
|
||
A simple example of how to create a reusable Go module with commonly used tools. | ||
|
||
Made as apart of [Trevor Sawler's course on Udemy](https://www.udemy.com/course/building-a-module-in-go-golang) | ||
|
||
The included tools are: | ||
|
||
- [X] Read JSON | ||
- [X] Write JSON | ||
- [X] Produce a JSON encoded error response | ||
- [X] Upload a file to a specified directory | ||
- [X] Download a static file | ||
- [X] Get a random string of length n | ||
- [X] Post JSON to a remote service | ||
- [X] Create a directory, including all parent directories, if it does not already exist | ||
- [X] Create a URL safe slug from a string | ||
|
||
## Installation | ||
|
||
`go get -u github.com/msf42/go-toolkit` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module github.com/MSF42/go-toolkit/v2 | ||
|
||
go 1.22.4 |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,322 @@ | ||
package toolkit | ||
|
||
import ( | ||
"bytes" | ||
"crypto/rand" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"os" | ||
"path/filepath" | ||
"regexp" | ||
"strings" | ||
) | ||
|
||
const randomStringSource = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_+" | ||
|
||
// Tools is the type used to instantiate this module. Any variable of this type | ||
// will have access to all the methods with the receiver Tools. | ||
type Tools struct { | ||
MaxFileSize int | ||
AllowedFileTypes []string | ||
MaxJSONSize int | ||
AllowUnknownFields bool | ||
} | ||
|
||
// RandomString generates a random string of length n | ||
func (t *Tools) RandomString(n int) string { | ||
s, r := make([]rune, n), []rune(randomStringSource) | ||
for i := range s { | ||
p, _ := rand.Prime(rand.Reader, len(r)) | ||
x, y := p.Uint64(), uint64(len(r)) | ||
s[i] = r[x%y] | ||
} | ||
return string(s) | ||
} | ||
|
||
// UploadedFile is a struct used to save information about an uploaded file | ||
type UploadedFile struct { | ||
NewFileName string | ||
OriginalFileName string | ||
FileSize int64 | ||
} | ||
|
||
// UploadOneFile uploads one file | ||
func (t *Tools) UploadOneFile(r *http.Request, uploadDir string, rename ...bool) (*UploadedFile, error) { | ||
renameFile := true | ||
if len(rename) > 0 { | ||
renameFile = rename[0] | ||
} | ||
files, err := t.UploadFiles(r, uploadDir, renameFile) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return files[0], nil | ||
} | ||
|
||
// UploadFiles uploads multiple files | ||
func (t *Tools) UploadFiles(r *http.Request, uploadDir string, rename ...bool) ([]*UploadedFile, error) { | ||
renameFile := true | ||
if len(rename) > 0 { | ||
renameFile = rename[0] | ||
} | ||
|
||
var uploadedFiles []*UploadedFile | ||
|
||
if t.MaxFileSize == 0 { | ||
t.MaxFileSize = 1024 * 1024 * 1024 | ||
} | ||
|
||
err := t.CreateDirIfNotExist(uploadDir) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
err = r.ParseMultipartForm(int64(t.MaxFileSize)) | ||
if err != nil { | ||
return nil, errors.New("the uploaded file size is too big") | ||
} | ||
|
||
for _, fHeaders := range r.MultipartForm.File { | ||
for _, hdr := range fHeaders { | ||
uploadedFiles, err = func(uploadedFiles []*UploadedFile) ([]*UploadedFile, error) { | ||
var uploadedFile UploadedFile | ||
infile, err := hdr.Open() | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer infile.Close() | ||
|
||
buff := make([]byte, 512) | ||
_, err = infile.Read(buff) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
allowed := false | ||
fileType := http.DetectContentType(buff) | ||
|
||
if len(t.AllowedFileTypes) > 0 { | ||
for _, x := range t.AllowedFileTypes { | ||
if strings.EqualFold(fileType, x) { | ||
allowed = true | ||
} | ||
} | ||
} else { | ||
allowed = true | ||
} | ||
|
||
if !allowed { | ||
return nil, errors.New("the uploaded file type is not permitted") | ||
} | ||
|
||
_, err = infile.Seek(0, 0) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if renameFile { | ||
uploadedFile.NewFileName = fmt.Sprintf("%s%s", t.RandomString(25), filepath.Ext(hdr.Filename)) | ||
} else { | ||
uploadedFile.NewFileName = hdr.Filename | ||
} | ||
uploadedFile.OriginalFileName = hdr.Filename | ||
|
||
var outfile *os.File | ||
defer outfile.Close() | ||
|
||
if outfile, err = os.Create(filepath.Join(uploadDir, uploadedFile.NewFileName)); err != nil { | ||
return nil, err | ||
} else { | ||
fileSize, err := io.Copy(outfile, infile) | ||
if err != nil { | ||
return nil, err | ||
} | ||
uploadedFile.FileSize = fileSize | ||
} | ||
uploadedFiles = append(uploadedFiles, &uploadedFile) | ||
|
||
return uploadedFiles, nil | ||
|
||
}(uploadedFiles) | ||
|
||
if err != nil { | ||
return uploadedFiles, err | ||
} | ||
} | ||
} | ||
return uploadedFiles, nil | ||
} | ||
|
||
// CreateDirIfNotExist creates a directory and all necessary parents if it does not exist | ||
func (t *Tools) CreateDirIfNotExist(path string) error { | ||
const mode = 0755 | ||
if _, err := os.Stat(path); os.IsNotExist(err) { | ||
err := os.MkdirAll(path, mode) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// Slugify is a very simple slug generator | ||
func (t *Tools) Slugify(s string) (string, error) { | ||
if s == "" { | ||
return "", errors.New("empty string not permitted") | ||
} | ||
|
||
var re = regexp.MustCompile(`[^a-z\d]+`) | ||
slug := strings.Trim(re.ReplaceAllString(strings.ToLower(s), "-"), "-") | ||
if len(slug) == 0 { | ||
return "", errors.New("after removing characters, slug is zero length") | ||
} | ||
return slug, nil | ||
} | ||
|
||
// DownloadStaticFile downloads a file without displaying it in the browser | ||
func (t *Tools) DownloadStaticFile(w http.ResponseWriter, r *http.Request, pathName, displayName string) { | ||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", displayName)) | ||
|
||
http.ServeFile(w, r, pathName) | ||
} | ||
|
||
// JSONResponse is the type used for sending JSON around | ||
type JSONResponse struct { | ||
Error bool `json:"error"` | ||
Message string `json:"message"` | ||
Data interface{} `json:"data,omitempty"` | ||
} | ||
|
||
// ReadJSON tries to read the body of a request and converts from json into a go data variable | ||
func (t *Tools) ReadJSON(w http.ResponseWriter, r *http.Request, data interface{}) error { | ||
maxBytes := 1024 * 1024 | ||
if t.MaxJSONSize != 0 { | ||
maxBytes = t.MaxJSONSize | ||
} | ||
|
||
r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes)) | ||
|
||
dec := json.NewDecoder(r.Body) | ||
|
||
if !t.AllowUnknownFields { | ||
dec.DisallowUnknownFields() | ||
} | ||
|
||
err := dec.Decode(data) | ||
if err != nil { | ||
var syntaxError *json.SyntaxError | ||
var unmarshalTypeError *json.UnmarshalTypeError | ||
var invalidUnmarshalError *json.InvalidUnmarshalError | ||
|
||
switch { | ||
case errors.As(err, &syntaxError): | ||
return fmt.Errorf("body contains badly-formed JSON (at character %d)", syntaxError.Offset) | ||
|
||
case errors.Is(err, io.ErrUnexpectedEOF): | ||
return errors.New("body contains badly-formed JSON") | ||
|
||
case errors.As(err, &unmarshalTypeError): | ||
if unmarshalTypeError.Field != "" { | ||
return fmt.Errorf("body contains incorrect JSON type for field %q", unmarshalTypeError.Field) | ||
} | ||
return fmt.Errorf("body contains incorrect JSON type (at character %d)", unmarshalTypeError.Offset) | ||
|
||
case errors.Is(err, io.EOF): | ||
return errors.New("body must not be empty") | ||
|
||
case strings.HasPrefix(err.Error(), "json: unknown field"): | ||
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field") | ||
return fmt.Errorf("body contains unknown key %s", fieldName) | ||
|
||
case err.Error() == "http: request body too large": | ||
return fmt.Errorf("body must not be larger than %d bytes", maxBytes) | ||
|
||
case errors.As(err, &invalidUnmarshalError): | ||
return fmt.Errorf("error unmarshalling JSON: %s", err.Error()) | ||
|
||
default: | ||
return err | ||
} | ||
} | ||
|
||
err = dec.Decode(&struct{}{}) | ||
if err != io.EOF { | ||
return errors.New("body must contain only one JSON value") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// WriteJSON takes a response status code and arbitrary data and writes json to the client | ||
func (t *Tools) WriteJSON(w http.ResponseWriter, status int, data interface{}, headers ...http.Header) error { | ||
out, err := json.Marshal(data) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if len(headers) > 0 { | ||
for key, value := range headers[0] { | ||
w.Header()[key] = value | ||
} | ||
} | ||
|
||
w.Header().Set("Content-Type", "application/json") | ||
w.WriteHeader(status) | ||
_, err = w.Write(out) | ||
if err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
// ErrorJSON takes an error, & optionally a status code, and generates and sends a JSON error message | ||
func (t *Tools) ErrorJSON(w http.ResponseWriter, err error, status ...int) error { | ||
statusCode := http.StatusBadRequest | ||
|
||
if len(status) > 0 { | ||
statusCode = status[0] | ||
} | ||
|
||
var payload JSONResponse | ||
payload.Error = true | ||
payload.Message = err.Error() | ||
|
||
return t.WriteJSON(w, statusCode, payload) | ||
} | ||
|
||
// PushJSONToRemote posts arbitrary data to some URL as JSON, and returns the response, status code, and error, if any. | ||
// The final parameter, client, is optional. If none is specified, we use the standard http.Client. | ||
func (t *Tools) PushJSONToRemote(uri string, data interface{}, client ...*http.Client) (*http.Response, int, error) { | ||
// create json | ||
jsonData, err := json.Marshal(data) | ||
if err != nil { | ||
return nil, 0, err | ||
} | ||
|
||
// check for custom http client | ||
httpClient := &http.Client{} | ||
if len(client) > 0 { | ||
httpClient = client[0] | ||
} | ||
|
||
// build the request and set the header | ||
request, err := http.NewRequest("POST", uri, bytes.NewBuffer(jsonData)) | ||
if err != nil { | ||
return nil, 0, err | ||
} | ||
request.Header.Set("Content-Type", "application/json") | ||
|
||
// call the remote uri | ||
response, err := httpClient.Do(request) | ||
if err != nil { | ||
return nil, 0, err | ||
} | ||
defer response.Body.Close() | ||
|
||
// send response back | ||
return response, response.StatusCode, nil | ||
} |
Oops, something went wrong.