Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: add File normalizer and utilize in runner
Browse files Browse the repository at this point in the history
jsteenb2 committed Jun 18, 2024
1 parent 9fe315d commit 1345113
Showing 6 changed files with 469 additions and 104 deletions.
103 changes: 102 additions & 1 deletion file.go
Original file line number Diff line number Diff line change
@@ -5,13 +5,35 @@ import (
"encoding/json"
"errors"
"io"
"mime"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
)

var initMime = func() func() {
var once sync.Once
return func() {
once.Do(func() {
_ = mime.AddExtensionType(".xml", "application/xml")
_ = mime.AddExtensionType(".jsonld", "application/ld+json")
_ = mime.AddExtensionType(".jsonnet", "application/jsonnet")
_ = mime.AddExtensionType(".yaml", "text/yaml")
_ = mime.AddExtensionType(".yml", "text/yaml")
})
}
}()

const contentTypeOctetStream = "application/octet-stream"

var nowFn = time.Now

// File represents a response that is a response body. The runner is in charge
// of getting the contents to the destination. The metadata will be received.
// of getting the contents to the destination. The metadata will be received. One
// note, we call NormalizeFile on the File's in the Runner's that execute the handler.
// Testing through the Run function will illustrate this.
type File struct {
ContentType string `json:"content_type"`
Encoding string `json:"encoding"`
@@ -25,6 +47,20 @@ func (f File) MarshalJSON() ([]byte, error) {
return json.Marshal(alias(f))
}

// NormalizeFile normalizes a file so that all fields are set with sane defaults.
func NormalizeFile(f File) File {
if f.ContentType == "" {
f.ContentType = normalizeContentType(f.Filename)
}
if f.Encoding == "" {
f.Encoding = normalizeEncoding(f.Filename)
}
if f.Filename == "" {
f.Filename = normalizeFilename(f.ContentType, f.Encoding, nowFn())
}
return f
}

// CompressGzip compresses a files contents with gzip compression.
func CompressGzip(file File) File {
switch {
@@ -99,3 +135,68 @@ func (c *compressorGzip) Close() error {

return errors.Join(errs...)
}

func normalizeContentType(filename string) string {
initMime()
parts := strings.Split(filepath.Base(filename), ".")
if len(parts) < 2 {
return contentTypeOctetStream
}
for _, ext := range parts[1:] {
if ct := mime.TypeByExtension("." + ext); ct != "" {
return ct
}
}
return contentTypeOctetStream
}

func normalizeEncoding(filename string) string {
parts := strings.SplitN(filepath.Base(filename), ".", 2)
if len(parts) == 1 {
return ""
}

mapping := map[string]string{
"br": "brotli",
"gz": "gzip",
"zst": "zstd",
}
var out []string
for _, part := range strings.Split(parts[1], ".") {
if encoding, ok := mapping[part]; ok {
out = append(out, encoding)
}
}
return strings.Join(out, ", ")
}

var compressionToExt = map[string]string{
"brotli": "br",
"gzip": "gz",
"zstd": "zst",
}

func normalizeFilename(contentType, encoding string, now time.Time) string {
filename := "upload_" + now.Format(time.RFC3339)
if encoding != "" {
var converted []string
for _, enc := range strings.Split(strings.ReplaceAll(encoding, " ", ""), ",") {
if ext := compressionToExt[enc]; ext != "" {
converted = append(converted, ext)
}
}
if len(converted) > 0 {
encoding = "." + strings.Join(converted, ".")
}
}

var ctExt string
if contentType != contentTypeOctetStream {
initMime()
exts, _ := mime.ExtensionsByType(contentType)
if len(exts) > 0 {
ctExt = exts[0]
}
}
return filename + ctExt + encoding
}
272 changes: 272 additions & 0 deletions file_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
package fdk

import (
"testing"
"time"
)

func TestNormalizeFile(t *testing.T) {
var start = time.Time{}.UTC().Add(time.Hour)

tests := []struct {
name string
nowFn func() time.Time
in File
want File
}{
{
name: "file with all values set should remain the same",
in: File{
ContentType: "application/json",
Encoding: "gzip",
Filename: "test.json",
},
want: File{
ContentType: "application/json",
Encoding: "gzip",
Filename: "test.json",
},
},
{
name: "file missing encoding should remain the same",
in: File{
ContentType: "application/json",
Filename: "test.json",
},
want: File{
ContentType: "application/json",
Filename: "test.json",
},
},
{
name: "file missing content type for filename with json extension should add content type to application/ld+json",
in: File{
Filename: "test.jsonld",
},
want: File{
ContentType: "application/ld+json",
Filename: "test.jsonld",
},
},
{
name: "file missing content type for filename with jsonld extension should add content type to application/json",
in: File{
Filename: "test.json",
},
want: File{
ContentType: "application/json",
Filename: "test.json",
},
},
{
name: "file missing content type for filename with jsonld extension should add content type to application/json",
in: File{
Filename: "test.js",
},
want: File{
ContentType: "text/javascript; charset=utf-8",
Filename: "test.js",
},
},
{
name: "file missing content type for filename with xml extension should add content type to application/xml",
in: File{
Filename: "test.xml",
},
want: File{
ContentType: "application/xml",
Filename: "test.xml",
},
},
{
name: "file missing content type for filename with tar extension should add content type to application/x-tar",
in: File{
Filename: "test.tar",
},
want: File{
ContentType: "application/x-tar",
Filename: "test.tar",
},
},
{
name: "file missing content type for filename with zip extension should add content type to application/zip",
in: File{
Filename: "test.zip",
},
want: File{
ContentType: "application/zip",
Filename: "test.zip",
},
},
{
name: "file missing content type for filename with html extension should add content type to text/html",
in: File{
Filename: "test.html",
},
want: File{
ContentType: "text/html; charset=utf-8",
Filename: "test.html",
},
},
{
name: "file missing content type for filename with yaml extension should add content type to text/yaml",
in: File{
Filename: "test.yaml",
},
want: File{
ContentType: "text/yaml; charset=utf-8",
Filename: "test.yaml",
},
},
{
name: "file missing content type for filename with yml extension should add content type to text/yaml",
in: File{
Filename: "test.yml",
},
want: File{
ContentType: "text/yaml; charset=utf-8",
Filename: "test.yml",
},
},
{
name: "file missing content type for filename with txt extension should add content type to text/plain",
in: File{
Filename: "test.txt",
},
want: File{
ContentType: "text/plain; charset=utf-8",
Filename: "test.txt",
},
},
{
name: "file missing content encoding for gzipped json filename should add content type and encoding",
in: File{
Filename: "test.json.gz",
},
want: File{
ContentType: "application/json",
Encoding: "gzip",
Filename: "test.json.gz",
},
},
{
name: "file missing content encoding for gzipped yaml filename should add content type and encoding",
in: File{
Filename: "test.yaml.gz",
},
want: File{
ContentType: "text/yaml; charset=utf-8",
Encoding: "gzip",
Filename: "test.yaml.gz",
},
},
{
name: "file missing content type and filename should set filename to timestamp",
nowFn: func() time.Time {
return start
},
in: File{},
want: File{
ContentType: "application/octet-stream",
Filename: "upload_" + start.Format(time.RFC3339),
},
},
{
name: "file missing filename with content type json set filename to timestamp.json file",
nowFn: func() time.Time {
return start
},
in: File{
ContentType: "application/json",
},
want: File{
ContentType: "application/json",
Filename: "upload_" + start.Format(time.RFC3339) + ".json",
},
},
{
name: "file missing filename with content type json and gz encoding set filename to timestamp.json.gz file",
nowFn: func() time.Time {
return start
},
in: File{
ContentType: "application/json",
Encoding: "gzip",
},
want: File{
ContentType: "application/json",
Encoding: "gzip",
Filename: "upload_" + start.Format(time.RFC3339) + ".json.gz",
},
},
{
name: "file missing filename with content type json and gz zstd encodings set filename to timestamp.json.gz file",
nowFn: func() time.Time {
return start
},
in: File{
ContentType: "application/json",
Encoding: "zstd, gzip",
},
want: File{
ContentType: "application/json",
Encoding: "zstd, gzip",
Filename: "upload_" + start.Format(time.RFC3339) + ".json.zst.gz",
},
},
{
name: "file missing filename with content type plain/javascript and gz zstd encodings set filename to timestamp.json.gz file",
nowFn: func() time.Time {
return start
},
in: File{
ContentType: "text/javascript; charset=utf-8",
Encoding: "zstd, gzip",
},
want: File{
ContentType: "text/javascript; charset=utf-8",
Encoding: "zstd, gzip",
Filename: "upload_" + start.Format(time.RFC3339) + ".js.zst.gz",
},
},
{
name: "file missing filename with content type xml and gz zstd encodings set filename to timestamp.json.gz file",
nowFn: func() time.Time {
return start
},
in: File{
ContentType: "application/xml",
Encoding: "zstd, gzip",
},
want: File{
ContentType: "application/xml",
Encoding: "zstd, gzip",
Filename: "upload_" + start.Format(time.RFC3339) + ".xml.zst.gz",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.nowFn != nil {
old := nowFn
nowFn = tt.nowFn
t.Cleanup(func() { nowFn = old })
}

got := NormalizeFile(tt.in)
EqualVals(t, tt.want.Filename, got.Filename)
EqualVals(t, tt.want.ContentType, got.ContentType)
EqualVals(t, tt.want.Encoding, got.Encoding)
})
}
}

func EqualVals[T comparable](t testing.TB, want, got T) bool {
t.Helper()

match := want == got
if !match {
t.Errorf("values not equal:\n\t\twant:\t%#v\n\t\tgot:\t%#v", want, got)
}
return match
}
Loading

0 comments on commit 1345113

Please sign in to comment.