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 }