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 }