diff --git a/changes/20251017171656.feature b/changes/20251017171656.feature new file mode 100644 index 0000000000..d38c6e6219 --- /dev/null +++ b/changes/20251017171656.feature @@ -0,0 +1 @@ +:sparkles: `[tus]` Add utilities for generating header values diff --git a/utils/http/headers/tus/tus.go b/utils/http/headers/tus/tus.go index d48cd5866b..7ea12d0082 100644 --- a/utils/http/headers/tus/tus.go +++ b/utils/http/headers/tus/tus.go @@ -2,6 +2,7 @@ package tus import ( "context" + "fmt" "net/url" "regexp" "strings" @@ -16,6 +17,19 @@ import ( const KeyTUSMetadata = "filename" +// GenerateTUSChecksumHeader generate the checksum header value +// See https://tus.io/protocols/resumable-upload#upload-checksum +func GenerateTUSChecksumHeader(hashAlgo, hash string) (header string, err error) { + hashAlgoC, err := hashing.DetermineHashingAlgorithmCanonicalReference(hashAlgo) + if err != nil { + err = commonerrors.WrapError(commonerrors.ErrUnsupported, err, "hashing algorithm is not supported") + return + } + base64Hash := base64.EncodeString(hash) + header = fmt.Sprintf("%v %v", strings.ToLower(hashAlgoC), base64Hash) + return +} + // ParseTUSHash parses the checksum header value and tries to determine the different elements it contains. // See https://tus.io/protocols/resumable-upload#upload-checksum func ParseTUSHash(checksum string) (hashAlgo, hash string, err error) { @@ -47,6 +61,17 @@ func ParseTUSHash(checksum string) (hashAlgo, hash string, err error) { return } +// GenerateTUSConcatFinalHeader generates the `Concat` header value https://tus.io/protocols/resumable-upload#upload-concat +func GenerateTUSConcatFinalHeader(partials []*url.URL) (header string, err error) { + header = fmt.Sprintf("final;%v", strings.Join(collection.Map[*url.URL, string](partials, func(u *url.URL) string { + if u == nil { + return "" + } + return u.EscapedPath() + }), " ")) + return +} + // ParseTUSConcatHeader parses the `Concat` header value https://tus.io/protocols/resumable-upload#upload-concat func ParseTUSConcatHeader(concat string) (isPartial bool, partials []*url.URL, err error) { header := strings.TrimSpace(concat) @@ -73,6 +98,24 @@ func ParseTUSConcatHeader(concat string) (isPartial bool, partials []*url.URL, e return } +// GenerateTUSMetadataHeader generates the `metadata` header value https://tus.io/protocols/resumable-upload#upload-metadata +func GenerateTUSMetadataHeader(filename *string, elements map[string]any) (header string, err error) { + newMap := make(map[string]string, len(elements)) + for key, value := range elements { + valueB, ok := value.(bool) + if ok && valueB { + newMap[key] = "" + } else { + newMap[key] = base64.EncodeString(fmt.Sprintf("%v", value)) + } + } + if !reflection.IsEmpty(filename) { + newMap[KeyTUSMetadata] = base64.EncodeString(field.OptionalString(filename, "")) + } + header = strings.Join(collection.ConvertMapToPairSlice(newMap, " "), ",") + return +} + // ParseTUSMetadataHeader parses the `metadata` header value https://tus.io/protocols/resumable-upload#upload-metadata func ParseTUSMetadataHeader(header string) (filename *string, elements map[string]any, err error) { h := strings.TrimSpace(header) diff --git a/utils/http/headers/tus/tus_test.go b/utils/http/headers/tus/tus_test.go index 7384077772..054e580303 100644 --- a/utils/http/headers/tus/tus_test.go +++ b/utils/http/headers/tus/tus_test.go @@ -77,6 +77,15 @@ func TestParseTUSHash(t *testing.T) { expectedAlgo: hashing.HashSha256, expectedChecksum: "this is a test value obviously", }, + { + header: func() string { + header, err := GenerateTUSChecksumHeader(hashing.HashSha256, "the cloudy crew: josh jennings, kem govender, bianca bunaciu, adrien cabarbaye, abdelrahman abdelraouf, phuong linh nguyen") + require.NoError(t, err) + return header + }(), + expectedAlgo: hashing.HashSha256, + expectedChecksum: "the cloudy crew: josh jennings, kem govender, bianca bunaciu, adrien cabarbaye, abdelrahman abdelraouf, phuong linh nguyen", + }, { header: "sha1-md5 Lve95gjOVATpfV8EL5X4nxwjKHE=", expectedAlgo: "sha1-md5", @@ -146,6 +155,31 @@ func TestParseTUSConcatHeader(t *testing.T) { "/y", }, }, + { + input: func() string { + header, err := GenerateTUSConcatFinalHeader( + []*url.URL{ + func() *url.URL { + u, err := url.Parse("/x") + require.NoError(t, err) + return u + }(), + func() *url.URL { + u, err := url.Parse("/ywh/h") + require.NoError(t, err) + return u + }(), + }, + ) + require.NoError(t, err) + return header + }(), + isPartial: false, + expectedPartialURL: []string{ + "/x", + "/ywh/h", + }, + }, { input: fmt.Sprintf(" final; %v %v ", url1, url2), isPartial: false, @@ -276,6 +310,25 @@ func TestParseTUSMetadataHeader(t *testing.T) { "empty": true, }, }, + { + input: func() string { + header, err := GenerateTUSMetadataHeader(field.ToOptionalString("test.txt"), map[string]any{ + "meta": "y", + "test": false, + "empty": true, + }, + ) + require.NoError(t, err) + return header + }(), + expectedFilename: field.ToOptionalString("test.txt"), + expectedElements: map[string]any{ + "filename": "test.txt", + "meta": "y", + "test": "false", + "empty": true, + }, + }, { input: "note " + toBase64Encoded("A/B+C=D=="), expectedElements: map[string]any{