diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..54548ad294ca856618a0ed3509c084a6d05545e7
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2022, uAfrica Technologies (Pty) Ltd
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+  contributors may be used to endorse or promote products derived from
+  this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
index e048f9274e050f594dc581e0cf68f08608c2db96..9e8fbdcdda876afe1a7754e8b8e7f85e38e0169d 100644
--- a/README.md
+++ b/README.md
@@ -2,19 +2,24 @@
 A set of utilities used by our Golang projects
 * [errors](./errors/README.md)
 
-## Creating a new tag
-When making changes, a new tag needs to be made in order to use the updated library in your project. First pull the tags, and check the latest version.
-
-`git pull --tags`
-
-`git tag`
-
-The output will be a list of tags. Create a new tag with the version number increased. E.g. if the last tag was `v1.2.7`, the new tag could be `v1.2.8`. Then push the tag.
-
-`git tag -a v1.2.8 -m "Tag v1.2.8"`
-
-`git push --tags`
-
-For your project, upgrade to the new version by running the `go get` command.
-
-`go get gitlab.com/uafrica/go-utils@v1.2.8`
\ No newline at end of file
+## Creating a new release
+When making changes, a new release needs to be made in order to use the updated library in your project.
+
+1. First, make sure your `uafrica-tools` repository is up to date (minimum at commit `442f62f0`):
+```
+git pull
+make install
+```
+
+2. After your changes have been merged to the `main` branch of `go-utils`, run the following command which will automatically create a new tag:
+```
+ua release 
+```
+and select project `uafrica/go-utils`
+
+3. For your project, upgrade to the new version by running the `go get` command and specifying the new tab:
+```
+go get gitlab.com/uafrica/go-utils@v1.6.0
+```
+
+**Note:** The release documentation can be found in GitLab, by navigating to the new tag. For example: https://gitlab.com/uafrica/go-utils/-/tags/v1.6.0
diff --git a/s3/s3.go b/s3/s3.go
index f2639419b18db2e7449335b82db8f724fabac981..ddae2b0fac635365527605b0fdf64103cf0e31df 100644
--- a/s3/s3.go
+++ b/s3/s3.go
@@ -27,6 +27,12 @@ type S3UploadResponse struct {
 	FileSize int    `json:"file_size"`
 }
 
+type S3UploadSettings struct {
+	MimeType          MIMEType
+	RetrieveSignedUrl bool
+	ExpiryDuration    *time.Duration
+}
+
 type MIMEType string
 
 const (
@@ -48,6 +54,9 @@ const (
 	// MIMETypeImage defines the constant for the Image MIME type.
 	MIMETypeImage MIMEType = "image/*"
 
+	// MIMETypePNG defines the constant for the PNG MIME type.
+	MIMETypePNG MIMEType = "image/png"
+
 	// MIMETypeDefault defines the constant for the default MIME type.
 	MIMETypeDefault MIMEType = "application/octet-stream"
 
@@ -68,7 +77,7 @@ func NewSession(session *session.Session) *SessionWithHelpers {
 	}
 }
 
-func (s SessionWithHelpers) Upload(data []byte, bucket, fileName string, metadata *map[string]*string, isDebug bool) (*s3.PutObjectOutput, error) {
+func (s SessionWithHelpers) Upload(data []byte, bucket, fileName string, metadata *map[string]*string) (*s3.PutObjectOutput, error) {
 	mimeType := getTypeForFilename(fileName)
 	putInput := &s3.PutObjectInput{
 		Bucket:      aws.String(bucket),
@@ -89,18 +98,21 @@ func (s SessionWithHelpers) Upload(data []byte, bucket, fileName string, metadat
 	return response, nil
 }
 
-func (s SessionWithHelpers) UploadWith1DayExpiry(data []byte, bucket, fileName string, mimeType MIMEType, isDebug bool) (string, error) {
-	if mimeType == "" {
-		mimeType = getTypeForFilename(fileName)
+func (s SessionWithHelpers) UploadWithSettings(data []byte, bucket, fileName string, settings S3UploadSettings) (string, error) {
+	if settings.MimeType == "" {
+		settings.MimeType = getTypeForFilename(fileName)
 	}
 
-	expiry := time.Now().Add(24 * time.Hour)
 	putInput := &s3.PutObjectInput{
 		Bucket:      aws.String(bucket),
 		Key:         aws.String(fileName),
-		ContentType: aws.String(string(mimeType)),
+		ContentType: aws.String(string(settings.MimeType)),
 		Body:        bytes.NewReader(data),
-		Expires:     &expiry,
+	}
+
+	if settings.ExpiryDuration != nil {
+		expiry := time.Now().Add(*settings.ExpiryDuration)
+		putInput.Expires = &expiry
 	}
 
 	_, err := s.S3Session.PutObject(putInput)
@@ -108,33 +120,33 @@ func (s SessionWithHelpers) UploadWith1DayExpiry(data []byte, bucket, fileName s
 		return "", err
 	}
 
-	return s.GetSignedDownloadURL(bucket, fileName, 24*time.Hour, isDebug)
+	if settings.RetrieveSignedUrl {
+		return s.GetSignedDownloadURL(bucket, fileName, 24*time.Hour)
+	}
+
+	return "", nil
 }
 
-func (s SessionWithHelpers) UploadWithExpiry(data []byte, bucket, fileName string, expiryDuration time.Duration, mimeType MIMEType, isDebug bool) (string, error) {
+func (s SessionWithHelpers) UploadWith1DayExpiry(data []byte, bucket, fileName string, mimeType MIMEType) (string, error) {
 	if mimeType == "" {
 		mimeType = getTypeForFilename(fileName)
 	}
 
-	expiry := time.Now().Add(expiryDuration)
-	putInput := &s3.PutObjectInput{
-		Bucket:      aws.String(bucket),
-		Key:         aws.String(fileName),
-		ContentType: aws.String(string(mimeType)),
-		Body:        bytes.NewReader(data),
-		Expires:     &expiry,
-	}
-
-	_, err := s.S3Session.PutObject(putInput)
+	expiry := 24 * time.Hour
+	signedUrl, err := s.UploadWithSettings(data, bucket, fileName, S3UploadSettings{
+		MimeType:          mimeType,
+		RetrieveSignedUrl: true,
+		ExpiryDuration:    &expiry,
+	})
 	if err != nil {
 		return "", err
 	}
 
-	return s.GetSignedDownloadURL(bucket, fileName, 24*time.Hour, isDebug)
+	return signedUrl, nil
 }
 
 // GetSignedDownloadURL gets a signed download URL for the duration. If scv is nil, a new session will be created.
-func (s SessionWithHelpers) GetSignedDownloadURL(bucket string, fileName string, duration time.Duration, isDebug bool) (string, error) {
+func (s SessionWithHelpers) GetSignedDownloadURL(bucket string, fileName string, duration time.Duration) (string, error) {
 	getInput := &s3.GetObjectInput{
 		Bucket: aws.String(bucket),
 		Key:    aws.String(fileName),
@@ -173,10 +185,15 @@ func (s SessionWithHelpers) FileExists(bucket string, fileName string) (bool, er
 }
 
 // UploadWithFileExtension will upload a file to S3 and return a standard S3UploadResponse.
-func (s SessionWithHelpers) UploadWithFileExtension(data []byte, bucket, filePrefix string, fileExt string, mimeType MIMEType, isDebug bool) (*S3UploadResponse, error) {
+func (s SessionWithHelpers) UploadWithFileExtension(data []byte, bucket, filePrefix, fileExt string, mimeType MIMEType) (*S3UploadResponse, error) {
 	fileName := fmt.Sprintf("%s_%s.%s", filePrefix, uuid.New().String(), fileExt)
 
-	uploadUrl, err := s.UploadWith1DayExpiry(data, bucket, fileName, mimeType, isDebug)
+	duration := 24 * time.Hour
+	uploadUrl, err := s.UploadWithSettings(data, bucket, fileName, S3UploadSettings{
+		MimeType:          mimeType,
+		RetrieveSignedUrl: true,
+		ExpiryDuration:    &duration,
+	})
 	if err != nil {
 		return nil, err
 	}
@@ -255,9 +272,9 @@ func (s SessionWithHelpers) CopyObjectBucketToBucket(sourceBucket string, destin
 	// copy the file
 	copySource := url.QueryEscape(sourceBucket + "/" + sourceFileName)
 	copyObjectInput := &s3.CopyObjectInput{
-		Bucket:     aws.String(destinationBucket),   //destination bucket
-		CopySource: aws.String(copySource),          //source path (ie: myBucket/myFile.csv)
-		Key:        aws.String(destinationFilename), //filename on destination
+		Bucket:     aws.String(destinationBucket),   // destination bucket
+		CopySource: aws.String(copySource),          // source path (ie: myBucket/myFile.csv)
+		Key:        aws.String(destinationFilename), // filename on destination
 	}
 	_, err := s.S3Session.CopyObject(copyObjectInput)
 	if err != nil {
@@ -298,12 +315,19 @@ func (s SessionWithHelpers) DeleteObjectFromBucket(bucket string, fileName strin
 }
 
 func GetS3FileKey(fileName string, folder string) string {
+	var fileKey string
+
 	// Trim leading and trailing slashes
 	fileName = strings.TrimLeft(fileName, "/")
 	fileName = strings.TrimRight(fileName, "/")
 
-	folder = strings.TrimLeft(folder, "/")
-	folder = strings.TrimRight(folder, "/")
+	if folder != "" {
+		folder = strings.TrimLeft(folder, "/")
+		folder = strings.TrimRight(folder, "/")
+		fileKey += "/" + folder
+	}
+
+	fileKey += "/" + fileName
 
-	return "/" + folder + "/" + fileName
+	return fileKey
 }
diff --git a/string_utils/snake.go b/string_utils/snake.go
deleted file mode 100644
index 358d7fa5e2e503e1f417fc507bd659cbbb30f8f2..0000000000000000000000000000000000000000
--- a/string_utils/snake.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package string_utils
-
-import "regexp"
-
-const snakeCasePattern = `[a-z]([a-z0-9_]*[a-z0-9])*`
-
-var snakeCaseRegex = regexp.MustCompile("^" + snakeCasePattern + "$")
-
-func IsSnakeCase(name string) bool {
-	return snakeCaseRegex.MatchString(name)
-}
diff --git a/string_utils/string_utils.go b/string_utils/string_utils.go
index fc871f45f011f9f7e0f216570d23a6342dcffbde..bd219a790924e89867fc46680afb513e44cf9b49 100644
--- a/string_utils/string_utils.go
+++ b/string_utils/string_utils.go
@@ -14,6 +14,23 @@ import (
 	"golang.org/x/text/unicode/norm"
 )
 
+const snakeCasePattern = `[a-z]([a-z0-9_]*[a-z0-9])*`
+
+var snakeCaseRegex = regexp.MustCompile("^" + snakeCasePattern + "$")
+
+func IsSnakeCase(name string) bool {
+	return snakeCaseRegex.MatchString(name)
+}
+
+func SnakeToKebabString(s string) string {
+	s = strings.TrimSpace(s)
+
+	re := regexp.MustCompile("(_)")
+	s = re.ReplaceAllString(s, "-")
+
+	return s
+}
+
 // ReplaceNonSpacingMarks removes diacritics e.g. êžů becomes ezu
 func ReplaceNonSpacingMarks(str string) string {
 	t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC) // Mn: non-spacing marks