github.com/ethersphere/bee/v2@v2.2.0/pkg/api/bzz.go (about)

     1  // Copyright 2020 The Swarm Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package api
     6  
     7  import (
     8  	"context"
     9  	"encoding/hex"
    10  	"errors"
    11  	"fmt"
    12  	"net/http"
    13  	"path"
    14  	"path/filepath"
    15  	"strconv"
    16  	"strings"
    17  	"time"
    18  
    19  	"github.com/opentracing/opentracing-go"
    20  	"github.com/opentracing/opentracing-go/ext"
    21  	olog "github.com/opentracing/opentracing-go/log"
    22  
    23  	"github.com/ethereum/go-ethereum/common"
    24  	"github.com/ethersphere/bee/v2/pkg/accesscontrol"
    25  	"github.com/ethersphere/bee/v2/pkg/feeds"
    26  	"github.com/ethersphere/bee/v2/pkg/file/joiner"
    27  	"github.com/ethersphere/bee/v2/pkg/file/loadsave"
    28  	"github.com/ethersphere/bee/v2/pkg/file/redundancy"
    29  	"github.com/ethersphere/bee/v2/pkg/file/redundancy/getter"
    30  	"github.com/ethersphere/bee/v2/pkg/jsonhttp"
    31  	"github.com/ethersphere/bee/v2/pkg/log"
    32  	"github.com/ethersphere/bee/v2/pkg/manifest"
    33  	"github.com/ethersphere/bee/v2/pkg/postage"
    34  	"github.com/ethersphere/bee/v2/pkg/storage"
    35  	"github.com/ethersphere/bee/v2/pkg/storer"
    36  	"github.com/ethersphere/bee/v2/pkg/swarm"
    37  	"github.com/ethersphere/bee/v2/pkg/topology"
    38  	"github.com/ethersphere/bee/v2/pkg/tracing"
    39  	"github.com/ethersphere/langos"
    40  	"github.com/gorilla/mux"
    41  )
    42  
    43  // The size of buffer used for prefetching content with Langos when not using erasure coding
    44  // Warning: This value influences the number of chunk requests and chunker join goroutines
    45  // per file request.
    46  // Recommended value is 8 or 16 times the io.Copy default buffer value which is 32kB, depending
    47  // on the file size. Use lookaheadBufferSize() to get the correct buffer size for the request.
    48  const (
    49  	smallFileBufferSize = 8 * 32 * 1024
    50  	largeFileBufferSize = 16 * 32 * 1024
    51  
    52  	largeBufferFilesizeThreshold = 10 * 1000000 // ten megs
    53  )
    54  
    55  func lookaheadBufferSize(size int64) int {
    56  	if size <= largeBufferFilesizeThreshold {
    57  		return smallFileBufferSize
    58  	}
    59  	return largeFileBufferSize
    60  }
    61  
    62  func (s *Service) bzzUploadHandler(w http.ResponseWriter, r *http.Request) {
    63  	span, logger, ctx := s.tracer.StartSpanFromContext(r.Context(), "post_bzz", s.logger.WithName("post_bzz").Build())
    64  	defer span.Finish()
    65  
    66  	headers := struct {
    67  		ContentType    string           `map:"Content-Type,mimeMediaType" validate:"required"`
    68  		BatchID        []byte           `map:"Swarm-Postage-Batch-Id" validate:"required"`
    69  		SwarmTag       uint64           `map:"Swarm-Tag"`
    70  		Pin            bool             `map:"Swarm-Pin"`
    71  		Deferred       *bool            `map:"Swarm-Deferred-Upload"`
    72  		Encrypt        bool             `map:"Swarm-Encrypt"`
    73  		IsDir          bool             `map:"Swarm-Collection"`
    74  		RLevel         redundancy.Level `map:"Swarm-Redundancy-Level"`
    75  		Act            bool             `map:"Swarm-Act"`
    76  		HistoryAddress swarm.Address    `map:"Swarm-Act-History-Address"`
    77  	}{}
    78  	if response := s.mapStructure(r.Header, &headers); response != nil {
    79  		response("invalid header params", logger, w)
    80  		return
    81  	}
    82  
    83  	var (
    84  		tag      uint64
    85  		err      error
    86  		deferred = defaultUploadMethod(headers.Deferred)
    87  	)
    88  
    89  	ctx = redundancy.SetLevelInContext(ctx, headers.RLevel)
    90  
    91  	if deferred || headers.Pin {
    92  		tag, err = s.getOrCreateSessionID(headers.SwarmTag)
    93  		if err != nil {
    94  			logger.Debug("get or create tag failed", "error", err)
    95  			logger.Error(nil, "get or create tag failed")
    96  			switch {
    97  			case errors.Is(err, storage.ErrNotFound):
    98  				jsonhttp.NotFound(w, "tag not found")
    99  			default:
   100  				jsonhttp.InternalServerError(w, "cannot get or create tag")
   101  			}
   102  			ext.LogError(span, err, olog.String("action", "tag.create"))
   103  			return
   104  		}
   105  		span.SetTag("tagID", tag)
   106  	}
   107  
   108  	putter, err := s.newStamperPutter(ctx, putterOptions{
   109  		BatchID:  headers.BatchID,
   110  		TagID:    tag,
   111  		Pin:      headers.Pin,
   112  		Deferred: deferred,
   113  	})
   114  	if err != nil {
   115  		logger.Debug("putter failed", "error", err)
   116  		logger.Error(nil, "putter failed")
   117  		switch {
   118  		case errors.Is(err, errBatchUnusable) || errors.Is(err, postage.ErrNotUsable):
   119  			jsonhttp.UnprocessableEntity(w, "batch not usable yet or does not exist")
   120  		case errors.Is(err, postage.ErrNotFound):
   121  			jsonhttp.NotFound(w, "batch with id not found")
   122  		case errors.Is(err, errInvalidPostageBatch):
   123  			jsonhttp.BadRequest(w, "invalid batch id")
   124  		case errors.Is(err, errUnsupportedDevNodeOperation):
   125  			jsonhttp.BadRequest(w, errUnsupportedDevNodeOperation)
   126  		default:
   127  			jsonhttp.BadRequest(w, nil)
   128  		}
   129  		ext.LogError(span, err, olog.String("action", "new.StamperPutter"))
   130  		return
   131  	}
   132  
   133  	ow := &cleanupOnErrWriter{
   134  		ResponseWriter: w,
   135  		onErr:          putter.Cleanup,
   136  		logger:         logger,
   137  	}
   138  
   139  	if headers.IsDir || headers.ContentType == multiPartFormData {
   140  		s.dirUploadHandler(ctx, logger, span, ow, r, putter, r.Header.Get(ContentTypeHeader), headers.Encrypt, tag, headers.RLevel, headers.Act, headers.HistoryAddress)
   141  		return
   142  	}
   143  	s.fileUploadHandler(ctx, logger, span, ow, r, putter, headers.Encrypt, tag, headers.RLevel, headers.Act, headers.HistoryAddress)
   144  }
   145  
   146  // fileUploadResponse is returned when an HTTP request to upload a file is successful
   147  type bzzUploadResponse struct {
   148  	Reference swarm.Address `json:"reference"`
   149  }
   150  
   151  // fileUploadHandler uploads the file and its metadata supplied in the file body and
   152  // the headers
   153  func (s *Service) fileUploadHandler(
   154  	ctx context.Context,
   155  	logger log.Logger,
   156  	span opentracing.Span,
   157  	w http.ResponseWriter,
   158  	r *http.Request,
   159  	putter storer.PutterSession,
   160  	encrypt bool,
   161  	tagID uint64,
   162  	rLevel redundancy.Level,
   163  	act bool,
   164  	historyAddress swarm.Address,
   165  ) {
   166  	queries := struct {
   167  		FileName string `map:"name" validate:"startsnotwith=/"`
   168  	}{}
   169  	if response := s.mapStructure(r.URL.Query(), &queries); response != nil {
   170  		response("invalid query params", logger, w)
   171  		return
   172  	}
   173  
   174  	p := requestPipelineFn(putter, encrypt, rLevel)
   175  
   176  	// first store the file and get its reference
   177  	fr, err := p(ctx, r.Body)
   178  	if err != nil {
   179  		logger.Debug("file store failed", "file_name", queries.FileName, "error", err)
   180  		logger.Error(nil, "file store failed", "file_name", queries.FileName)
   181  		switch {
   182  		case errors.Is(err, postage.ErrBucketFull):
   183  			jsonhttp.PaymentRequired(w, "batch is overissued")
   184  		default:
   185  			jsonhttp.InternalServerError(w, errFileStore)
   186  		}
   187  		ext.LogError(span, err, olog.String("action", "file.store"))
   188  		return
   189  	}
   190  
   191  	// If filename is still empty, use the file hash as the filename
   192  	if queries.FileName == "" {
   193  		queries.FileName = fr.String()
   194  		if err := s.validate.Struct(queries); err != nil {
   195  			verr := &validationError{
   196  				Entry: "file hash",
   197  				Value: queries.FileName,
   198  				Cause: err,
   199  			}
   200  			logger.Debug("invalid body filename", "error", verr)
   201  			logger.Error(nil, "invalid body filename")
   202  			jsonhttp.BadRequest(w, jsonhttp.StatusResponse{
   203  				Message: "invalid body params",
   204  				Code:    http.StatusBadRequest,
   205  				Reasons: []jsonhttp.Reason{{
   206  					Field: "file hash",
   207  					Error: verr.Error(),
   208  				}},
   209  			})
   210  			return
   211  		}
   212  	}
   213  
   214  	factory := requestPipelineFactory(ctx, putter, encrypt, rLevel)
   215  	l := loadsave.New(s.storer.ChunkStore(), s.storer.Cache(), factory)
   216  
   217  	m, err := manifest.NewDefaultManifest(l, encrypt)
   218  	if err != nil {
   219  		logger.Debug("create manifest failed", "file_name", queries.FileName, "error", err)
   220  		logger.Error(nil, "create manifest failed", "file_name", queries.FileName)
   221  		switch {
   222  		case errors.Is(err, manifest.ErrInvalidManifestType):
   223  			jsonhttp.BadRequest(w, "create manifest failed")
   224  		default:
   225  			jsonhttp.InternalServerError(w, nil)
   226  		}
   227  		return
   228  	}
   229  
   230  	rootMetadata := map[string]string{
   231  		manifest.WebsiteIndexDocumentSuffixKey: queries.FileName,
   232  	}
   233  	err = m.Add(ctx, manifest.RootPath, manifest.NewEntry(swarm.ZeroAddress, rootMetadata))
   234  	if err != nil {
   235  		logger.Debug("adding metadata to manifest failed", "file_name", queries.FileName, "error", err)
   236  		logger.Error(nil, "adding metadata to manifest failed", "file_name", queries.FileName)
   237  		jsonhttp.InternalServerError(w, "add metadata failed")
   238  		return
   239  	}
   240  
   241  	fileMtdt := map[string]string{
   242  		manifest.EntryMetadataContentTypeKey: r.Header.Get(ContentTypeHeader), // Content-Type has already been validated.
   243  		manifest.EntryMetadataFilenameKey:    queries.FileName,
   244  	}
   245  
   246  	err = m.Add(ctx, queries.FileName, manifest.NewEntry(fr, fileMtdt))
   247  	if err != nil {
   248  		logger.Debug("adding file to manifest failed", "file_name", queries.FileName, "error", err)
   249  		logger.Error(nil, "adding file to manifest failed", "file_name", queries.FileName)
   250  		jsonhttp.InternalServerError(w, "add file failed")
   251  		return
   252  	}
   253  
   254  	logger.Debug("info", "encrypt", encrypt, "file_name", queries.FileName, "hash", fr, "metadata", fileMtdt)
   255  
   256  	manifestReference, err := m.Store(ctx)
   257  	if err != nil {
   258  		logger.Debug("manifest store failed", "file_name", queries.FileName, "error", err)
   259  		logger.Error(nil, "manifest store failed", "file_name", queries.FileName)
   260  		switch {
   261  		case errors.Is(err, postage.ErrBucketFull):
   262  			jsonhttp.PaymentRequired(w, "batch is overissued")
   263  		default:
   264  			jsonhttp.InternalServerError(w, "manifest store failed")
   265  		}
   266  		return
   267  	}
   268  	logger.Debug("store", "manifest_reference", manifestReference)
   269  
   270  	reference := manifestReference
   271  	if act {
   272  		reference, err = s.actEncryptionHandler(r.Context(), w, putter, reference, historyAddress)
   273  		if err != nil {
   274  			logger.Debug("access control upload failed", "error", err)
   275  			logger.Error(nil, "access control upload failed")
   276  			switch {
   277  			case errors.Is(err, accesscontrol.ErrNotFound):
   278  				jsonhttp.NotFound(w, "act or history entry not found")
   279  			case errors.Is(err, accesscontrol.ErrInvalidPublicKey) || errors.Is(err, accesscontrol.ErrSecretKeyInfinity):
   280  				jsonhttp.BadRequest(w, "invalid public key")
   281  			case errors.Is(err, accesscontrol.ErrUnexpectedType):
   282  				jsonhttp.BadRequest(w, "failed to create history")
   283  			default:
   284  				jsonhttp.InternalServerError(w, errActUpload)
   285  			}
   286  			return
   287  		}
   288  	}
   289  
   290  	err = putter.Done(manifestReference)
   291  	if err != nil {
   292  		logger.Debug("done split failed", "reference", manifestReference, "error", err)
   293  		logger.Error(nil, "done split failed")
   294  		jsonhttp.InternalServerError(w, "done split failed")
   295  		ext.LogError(span, err, olog.String("action", "putter.Done"))
   296  		return
   297  	}
   298  	span.LogFields(olog.Bool("success", true))
   299  	span.SetTag("root_address", reference)
   300  
   301  	if tagID != 0 {
   302  		w.Header().Set(SwarmTagHeader, fmt.Sprint(tagID))
   303  		span.SetTag("tagID", tagID)
   304  	}
   305  	w.Header().Set(ETagHeader, fmt.Sprintf("%q", reference.String()))
   306  	w.Header().Set("Access-Control-Expose-Headers", SwarmTagHeader)
   307  
   308  	jsonhttp.Created(w, bzzUploadResponse{
   309  		Reference: reference,
   310  	})
   311  }
   312  
   313  func (s *Service) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) {
   314  	logger := tracing.NewLoggerWithTraceID(r.Context(), s.logger.WithName("get_bzz_by_path").Build())
   315  
   316  	paths := struct {
   317  		Address swarm.Address `map:"address,resolve" validate:"required"`
   318  		Path    string        `map:"path"`
   319  	}{}
   320  	if response := s.mapStructure(mux.Vars(r), &paths); response != nil {
   321  		response("invalid path params", logger, w)
   322  		return
   323  	}
   324  
   325  	address := paths.Address
   326  	if v := getAddressFromContext(r.Context()); !v.IsZero() {
   327  		address = v
   328  	}
   329  
   330  	if strings.HasSuffix(paths.Path, "/") {
   331  		paths.Path = strings.TrimRight(paths.Path, "/") + "/" // NOTE: leave one slash if there was some.
   332  	}
   333  
   334  	s.serveReference(logger, address, paths.Path, w, r, false)
   335  }
   336  
   337  func (s *Service) bzzHeadHandler(w http.ResponseWriter, r *http.Request) {
   338  	logger := tracing.NewLoggerWithTraceID(r.Context(), s.logger.WithName("head_bzz_by_path").Build())
   339  
   340  	paths := struct {
   341  		Address swarm.Address `map:"address,resolve" validate:"required"`
   342  		Path    string        `map:"path"`
   343  	}{}
   344  	if response := s.mapStructure(mux.Vars(r), &paths); response != nil {
   345  		response("invalid path params", logger, w)
   346  		return
   347  	}
   348  
   349  	address := paths.Address
   350  	if v := getAddressFromContext(r.Context()); !v.IsZero() {
   351  		address = v
   352  	}
   353  
   354  	if strings.HasSuffix(paths.Path, "/") {
   355  		paths.Path = strings.TrimRight(paths.Path, "/") + "/" // NOTE: leave one slash if there was some.
   356  	}
   357  
   358  	s.serveReference(logger, address, paths.Path, w, r, true)
   359  }
   360  
   361  func (s *Service) serveReference(logger log.Logger, address swarm.Address, pathVar string, w http.ResponseWriter, r *http.Request, headerOnly bool) {
   362  	loggerV1 := logger.V(1).Build()
   363  
   364  	headers := struct {
   365  		Cache                 *bool            `map:"Swarm-Cache"`
   366  		Strategy              *getter.Strategy `map:"Swarm-Redundancy-Strategy"`
   367  		FallbackMode          *bool            `map:"Swarm-Redundancy-Fallback-Mode"`
   368  		ChunkRetrievalTimeout *string          `map:"Swarm-Chunk-Retrieval-Timeout"`
   369  	}{}
   370  
   371  	if response := s.mapStructure(r.Header, &headers); response != nil {
   372  		response("invalid header params", logger, w)
   373  		return
   374  	}
   375  	cache := true
   376  	if headers.Cache != nil {
   377  		cache = *headers.Cache
   378  	}
   379  
   380  	ls := loadsave.NewReadonly(s.storer.Download(cache))
   381  	feedDereferenced := false
   382  
   383  	ctx := r.Context()
   384  	ctx, err := getter.SetConfigInContext(ctx, headers.Strategy, headers.FallbackMode, headers.ChunkRetrievalTimeout, logger)
   385  	if err != nil {
   386  		logger.Error(err, err.Error())
   387  		jsonhttp.BadRequest(w, "could not parse headers")
   388  		return
   389  	}
   390  
   391  FETCH:
   392  	// read manifest entry
   393  	m, err := manifest.NewDefaultManifestReference(
   394  		address,
   395  		ls,
   396  	)
   397  	if err != nil {
   398  		logger.Debug("bzz download: not manifest", "address", address, "error", err)
   399  		logger.Error(nil, "not manifest")
   400  		jsonhttp.NotFound(w, nil)
   401  		return
   402  	}
   403  
   404  	// there's a possible ambiguity here, right now the data which was
   405  	// read can be an entry.Entry or a mantaray feed manifest. Try to
   406  	// unmarshal as mantaray first and possibly resolve the feed, otherwise
   407  	// go on normally.
   408  	if !feedDereferenced {
   409  		if l, err := s.manifestFeed(ctx, m); err == nil {
   410  			//we have a feed manifest here
   411  			ch, cur, _, err := l.At(ctx, time.Now().Unix(), 0)
   412  			if err != nil {
   413  				logger.Debug("bzz download: feed lookup failed", "error", err)
   414  				logger.Error(nil, "bzz download: feed lookup failed")
   415  				jsonhttp.NotFound(w, "feed not found")
   416  				return
   417  			}
   418  			if ch == nil {
   419  				logger.Debug("bzz download: feed lookup: no updates")
   420  				logger.Error(nil, "bzz download: feed lookup")
   421  				jsonhttp.NotFound(w, "no update found")
   422  				return
   423  			}
   424  			ref, _, err := parseFeedUpdate(ch)
   425  			if err != nil {
   426  				logger.Debug("bzz download: mapStructure feed update failed", "error", err)
   427  				logger.Error(nil, "bzz download: mapStructure feed update failed")
   428  				jsonhttp.InternalServerError(w, "mapStructure feed update")
   429  				return
   430  			}
   431  			address = ref
   432  			feedDereferenced = true
   433  			curBytes, err := cur.MarshalBinary()
   434  			if err != nil {
   435  				s.logger.Debug("bzz download: marshal feed index failed", "error", err)
   436  				s.logger.Error(nil, "bzz download: marshal index failed")
   437  				jsonhttp.InternalServerError(w, "marshal index")
   438  				return
   439  			}
   440  
   441  			w.Header().Set(SwarmFeedIndexHeader, hex.EncodeToString(curBytes))
   442  			// this header might be overriding others. handle with care. in the future
   443  			// we should implement an append functionality for this specific header,
   444  			// since different parts of handlers might be overriding others' values
   445  			// resulting in inconsistent headers in the response.
   446  			w.Header().Set("Access-Control-Expose-Headers", SwarmFeedIndexHeader)
   447  			goto FETCH
   448  		}
   449  	}
   450  
   451  	if pathVar == "" {
   452  		loggerV1.Debug("bzz download: handle empty path", "address", address)
   453  
   454  		if indexDocumentSuffixKey, ok := manifestMetadataLoad(ctx, m, manifest.RootPath, manifest.WebsiteIndexDocumentSuffixKey); ok {
   455  			pathWithIndex := path.Join(pathVar, indexDocumentSuffixKey)
   456  			indexDocumentManifestEntry, err := m.Lookup(ctx, pathWithIndex)
   457  			if err == nil {
   458  				// index document exists
   459  				logger.Debug("bzz download: serving path", "path", pathWithIndex)
   460  
   461  				s.serveManifestEntry(logger, w, r, indexDocumentManifestEntry, !feedDereferenced, headerOnly)
   462  				return
   463  			}
   464  		}
   465  		logger.Debug("bzz download: address not found or incorrect", "address", address, "path", pathVar)
   466  		logger.Error(nil, "address not found or incorrect")
   467  		jsonhttp.NotFound(w, "address not found or incorrect")
   468  		return
   469  	}
   470  	me, err := m.Lookup(ctx, pathVar)
   471  	if err != nil {
   472  		loggerV1.Debug("bzz download: invalid path", "address", address, "path", pathVar, "error", err)
   473  		logger.Error(nil, "bzz download: invalid path")
   474  
   475  		if errors.Is(err, manifest.ErrNotFound) {
   476  
   477  			if !strings.HasPrefix(pathVar, "/") {
   478  				// check for directory
   479  				dirPath := pathVar + "/"
   480  				exists, err := m.HasPrefix(ctx, dirPath)
   481  				if err == nil && exists {
   482  					// redirect to directory
   483  					u := r.URL
   484  					u.Path += "/"
   485  					redirectURL := u.String()
   486  
   487  					logger.Debug("bzz download: redirecting failed", "url", redirectURL, "error", err)
   488  
   489  					http.Redirect(w, r, redirectURL, http.StatusPermanentRedirect)
   490  					return
   491  				}
   492  			}
   493  
   494  			// check index suffix path
   495  			if indexDocumentSuffixKey, ok := manifestMetadataLoad(ctx, m, manifest.RootPath, manifest.WebsiteIndexDocumentSuffixKey); ok {
   496  				if !strings.HasSuffix(pathVar, indexDocumentSuffixKey) {
   497  					// check if path is directory with index
   498  					pathWithIndex := path.Join(pathVar, indexDocumentSuffixKey)
   499  					indexDocumentManifestEntry, err := m.Lookup(ctx, pathWithIndex)
   500  					if err == nil {
   501  						// index document exists
   502  						logger.Debug("bzz download: serving path", "path", pathWithIndex)
   503  
   504  						s.serveManifestEntry(logger, w, r, indexDocumentManifestEntry, !feedDereferenced, headerOnly)
   505  						return
   506  					}
   507  				}
   508  			}
   509  
   510  			// check if error document is to be shown
   511  			if errorDocumentPath, ok := manifestMetadataLoad(ctx, m, manifest.RootPath, manifest.WebsiteErrorDocumentPathKey); ok {
   512  				if pathVar != errorDocumentPath {
   513  					errorDocumentManifestEntry, err := m.Lookup(ctx, errorDocumentPath)
   514  					if err == nil {
   515  						// error document exists
   516  						logger.Debug("bzz download: serving path", "path", errorDocumentPath)
   517  
   518  						s.serveManifestEntry(logger, w, r, errorDocumentManifestEntry, !feedDereferenced, headerOnly)
   519  						return
   520  					}
   521  				}
   522  			}
   523  
   524  			jsonhttp.NotFound(w, "path address not found")
   525  		} else {
   526  			jsonhttp.NotFound(w, nil)
   527  		}
   528  		return
   529  	}
   530  
   531  	// serve requested path
   532  	s.serveManifestEntry(logger, w, r, me, !feedDereferenced, headerOnly)
   533  }
   534  
   535  func (s *Service) serveManifestEntry(
   536  	logger log.Logger,
   537  	w http.ResponseWriter,
   538  	r *http.Request,
   539  	manifestEntry manifest.Entry,
   540  	etag, headersOnly bool,
   541  ) {
   542  	additionalHeaders := http.Header{}
   543  	mtdt := manifestEntry.Metadata()
   544  	if fname, ok := mtdt[manifest.EntryMetadataFilenameKey]; ok {
   545  		fname = filepath.Base(fname) // only keep the file name
   546  		additionalHeaders[ContentDispositionHeader] =
   547  			[]string{fmt.Sprintf("inline; filename=\"%s\"", fname)}
   548  	}
   549  	if mimeType, ok := mtdt[manifest.EntryMetadataContentTypeKey]; ok {
   550  		additionalHeaders[ContentTypeHeader] = []string{mimeType}
   551  	}
   552  
   553  	s.downloadHandler(logger, w, r, manifestEntry.Reference(), additionalHeaders, etag, headersOnly)
   554  }
   555  
   556  // downloadHandler contains common logic for downloading Swarm file from API
   557  func (s *Service) downloadHandler(logger log.Logger, w http.ResponseWriter, r *http.Request, reference swarm.Address, additionalHeaders http.Header, etag, headersOnly bool) {
   558  	headers := struct {
   559  		Strategy              *getter.Strategy `map:"Swarm-Redundancy-Strategy"`
   560  		FallbackMode          *bool            `map:"Swarm-Redundancy-Fallback-Mode"`
   561  		ChunkRetrievalTimeout *string          `map:"Swarm-Chunk-Retrieval-Timeout"`
   562  		LookaheadBufferSize   *int             `map:"Swarm-Lookahead-Buffer-Size"`
   563  		Cache                 *bool            `map:"Swarm-Cache"`
   564  	}{}
   565  
   566  	if response := s.mapStructure(r.Header, &headers); response != nil {
   567  		response("invalid header params", logger, w)
   568  		return
   569  	}
   570  	cache := true
   571  	if headers.Cache != nil {
   572  		cache = *headers.Cache
   573  	}
   574  
   575  	ctx := r.Context()
   576  	ctx, err := getter.SetConfigInContext(ctx, headers.Strategy, headers.FallbackMode, headers.ChunkRetrievalTimeout, logger)
   577  	if err != nil {
   578  		logger.Error(err, err.Error())
   579  		jsonhttp.BadRequest(w, "could not parse headers")
   580  		return
   581  	}
   582  
   583  	reader, l, err := joiner.New(ctx, s.storer.Download(cache), s.storer.Cache(), reference)
   584  	if err != nil {
   585  		if errors.Is(err, storage.ErrNotFound) || errors.Is(err, topology.ErrNotFound) {
   586  			logger.Debug("api download: not found ", "address", reference, "error", err)
   587  			logger.Error(nil, err.Error())
   588  			jsonhttp.NotFound(w, nil)
   589  			return
   590  		}
   591  		logger.Debug("api download: unexpected error", "address", reference, "error", err)
   592  		logger.Error(nil, "api download: unexpected error")
   593  		jsonhttp.InternalServerError(w, "joiner failed")
   594  		return
   595  	}
   596  
   597  	// include additional headers
   598  	for name, values := range additionalHeaders {
   599  		w.Header().Set(name, strings.Join(values, "; "))
   600  	}
   601  	if etag {
   602  		w.Header().Set(ETagHeader, fmt.Sprintf("%q", reference))
   603  	}
   604  	w.Header().Set(ContentLengthHeader, strconv.FormatInt(l, 10))
   605  	w.Header().Set("Access-Control-Expose-Headers", ContentDispositionHeader)
   606  
   607  	if headersOnly {
   608  		w.WriteHeader(http.StatusOK)
   609  		return
   610  	}
   611  
   612  	bufSize := lookaheadBufferSize(l)
   613  	if headers.LookaheadBufferSize != nil {
   614  		bufSize = *(headers.LookaheadBufferSize)
   615  	}
   616  	if bufSize > 0 {
   617  		http.ServeContent(w, r, "", time.Now(), langos.NewBufferedLangos(reader, bufSize))
   618  		return
   619  	}
   620  	http.ServeContent(w, r, "", time.Now(), reader)
   621  }
   622  
   623  // manifestMetadataLoad returns the value for a key stored in the metadata of
   624  // manifest path, or empty string if no value is present.
   625  // The ok result indicates whether value was found in the metadata.
   626  func manifestMetadataLoad(
   627  	ctx context.Context,
   628  	manifest manifest.Interface,
   629  	path, metadataKey string,
   630  ) (string, bool) {
   631  	me, err := manifest.Lookup(ctx, path)
   632  	if err != nil {
   633  		return "", false
   634  	}
   635  
   636  	manifestRootMetadata := me.Metadata()
   637  	if val, ok := manifestRootMetadata[metadataKey]; ok {
   638  		return val, ok
   639  	}
   640  
   641  	return "", false
   642  }
   643  
   644  func (s *Service) manifestFeed(
   645  	ctx context.Context,
   646  	m manifest.Interface,
   647  ) (feeds.Lookup, error) {
   648  	e, err := m.Lookup(ctx, "/")
   649  	if err != nil {
   650  		return nil, fmt.Errorf("node lookup: %w", err)
   651  	}
   652  	var (
   653  		owner, topic []byte
   654  		t            = new(feeds.Type)
   655  	)
   656  	meta := e.Metadata()
   657  	if e := meta[feedMetadataEntryOwner]; e != "" {
   658  		owner, err = hex.DecodeString(e)
   659  		if err != nil {
   660  			return nil, err
   661  		}
   662  	}
   663  	if e := meta[feedMetadataEntryTopic]; e != "" {
   664  		topic, err = hex.DecodeString(e)
   665  		if err != nil {
   666  			return nil, err
   667  		}
   668  	}
   669  	if e := meta[feedMetadataEntryType]; e != "" {
   670  		err := t.FromString(e)
   671  		if err != nil {
   672  			return nil, err
   673  		}
   674  	}
   675  	if len(owner) == 0 || len(topic) == 0 {
   676  		return nil, fmt.Errorf("node lookup: %s", "feed metadata absent")
   677  	}
   678  	f := feeds.New(topic, common.BytesToAddress(owner))
   679  	return s.feedFactory.NewLookup(*t, f)
   680  }