Skip to content

Commit

Permalink
additions needed for file-maker feature in content-fabric (#57)
Browse files Browse the repository at this point in the history
* expose ginutil.HttpStatus() to convert error to status code, add structured.P helper
* add LocalMediaFile token, httputil.SetContentDisposition, etc.
  • Loading branch information
lukseven authored Dec 19, 2024
1 parent 8074b98 commit 328c8e2
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 24 deletions.
16 changes: 16 additions & 0 deletions format/structured/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,19 @@ func EscapeSeparators(path string, separator ...string) string {
}
return strings.NewReplacer("~", "~0", sep, "~1").Replace(path)
}

// P creates a new path from the given paths or path segments with the default '/' separator. Each of the provided
// strings is parsed as path and appended to the resulting path.
//
// Notes:
//
// - a string containing the separator is always split into multiple segments. If you want to preserve
// segments with separators, use NewPath().
// - empty strings are ignored: P("a", "", "b") => /a/b
func P(pathsOrSegments ...string) Path {
var p Path
for _, pathOrSeg := range pathsOrSegments {
p = p.append(ParsePath(pathOrSeg)...)
}
return p
}
38 changes: 38 additions & 0 deletions format/structured/path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,41 @@ func TestPathParsePaths(t *testing.T) {
})
}
}

func TestP(t *testing.T) {
tests := []struct {
paths []string
want Path
}{
{
paths: nil,
want: nil,
},
{
paths: []string{},
want: Path(nil),
},
{
paths: []string{"/"},
want: Path(nil),
},
{
paths: []string{"a", "b", "c"},
want: Path{"a", "b", "c"},
},
{
paths: []string{"", "a", "", "c", ""},
want: Path{"a", "c"}, // empty segments are ignored
},
{
paths: []string{"/", "/a/b", "c/d/e"},
want: Path{"a", "b", "c", "d", "e"},
},
}
for _, tt := range tests {
t.Run(jsonutil.MarshalCompactString(tt.paths), func(t *testing.T) {
res := P(tt.paths...)
require.Equal(t, tt.want, res)
})
}
}
33 changes: 29 additions & 4 deletions format/token/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,28 @@ func NewLRO(code Code, nid id.ID, bytes ...byte) (*Token, error) {
return res, nil
}

// NewLocalFile creates a new local file token. The bytes are arbitrary, but should be unique.
func NewLocalFile(nid id.ID, qid id.ID, bts []byte) (*Token, error) {
e := errors.Template("init local file token", errors.K.Invalid)
if nid.AssertCode(id.QNode) != nil {
return nil, e("reason", "invalid nid", "nid", nid)
}
if qid.AssertCode(id.Q) != nil {
return nil, e("reason", "invalid qid", "qid", qid)
}
if len(bts) == 0 {
return nil, e("reason", "byte slice empty")
}
res := &Token{
Code: LocalFile,
Bytes: bts,
QID: qid,
NID: nid,
}
res.MakeString()
return res, nil
}

// Code is the type of a Token
type Code uint8

Expand Down Expand Up @@ -136,6 +158,7 @@ const (
QPartWriteV1 // 1st version: random bytes
QPartWrite // 2nd version: scheme, flags, random bytes
LRO // node ID, random bytes
LocalFile // node ID, content ID, bytes = hash(offering + presentation + format + start + end)
)

const prefixLen = 4
Expand All @@ -148,6 +171,7 @@ var prefixToCode = map[string]Code{
"tqpw": QPartWriteV1,
"tqp_": QPartWrite, // QPartWrite new version
"tlro": LRO,
"tlf_": LocalFile,
}
var codeToName = map[Code]string{
UNKNOWN: "unknown",
Expand All @@ -156,6 +180,7 @@ var codeToName = map[Code]string{
QPartWriteV1: "content part write token v1",
QPartWrite: "content part write token",
LRO: "bitcode LRO handle",
LocalFile: "local file",
}

// NOTE: 5 char prefix - 2 underscores!
Expand Down Expand Up @@ -244,7 +269,7 @@ func (t *Token) MakeString() string {
case QWriteV1, QPartWriteV1:
b = make([]byte, len(t.Bytes))
copy(b, t.Bytes)
case QWrite, LRO:
case QWrite, LRO, LocalFile:
// prefix + base58(uvarint(len(QID) | QID |
// uvarint(len(NID) | NID |
// uvarint(len(RAND_BYTES) | RAND_BYTES)
Expand Down Expand Up @@ -353,10 +378,10 @@ func (t *Token) Describe() string {

add("type: " + t.Code.Describe())
add("bytes: 0x" + hex.EncodeToString(t.Bytes))
if t.Code == QWrite {
if t.Code == QWrite || t.Code == LocalFile {
add("qid: " + t.QID.String())
}
if t.Code == QWrite || t.Code == LRO {
if t.Code == QWrite || t.Code == LRO || t.Code == LocalFile {
add("nid: " + t.NID.String())
}
if t.Code == QPartWrite {
Expand Down Expand Up @@ -452,7 +477,7 @@ func Parse(s string) (*Token, error) {
switch code {
case QWriteV1, QPartWriteV1:
return &Token{Code: code, Bytes: dec, s: s}, nil
case QWrite, LRO:
case QWrite, LRO, LocalFile:
// prefix + base58(uvarint(len(QID) | QID |
// uvarint(len(NID) | NID |
// uvarint(len(RAND_BYTES) | RAND_BYTES)
Expand Down
44 changes: 41 additions & 3 deletions format/token/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,33 @@ func TestWrappedJSON(t *testing.T) {
assert.True(t, s.Token.Equal(unmarshalled.Token))
}

func ExampleToken_Describe_Object() {
func TestNewLocalFile(t *testing.T) {
validate := func(tok *token.Token) {
require.NoError(t, tok.AssertCode(token.LocalFile))
require.Equal(t, nid, tok.NID)
require.Equal(t, qid, tok.QID)
}

tok, err := token.NewLocalFile(nid, qid, []byte("some bytes"))
require.NoError(t, err)
fmt.Println(tok)
validate(tok)

tok2, err := token.Parse(tok.String())
require.NoError(t, err)
validate(tok2)

validateError := func(tok *token.Token, err error) {
require.Nil(t, tok)
require.Error(t, err)
}
validateError(token.NewLocalFile(nid, qid, nil))
validateError(token.NewLocalFile(nid, qid, []byte{}))
validateError(token.NewLocalFile(nil, qid, []byte{1}))
validateError(token.NewLocalFile(nid, nil, []byte{1}))
}

func ExampleToken_Describe_object() {
tok, _ := token.FromString("tq__3WhUFGKoJAzvqrDWiZtkcfQHiKp4Gda4KkiwuRgX6BTFfq7hNeji2hPDW6qZxLuk7xAju4bgm8iLwK")
fmt.Println(tok.Describe())

Expand All @@ -160,7 +186,7 @@ func ExampleToken_Describe_Object() {
// nid: inod2KRn6vRvn8U3gczhSMJwd1
}

func ExampleToken_Describe_Part() {
func ExampleToken_Describe_part() {
tok, _ := token.FromString("tqp_NHG92YAkoUg7dnCrWT8J3RLp6")
fmt.Println(tok.Describe())

Expand All @@ -172,7 +198,7 @@ func ExampleToken_Describe_Part() {
// flags: [preamble]
}

func ExampleToken_Describe_LRO() {
func ExampleToken_Describe_lro() {
tok, _ := token.FromString("tlro12hb4zikV2ArEoXXyUV6xKJPfC6Ff2siNKDKBVM6js8adif81")
fmt.Println(tok.Describe())

Expand All @@ -182,3 +208,15 @@ func ExampleToken_Describe_LRO() {
// bytes: 0x2df2a5d3d6c4e0830a95e7f1e8c779f6
// nid: inod2KRn6vRvn8U3gczhSMJwd1
}

func ExampleToken_Describe_localFile() {
tok, _ := token.FromString("tlf_HSQJP67VzgDtDSwhGoSTog7XxkePrBfLagrm8p7QWUqUPiuoj1gp5MvrxS3awRCZu6oMQdNZPUWxM8b9uan")
fmt.Println(tok.Describe())

// Output:
//
// type: local file
// bytes: 0x9cd9260a25a7013e0e9a48f7a83a5937
// qid: iq__99d4kp14eSDEP7HWfjU4W6qmqDw
// nid: inod3Sa5p3czRyYi8GnVGnh8gBDLaqJr
}
3 changes: 3 additions & 0 deletions format/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ type QPWriteToken = *token.Token
// LROHandle is a handle for long running bitcode operations
type LROHandle = *token.Token

// LocalMediaFile is a handle for local media file jobs
type LocalMediaFile = *token.Token

// Attributes is the type of content attributes
type Attributes struct{}

Expand Down
19 changes: 12 additions & 7 deletions util/ginutil/ginutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,21 @@ const loggerKey = "ginutil.LOGGER"
// of all goroutines. The logger (an instance of eluv-io/log-go) can be set in the gin context under the "LOGGER" key.
// If not set, the root logger will be used.
func Abort(c *gin.Context, err error) {
AbortWithStatus(c, abortCode(c, err), err)
code := HttpStatus(err)
if code == http.StatusInternalServerError {
dumpGoRoutines(c)
}
AbortWithStatus(c, code, err)
}

// AbortHead aborts the current HTTP HEAD request with the HTTP status code set according to the
// given error type.
func AbortHead(c *gin.Context, err error) {
AbortHeadWithStatus(c, abortCode(c, err))
AbortHeadWithStatus(c, HttpStatus(err))
}

func abortCode(c *gin.Context, err error) int {
// HttpStatus returns the HTTP status code for the given error. The status code is determined based on the error kind.
func HttpStatus(err error) int {
code := http.StatusInternalServerError
if e, ok := err.(*errors.Error); ok {
switch e.Kind() {
Expand All @@ -54,11 +59,11 @@ func abortCode(c *gin.Context, err error) int {
code = http.StatusNotAcceptable
case errors.K.Unavailable:
code = http.StatusServiceUnavailable
case errors.K.Other:
dumpGoRoutines(c)
case errors.K.NotImplemented:
code = http.StatusNotImplemented
case httputil.KindRangeNotSatisfiable:
code = http.StatusRequestedRangeNotSatisfiable
}
} else {
dumpGoRoutines(c)
}
return code
}
Expand Down
11 changes: 7 additions & 4 deletions util/ginutil/ginutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import (
"net/http/httptest"
"testing"

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"

"github.com/eluv-io/apexlog-go/handlers/memory"
"github.com/eluv-io/common-go/util/httputil"
"github.com/eluv-io/common-go/util/jsonutil"
"github.com/eluv-io/errors-go"
"github.com/eluv-io/log-go"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)

func init() {
Expand All @@ -39,12 +41,13 @@ func TestAbort(t *testing.T) {
{errors.E("op", errors.K.IO), 500},
{errors.E("op", errors.K.AVInput), 500},
{errors.E("op", errors.K.AVProcessing), 500},
{errors.E("op", errors.K.NotImplemented), 500},
{errors.E("op", errors.K.NotImplemented), 501},
{errors.E("op", errors.K.Unavailable), 503},
{errors.E("op", httputil.KindRangeNotSatisfiable), 416},
}

for _, tt := range tests {
t.Run(fmt.Sprint(tt.err), func(t *testing.T) {
t.Run(fmt.Sprint(errors.Field(tt.err, "kind")), func(t *testing.T) {
w, c := testCtx(t)

Abort(c, tt.err)
Expand Down
11 changes: 6 additions & 5 deletions util/httputil/content_range.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"github.com/eluv-io/errors-go"
)

const KindRangeNotSatisfiable = errors.Kind("range not satisfiable")

type ContentRange struct {
Off, Len, TotalLen int64
AdaptedOff, AdaptedLen int64
Expand Down Expand Up @@ -56,27 +58,26 @@ func (c *ContentRange) TotalSize() int64 {
// [Byte Range]: https://tools.ietf.org/html/rfc7233#section-2.1
// [RFC 7233, section 4]: https://tools.ietf.org/html/rfc7233#section-4
func AdaptRange(off, len, totalLen int64) (*ContentRange, error) {
e := errors.Template("adapt-byte-range", KindRangeNotSatisfiable, "offset", off, "length", len, "total_length", totalLen)
var err error = nil
realOff := off
realLen := len
if off < 0 && len < 0 {
err = errors.E("adapt-byte-range", errors.K.Invalid, "reason", "negative offset and length")
err = e("reason", "negative offset and length")
} else if off < 0 {
realOff = totalLen - len
} else if len < 0 {
realLen = totalLen - off
if realLen < 0 {
err = errors.E("adapt-byte-range", errors.K.Invalid, "reason", "offset larger than total length",
"offset", off, "length", len, "total_length", totalLen)
err = e("reason", "offset larger than total length")
}
}
if err == nil {
if realOff+realLen > totalLen {
realLen = totalLen - realOff
}
if realOff < 0 || (realOff > totalLen && realOff > 0) {
err = errors.E("adapt-byte-range", errors.K.Invalid, "reason", "invalid offset result",
"offset", off, "length", len, "total_length", totalLen)
err = e("reason", "invalid offset result")
}
}
if err != nil {
Expand Down
14 changes: 14 additions & 0 deletions util/httputil/httputil.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
cbor "github.com/multiformats/go-multicodec/cbor"
mcjson "github.com/multiformats/go-multicodec/json"
mux "github.com/multiformats/go-multicodec/mux"
"golang.org/x/text/encoding/charmap"

"github.com/eluv-io/common-go/format/id"
eioutil "github.com/eluv-io/common-go/util/ioutil"
Expand Down Expand Up @@ -569,3 +570,16 @@ func ParseServerError(body io.ReadCloser, httpStatusCode int) error {
}
return e(list.ErrorOrNil())
}

// SetContentDisposition sets the Content-Disposition header as a filename attachment.
//
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
// See https://datatracker.ietf.org/doc/html/rfc5987
func SetContentDisposition(header http.Header, filename string) {
if isoName, err := charmap.ISO8859_1.NewEncoder().String(filename); err == nil {
header.Add("Content-Disposition", "attachment; filename=\""+isoName+"\"")
} else {
// we could always add this variant - multiple Content-Disposition headers are allowed
header.Add("Content-Disposition", "attachment; filename*=UTF-8''"+url.PathEscape(filename)+"")
}
}
Loading

0 comments on commit 328c8e2

Please sign in to comment.