frostfs-http-gw/downloader/download.go

277 lines
7.4 KiB
Go
Raw Normal View History

package downloader
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"path"
"strconv"
2020-11-09 13:43:23 +00:00
"strings"
"sync"
"time"
"github.com/nspcc-dev/neofs-api-go/pkg/container"
2020-11-09 13:43:23 +00:00
"github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-http-gw/neofs"
"github.com/nspcc-dev/neofs-http-gw/tokens"
"github.com/valyala/fasthttp"
"go.uber.org/zap"
2020-04-22 10:34:48 +00:00
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var (
getOptionsPool = sync.Pool{
New: func() interface{} {
return new(neofs.GetOptions)
},
}
searchOptionsPool = sync.Pool{
New: func() interface{} {
return new(neofs.SearchOptions)
},
}
)
type (
detector struct {
io.Writer
sync.Once
contentType string
}
request struct {
*fasthttp.RequestCtx
log *zap.Logger
objectClient neofs.ObjectClient
}
objectIDs []*object.ID
)
2020-11-09 13:43:23 +00:00
func newDetector(w io.Writer) *detector {
return &detector{Writer: w}
}
func (d *detector) Write(data []byte) (int, error) {
d.Once.Do(func() {
d.contentType = http.DetectContentType(data)
})
return d.Writer.Write(data)
}
func isValidToken(s string) bool {
for _, c := range s {
if c <= ' ' || c > 127 {
return false
}
if strings.ContainsRune("()<>@,;:\\\"/[]?={}", c) {
return false
}
}
return true
}
func isValidValue(s string) bool {
for _, c := range s {
// HTTP specification allows for more technically, but we don't want to escape things.
if c < ' ' || c > 127 || c == '"' {
return false
}
}
return true
}
func (r *request) receiveFile(options *neofs.GetOptions) {
var (
err error
dis = "inline"
start = time.Now()
2020-11-09 13:43:23 +00:00
filename string
)
if err = tokens.StoreBearerToken(r.RequestCtx); err != nil {
r.log.Error("could not fetch and store bearer token", zap.Error(err))
r.Error("could not fetch and store bearer token", fasthttp.StatusBadRequest)
return
}
writer := newDetector(r.Response.BodyWriter())
options.Writer = writer
obj, err := r.objectClient.Get(r.RequestCtx, options)
2020-11-09 13:43:23 +00:00
if err != nil {
r.log.Error(
"could not receive object",
zap.Stringer("elapsed", time.Since(start)),
zap.Error(err),
)
2020-04-22 10:34:48 +00:00
var (
msg = fmt.Sprintf("could not receive object: %v", err)
code = fasthttp.StatusBadRequest
cause = err
2020-04-22 10:34:48 +00:00
)
for unwrap := errors.Unwrap(err); unwrap != nil; unwrap = errors.Unwrap(cause) {
cause = unwrap
}
if st, ok := status.FromError(cause); ok && st != nil {
2020-04-22 10:34:48 +00:00
if st.Code() == codes.NotFound {
code = fasthttp.StatusNotFound
}
msg = st.Message()
}
r.Error(msg, code)
return
}
if r.Request.URI().QueryArgs().GetBool("download") {
dis = "attachment"
2020-11-09 13:43:23 +00:00
}
r.Response.Header.Set("Content-Length", strconv.FormatUint(obj.PayloadSize(), 10))
for _, attr := range obj.Attributes() {
key := attr.Key()
val := attr.Value()
if !isValidToken(key) || !isValidValue(val) {
continue
}
r.Response.Header.Set("X-Attribute-"+key, val)
switch key {
case object.AttributeFileName:
2020-11-09 13:43:23 +00:00
filename = val
case object.AttributeTimestamp:
value, err := strconv.ParseInt(val, 10, 64)
if err != nil {
r.log.Info("couldn't parse creation date",
zap.String("key", key),
zap.String("val", val),
zap.Error(err))
continue
}
r.Response.Header.Set("Last-Modified",
time.Unix(value, 0).Format(time.RFC1123))
}
2020-11-09 13:43:23 +00:00
}
r.Response.Header.Set("x-object-id", obj.ID().String())
r.Response.Header.Set("x-owner-id", obj.OwnerID().String())
r.Response.Header.Set("x-container-id", obj.ContainerID().String())
r.SetContentType(writer.contentType)
r.Response.Header.Set("Content-Disposition", dis+"; filename="+path.Base(filename))
}
func (o objectIDs) Slice() []string {
res := make([]string, 0, len(o))
for _, oid := range o {
res = append(res, oid.String())
}
return res
}
// Downloader is a download request handler.
type Downloader struct {
log *zap.Logger
plant neofs.ClientPlant
}
// New creates an instance of Downloader using specified options.
func New(ctx context.Context, log *zap.Logger, plant neofs.ClientPlant) (*Downloader, error) {
var err error
d := &Downloader{log: log, plant: plant}
if err != nil {
return nil, fmt.Errorf("failed to get neofs client's reusable artifacts: %w", err)
}
return d, nil
}
func (d *Downloader) newRequest(ctx *fasthttp.RequestCtx, log *zap.Logger) *request {
return &request{
RequestCtx: ctx,
log: log,
objectClient: d.plant.Object(),
}
}
// DownloadByAddress handles download requests using simple cid/oid format.
func (d *Downloader) DownloadByAddress(c *fasthttp.RequestCtx) {
var (
err error
address = object.NewAddress()
cid, _ = c.UserValue("cid").(string)
oid, _ = c.UserValue("oid").(string)
val = strings.Join([]string{cid, oid}, "/")
log = d.log.With(zap.String("cid", cid), zap.String("oid", oid))
)
if err = address.Parse(val); err != nil {
log.Error("wrong object address", zap.Error(err))
c.Error("wrong object address", fasthttp.StatusBadRequest)
return
}
getOpts := getOptionsPool.Get().(*neofs.GetOptions)
defer getOptionsPool.Put(getOpts)
getOpts.Client, getOpts.SessionToken, err = d.plant.ConnectionArtifacts()
if err != nil {
log.Error("failed to get neofs connection artifacts", zap.Error(err))
c.Error("failed to get neofs connection artifacts", fasthttp.StatusInternalServerError)
return
}
getOpts.ObjectAddress = address
getOpts.Writer = nil
d.newRequest(c, log).receiveFile(getOpts)
}
// DownloadByAttribute handles attribute-based download requests.
func (d *Downloader) DownloadByAttribute(c *fasthttp.RequestCtx) {
var (
err error
scid, _ = c.UserValue("cid").(string)
key, _ = c.UserValue("attr_key").(string)
val, _ = c.UserValue("attr_val").(string)
log = d.log.With(zap.String("cid", scid), zap.String("attr_key", key), zap.String("attr_val", val))
)
cid := container.NewID()
if err = cid.Parse(scid); err != nil {
log.Error("wrong container id", zap.Error(err))
c.Error("wrong container id", fasthttp.StatusBadRequest)
return
}
searchOpts := searchOptionsPool.Get().(*neofs.SearchOptions)
defer searchOptionsPool.Put(searchOpts)
searchOpts.Client, searchOpts.SessionToken, err = d.plant.ConnectionArtifacts()
if err != nil {
log.Error("failed to get neofs connection artifacts", zap.Error(err))
c.Error("failed to get neofs connection artifacts", fasthttp.StatusInternalServerError)
return
}
searchOpts.BearerToken = nil
searchOpts.ContainerID = cid
searchOpts.Attribute.Key = key
searchOpts.Attribute.Value = val
var ids []*object.ID
if ids, err = d.plant.Object().Search(c, searchOpts); err != nil {
log.Error("something went wrong", zap.Error(err))
c.Error("something went wrong", fasthttp.StatusBadRequest)
return
} else if len(ids) == 0 {
log.Debug("object not found")
c.Error("object not found", fasthttp.StatusNotFound)
return
}
if len(ids) > 1 {
log.Debug("found multiple objects",
zap.Strings("object_ids", objectIDs(ids).Slice()),
zap.Stringer("show_object_id", ids[0]))
}
address := object.NewAddress()
address.SetContainerID(cid)
address.SetObjectID(ids[0])
getOpts := getOptionsPool.Get().(*neofs.GetOptions)
defer getOptionsPool.Put(getOpts)
getOpts.Client, getOpts.SessionToken, err = d.plant.ConnectionArtifacts()
if err != nil {
log.Error("failed to get neofs connection artifacts", zap.Error(err))
c.Error("failed to get neofs connection artifacts", fasthttp.StatusInternalServerError)
return
}
getOpts.ObjectAddress = address
getOpts.Writer = nil
d.newRequest(c, log).receiveFile(getOpts)
}