feat(storage): support signed read options and local byte ranges
This commit is contained in:
72
storage/read.go
Normal file
72
storage/read.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const (
|
||||
DispositionAttachment = "attachment"
|
||||
DispositionInline = "inline"
|
||||
)
|
||||
|
||||
// ReadOptions controls the response to one signed GET. Empty Disposition means
|
||||
// attachment; empty ContentType preserves object metadata. FileName is a display
|
||||
// name, not an object key or filesystem path. All options are signed.
|
||||
type ReadOptions struct {
|
||||
Disposition string
|
||||
FileName string
|
||||
ContentType string
|
||||
}
|
||||
|
||||
// ReadLink grants GET access without additional request headers. ExpiresAt is
|
||||
// the expiry encoded in the signature, at second precision. Provider credentials
|
||||
// may expire earlier. Do not log the URL. Issuing another link does not revoke it.
|
||||
type ReadLink struct {
|
||||
URL string `json:"url"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
}
|
||||
|
||||
// ReadPresigner is optional; existing Store and Presigner implementations do not
|
||||
// need to change. Cloud GET signatures do not also authorize HEAD requests.
|
||||
type ReadPresigner interface {
|
||||
PresignGetWithOptions(context.Context, string, time.Duration, ReadOptions) (ReadLink, error)
|
||||
}
|
||||
|
||||
// ResponseHeaders validates options and returns a fresh set of response headers
|
||||
// for provider adapters to include BEFORE signing, requesting private, no-store.
|
||||
func (o ReadOptions) ResponseHeaders() (http.Header, error) {
|
||||
disposition := o.Disposition
|
||||
if disposition == "" {
|
||||
disposition = DispositionAttachment
|
||||
}
|
||||
if disposition != DispositionAttachment && disposition != DispositionInline {
|
||||
return nil, errors.New("storage: disposition must be inline or attachment")
|
||||
}
|
||||
if len(o.FileName) > 1024 || !utf8.ValidString(o.FileName) || strings.IndexFunc(o.FileName, unicode.IsControl) >= 0 || strings.ContainsAny(o.FileName, `/\`) || o.FileName == "." || o.FileName == ".." {
|
||||
return nil, errors.New("storage: invalid display filename")
|
||||
}
|
||||
params := map[string]string{}
|
||||
if o.FileName != "" {
|
||||
params["filename"] = o.FileName
|
||||
}
|
||||
h := http.Header{"Content-Disposition": {mime.FormatMediaType(disposition, params)}, "Cache-Control": {"private, no-store"}}
|
||||
if o.ContentType != "" {
|
||||
if !utf8.ValidString(o.ContentType) || strings.IndexFunc(o.ContentType, unicode.IsControl) >= 0 {
|
||||
return nil, ErrContentType
|
||||
}
|
||||
kind, err := MediaType(o.ContentType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, params, _ := mime.ParseMediaType(o.ContentType)
|
||||
h.Set("Content-Type", mime.FormatMediaType(kind, params))
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
Reference in New Issue
Block a user