Skip to content

Commit

Permalink
v2
Browse files Browse the repository at this point in the history
  • Loading branch information
furches-cadre5 committed Oct 25, 2024
1 parent 4cbfb2b commit 17c3a45
Show file tree
Hide file tree
Showing 7 changed files with 729 additions and 0 deletions.
21 changes: 21 additions & 0 deletions v2/LICENSE
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.
21 changes: 21 additions & 0 deletions v2/README.md
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`
3 changes: 3 additions & 0 deletions v2/go.mod
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
Binary file added v2/testdata/test.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added v2/testdata/test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
322 changes: 322 additions & 0 deletions v2/tools.go
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
}
Loading

0 comments on commit 17c3a45

Please sign in to comment.