11package toolkit
22
33import (
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.
1822type 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
4148func (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
5462func (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