Commit 298a5386 authored by Nemanja Zbiljić's avatar Nemanja Zbiljić Committed by GitHub

Add support for manifest metadata (#713)

parent 3c321e9d
...@@ -9,7 +9,7 @@ require ( ...@@ -9,7 +9,7 @@ require (
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
github.com/ethereum/go-ethereum v1.9.20 github.com/ethereum/go-ethereum v1.9.20
github.com/ethersphere/bmt v0.1.2 github.com/ethersphere/bmt v0.1.2
github.com/ethersphere/manifest v0.2.0 github.com/ethersphere/manifest v0.3.0
github.com/ethersphere/sw3-bindings/v2 v2.0.0 github.com/ethersphere/sw3-bindings/v2 v2.0.0
github.com/gogo/protobuf v1.3.1 github.com/gogo/protobuf v1.3.1
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
......
...@@ -165,8 +165,8 @@ github.com/ethereum/go-ethereum v1.9.20 h1:kk/J5OIoaoz3DRrCXznz3RGi212mHHXwzXlY/ ...@@ -165,8 +165,8 @@ github.com/ethereum/go-ethereum v1.9.20 h1:kk/J5OIoaoz3DRrCXznz3RGi212mHHXwzXlY/
github.com/ethereum/go-ethereum v1.9.20/go.mod h1:JSSTypSMTkGZtAdAChH2wP5dZEvPGh3nUTuDpH+hNrg= github.com/ethereum/go-ethereum v1.9.20/go.mod h1:JSSTypSMTkGZtAdAChH2wP5dZEvPGh3nUTuDpH+hNrg=
github.com/ethersphere/bmt v0.1.2 h1:FEuvQY9xuK+rDp3VwDVyde8T396Matv/u9PdtKa2r9Q= github.com/ethersphere/bmt v0.1.2 h1:FEuvQY9xuK+rDp3VwDVyde8T396Matv/u9PdtKa2r9Q=
github.com/ethersphere/bmt v0.1.2/go.mod h1:fqRBDmYwn3lX2MH4lkImXQgFWeNP8ikLkS/hgi/HRws= github.com/ethersphere/bmt v0.1.2/go.mod h1:fqRBDmYwn3lX2MH4lkImXQgFWeNP8ikLkS/hgi/HRws=
github.com/ethersphere/manifest v0.2.0 h1:HD2ufiIaw/5Vgrl4XyeGduDJ5tn50wIhqMQoWdT2GME= github.com/ethersphere/manifest v0.3.0 h1:+QRXY/AQ17mg0x3e20gvn4aAOHsZpm3rzi930bsOlro=
github.com/ethersphere/manifest v0.2.0/go.mod h1:ygAx0KLhXYmKqsjUab95RCbXf8UcO7yMDjyfP0lY76Y= github.com/ethersphere/manifest v0.3.0/go.mod h1:ygAx0KLhXYmKqsjUab95RCbXf8UcO7yMDjyfP0lY76Y=
github.com/ethersphere/sw3-bindings/v2 v2.0.0 h1:uc+wBqEMMq7c4NWj+MSkKkkpObgrUYxfAxz6FYJWkI4= github.com/ethersphere/sw3-bindings/v2 v2.0.0 h1:uc+wBqEMMq7c4NWj+MSkKkkpObgrUYxfAxz6FYJWkI4=
github.com/ethersphere/sw3-bindings/v2 v2.0.0/go.mod h1:OA34yk7ludjNag+yBDY9Gp3czWoFUVMsiK7gUXnZ26U= github.com/ethersphere/sw3-bindings/v2 v2.0.0/go.mod h1:OA34yk7ludjNag+yBDY9Gp3czWoFUVMsiK7gUXnZ26U=
github.com/fatih/color v1.3.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.3.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
......
...@@ -25,10 +25,11 @@ import ( ...@@ -25,10 +25,11 @@ import (
) )
const ( const (
SwarmPinHeader = "Swarm-Pin" SwarmPinHeader = "Swarm-Pin"
SwarmTagUidHeader = "Swarm-Tag-Uid" SwarmTagUidHeader = "Swarm-Tag-Uid"
SwarmEncryptHeader = "Swarm-Encrypt" SwarmEncryptHeader = "Swarm-Encrypt"
SwarmIndextHeader = "Swarm-Index" SwarmIndexDocumentHeader = "Swarm-Index-Document"
SwarmErrorDocumentHeader = "Swarm-Error-Document"
) )
var ( var (
......
...@@ -26,14 +26,15 @@ import ( ...@@ -26,14 +26,15 @@ import (
) )
type testServerOptions struct { type testServerOptions struct {
Storer storage.Storer Storer storage.Storer
Resolver resolver.Interface Resolver resolver.Interface
Pss pss.Interface Pss pss.Interface
WsPath string WsPath string
Tags *tags.Tags Tags *tags.Tags
GatewayMode bool GatewayMode bool
WsPingPeriod time.Duration WsPingPeriod time.Duration
Logger logging.Logger Logger logging.Logger
PreventRedirect bool
} }
func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket.Conn, string) { func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket.Conn, string) {
...@@ -74,8 +75,14 @@ func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket. ...@@ -74,8 +75,14 @@ func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket.
if err != nil { if err != nil {
t.Fatalf("dial: %v. url %v", err, u.String()) t.Fatalf("dial: %v. url %v", err, u.String())
} }
}
if o.PreventRedirect {
httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
} }
return httpClient, conn, ts.Listener.Addr().String() return httpClient, conn, ts.Listener.Addr().String()
} }
......
...@@ -10,6 +10,7 @@ import ( ...@@ -10,6 +10,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"path"
"strings" "strings"
"github.com/gorilla/mux" "github.com/gorilla/mux"
...@@ -20,6 +21,7 @@ import ( ...@@ -20,6 +21,7 @@ import (
"github.com/ethersphere/bee/pkg/jsonhttp" "github.com/ethersphere/bee/pkg/jsonhttp"
"github.com/ethersphere/bee/pkg/manifest" "github.com/ethersphere/bee/pkg/manifest"
"github.com/ethersphere/bee/pkg/sctx" "github.com/ethersphere/bee/pkg/sctx"
"github.com/ethersphere/bee/pkg/swarm"
"github.com/ethersphere/bee/pkg/tracing" "github.com/ethersphere/bee/pkg/tracing"
) )
...@@ -30,8 +32,12 @@ func (s *server) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -30,8 +32,12 @@ func (s *server) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
nameOrHex := mux.Vars(r)["address"] nameOrHex := mux.Vars(r)["address"]
path := mux.Vars(r)["path"] pathVar := mux.Vars(r)["path"]
path = strings.TrimRight(path, "/") if strings.HasSuffix(pathVar, "/") {
pathVar = strings.TrimRight(pathVar, "/")
// NOTE: leave one slash if there was some
pathVar += "/"
}
address, err := s.resolveNameOrAddress(nameOrHex) address, err := s.resolveNameOrAddress(nameOrHex)
if err != nil { if err != nil {
...@@ -96,12 +102,76 @@ func (s *server) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -96,12 +102,76 @@ func (s *server) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
me, err := m.Lookup(path) if pathVar == "" {
logger.Tracef("bzz download: handle empty path %s", address)
if indexDocumentSuffixKey, ok := manifestMetadataLoad(m, manifestRootPath, manifestWebsiteIndexDocumentSuffixKey); ok {
pathWithIndex := path.Join(pathVar, indexDocumentSuffixKey)
indexDocumentManifestEntry, err := m.Lookup(pathWithIndex)
if err == nil {
// index document exists
logger.Debugf("bzz download: serving path: %s", pathWithIndex)
s.serveManifestEntry(w, r, j, address, indexDocumentManifestEntry.Reference())
return
}
}
}
me, err := m.Lookup(pathVar)
if err != nil { if err != nil {
logger.Debugf("bzz download: invalid path %s/%s: %v", address, path, err) logger.Debugf("bzz download: invalid path %s/%s: %v", address, pathVar, err)
logger.Error("bzz download: invalid path") logger.Error("bzz download: invalid path")
if errors.Is(err, manifest.ErrNotFound) { if errors.Is(err, manifest.ErrNotFound) {
if !strings.HasPrefix(pathVar, "/") {
// check for directory
dirPath := pathVar + "/"
exists, err := m.HasPrefix(dirPath)
if err == nil && exists {
// redirect to directory
u := r.URL
u.Path += "/"
redirectURL := u.String()
logger.Debugf("bzz download: redirecting to %s: %v", redirectURL, err)
http.Redirect(w, r, redirectURL, http.StatusPermanentRedirect)
return
}
}
// check index suffix path
if indexDocumentSuffixKey, ok := manifestMetadataLoad(m, manifestRootPath, manifestWebsiteIndexDocumentSuffixKey); ok {
if !strings.HasSuffix(pathVar, indexDocumentSuffixKey) {
// check if path is directory with index
pathWithIndex := path.Join(pathVar, indexDocumentSuffixKey)
indexDocumentManifestEntry, err := m.Lookup(pathWithIndex)
if err == nil {
// index document exists
logger.Debugf("bzz download: serving path: %s", pathWithIndex)
s.serveManifestEntry(w, r, j, address, indexDocumentManifestEntry.Reference())
return
}
}
}
// check if error document is to be shown
if errorDocumentPath, ok := manifestMetadataLoad(m, manifestRootPath, manifestWebsiteErrorDocumentPathKey); ok {
if pathVar != errorDocumentPath {
errorDocumentManifestEntry, err := m.Lookup(errorDocumentPath)
if err == nil {
// error document exists
logger.Debugf("bzz download: serving path: %s", errorDocumentPath)
s.serveManifestEntry(w, r, j, address, errorDocumentManifestEntry.Reference())
return
}
}
}
jsonhttp.NotFound(w, "path address not found") jsonhttp.NotFound(w, "path address not found")
} else { } else {
jsonhttp.BadRequest(w, "invalid path address") jsonhttp.BadRequest(w, "invalid path address")
...@@ -109,11 +179,17 @@ func (s *server) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -109,11 +179,17 @@ func (s *server) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
manifestEntryAddress := me.Reference() // serve requested path
s.serveManifestEntry(w, r, j, address, me.Reference())
}
func (s *server) serveManifestEntry(w http.ResponseWriter, r *http.Request, j file.JoinSeeker, address, manifestEntryAddress swarm.Address) {
logger := tracing.NewLoggerWithTraceID(r.Context(), s.Logger)
ctx := r.Context()
// read file entry // read file entry
buf = bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
_, err = file.JoinReadAll(ctx, j, manifestEntryAddress, buf) _, err := file.JoinReadAll(ctx, j, manifestEntryAddress, buf)
if err != nil { if err != nil {
logger.Debugf("bzz download: read file entry %s: %v", address, err) logger.Debugf("bzz download: read file entry %s: %v", address, err)
logger.Errorf("bzz download: read file entry %s", address) logger.Errorf("bzz download: read file entry %s", address)
...@@ -156,3 +232,20 @@ func (s *server) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -156,3 +232,20 @@ func (s *server) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) {
s.downloadHandler(w, r, fileEntryAddress, additionalHeaders) s.downloadHandler(w, r, fileEntryAddress, additionalHeaders)
} }
// manifestMetadataLoad returns the value for a key stored in the metadata of
// manifest path, or empty string if no value is present.
// The ok result indicates whether value was found in the metadata.
func manifestMetadataLoad(manifest manifest.Interface, path, metadataKey string) (string, bool) {
me, err := manifest.Lookup(path)
if err != nil {
return "", false
}
manifestRootMetadata := me.Metadata()
if val, ok := manifestRootMetadata[metadataKey]; ok {
return val, ok
}
return "", false
}
...@@ -16,14 +16,13 @@ import ( ...@@ -16,14 +16,13 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/ethersphere/bee/pkg/file/pipeline/builder"
statestore "github.com/ethersphere/bee/pkg/statestore/mock"
"github.com/ethersphere/bee/pkg/collection/entry" "github.com/ethersphere/bee/pkg/collection/entry"
"github.com/ethersphere/bee/pkg/file/pipeline/builder"
"github.com/ethersphere/bee/pkg/jsonhttp" "github.com/ethersphere/bee/pkg/jsonhttp"
"github.com/ethersphere/bee/pkg/jsonhttp/jsonhttptest" "github.com/ethersphere/bee/pkg/jsonhttp/jsonhttptest"
"github.com/ethersphere/bee/pkg/logging" "github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/manifest" "github.com/ethersphere/bee/pkg/manifest"
statestore "github.com/ethersphere/bee/pkg/statestore/mock"
"github.com/ethersphere/bee/pkg/storage" "github.com/ethersphere/bee/pkg/storage"
smock "github.com/ethersphere/bee/pkg/storage/mock" smock "github.com/ethersphere/bee/pkg/storage/mock"
"github.com/ethersphere/bee/pkg/swarm" "github.com/ethersphere/bee/pkg/swarm"
...@@ -104,7 +103,7 @@ func TestBzz(t *testing.T) { ...@@ -104,7 +103,7 @@ func TestBzz(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
e := manifest.NewEntry(fileReference) e := manifest.NewEntry(fileReference, nil)
err = m.Add(filePath, e) err = m.Add(filePath, e)
if err != nil { if err != nil {
......
...@@ -33,6 +33,12 @@ const ( ...@@ -33,6 +33,12 @@ const (
contentTypeTar = "application/x-tar" contentTypeTar = "application/x-tar"
) )
const (
manifestRootPath = "/"
manifestWebsiteIndexDocumentSuffixKey = "website-index-document"
manifestWebsiteErrorDocumentPathKey = "website-error-document"
)
// dirUploadHandler uploads a directory supplied as a tar in an HTTP request // dirUploadHandler uploads a directory supplied as a tar in an HTTP request
func (s *server) dirUploadHandler(w http.ResponseWriter, r *http.Request) { func (s *server) dirUploadHandler(w http.ResponseWriter, r *http.Request) {
logger := tracing.NewLoggerWithTraceID(r.Context(), s.Logger) logger := tracing.NewLoggerWithTraceID(r.Context(), s.Logger)
...@@ -55,7 +61,7 @@ func (s *server) dirUploadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -55,7 +61,7 @@ func (s *server) dirUploadHandler(w http.ResponseWriter, r *http.Request) {
// Add the tag to the context // Add the tag to the context
ctx := sctx.SetTag(r.Context(), tag) ctx := sctx.SetTag(r.Context(), tag)
reference, err := storeDir(ctx, r.Body, s.Storer, requestModePut(r), s.Logger, requestEncrypt(r), r.Header.Get(SwarmIndextHeader)) reference, err := storeDir(ctx, r.Body, s.Storer, requestModePut(r), s.Logger, requestEncrypt(r), r.Header.Get(SwarmIndexDocumentHeader), r.Header.Get(SwarmErrorDocumentHeader))
if err != nil { if err != nil {
logger.Debugf("dir upload: store dir err: %v", err) logger.Debugf("dir upload: store dir err: %v", err)
logger.Errorf("dir upload: store dir") logger.Errorf("dir upload: store dir")
...@@ -95,7 +101,7 @@ func validateRequest(r *http.Request) error { ...@@ -95,7 +101,7 @@ func validateRequest(r *http.Request) error {
// storeDir stores all files recursively contained in the directory given as a tar // storeDir stores all files recursively contained in the directory given as a tar
// it returns the hash for the uploaded manifest corresponding to the uploaded dir // it returns the hash for the uploaded manifest corresponding to the uploaded dir
func storeDir(ctx context.Context, reader io.ReadCloser, s storage.Storer, mode storage.ModePut, log logging.Logger, encrypt bool, indexFilename string) (swarm.Address, error) { func storeDir(ctx context.Context, reader io.ReadCloser, s storage.Storer, mode storage.ModePut, log logging.Logger, encrypt bool, indexFilename string, errorFilename string) (swarm.Address, error) {
logger := tracing.NewLoggerWithTraceID(ctx, log) logger := tracing.NewLoggerWithTraceID(ctx, log)
dirManifest, err := manifest.NewDefaultManifest(encrypt, s) dirManifest, err := manifest.NewDefaultManifest(encrypt, s)
...@@ -103,6 +109,10 @@ func storeDir(ctx context.Context, reader io.ReadCloser, s storage.Storer, mode ...@@ -103,6 +109,10 @@ func storeDir(ctx context.Context, reader io.ReadCloser, s storage.Storer, mode
return swarm.ZeroAddress, err return swarm.ZeroAddress, err
} }
if indexFilename != "" && strings.ContainsRune(indexFilename, '/') {
return swarm.ZeroAddress, fmt.Errorf("index document suffix must not include slash character")
}
// set up HTTP body reader // set up HTTP body reader
tarReader := tar.NewReader(reader) tarReader := tar.NewReader(reader)
defer reader.Close() defer reader.Close()
...@@ -143,23 +153,11 @@ func storeDir(ctx context.Context, reader io.ReadCloser, s storage.Storer, mode ...@@ -143,23 +153,11 @@ func storeDir(ctx context.Context, reader io.ReadCloser, s storage.Storer, mode
logger.Tracef("uploaded dir file %v with reference %v", filePath, fileReference) logger.Tracef("uploaded dir file %v with reference %v", filePath, fileReference)
// add file entry to dir manifest // add file entry to dir manifest
err = dirManifest.Add(filePath, manifest.NewEntry(fileReference)) err = dirManifest.Add(filePath, manifest.NewEntry(fileReference, nil))
if err != nil { if err != nil {
return swarm.ZeroAddress, fmt.Errorf("add to manifest: %w", err) return swarm.ZeroAddress, fmt.Errorf("add to manifest: %w", err)
} }
if indexFilename != "" && filePath == indexFilename || strings.HasSuffix(filePath, "/"+indexFilename) {
filePath := strings.TrimSuffix(filePath, indexFilename)
filePath = strings.TrimRight(filePath, "/")
// add file entry to dir manifest
err = dirManifest.Add(filePath, manifest.NewEntry(fileReference))
if err != nil {
return swarm.ZeroAddress, fmt.Errorf("add to manifest: %w", err)
}
}
filesAdded++ filesAdded++
} }
...@@ -168,6 +166,22 @@ func storeDir(ctx context.Context, reader io.ReadCloser, s storage.Storer, mode ...@@ -168,6 +166,22 @@ func storeDir(ctx context.Context, reader io.ReadCloser, s storage.Storer, mode
return swarm.ZeroAddress, fmt.Errorf("no files in tar") return swarm.ZeroAddress, fmt.Errorf("no files in tar")
} }
// store website information
if indexFilename != "" || errorFilename != "" {
metadata := map[string]string{}
if indexFilename != "" {
metadata[manifestWebsiteIndexDocumentSuffixKey] = indexFilename
}
if errorFilename != "" {
metadata[manifestWebsiteErrorDocumentPathKey] = errorFilename
}
rootManifestEntry := manifest.NewEntry(swarm.ZeroAddress, metadata)
err = dirManifest.Add(manifestRootPath, rootManifestEntry)
if err != nil {
return swarm.ZeroAddress, fmt.Errorf("add to manifest: %w", err)
}
}
// save manifest // save manifest
manifestBytesReference, err := dirManifest.Store(ctx, mode) manifestBytesReference, err := dirManifest.Store(ctx, mode)
if err != nil { if err != nil {
......
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"path" "path"
...@@ -32,13 +33,15 @@ func TestDirs(t *testing.T) { ...@@ -32,13 +33,15 @@ func TestDirs(t *testing.T) {
var ( var (
dirUploadResource = "/dirs" dirUploadResource = "/dirs"
fileDownloadResource = func(addr string) string { return "/files/" + addr } fileDownloadResource = func(addr string) string { return "/files/" + addr }
bzzDownloadResource = func(addr, path string) string { return "/bzz/" + addr + "/" + path }
storer = mock.NewStorer() storer = mock.NewStorer()
mockStatestore = statestore.NewStateStore() mockStatestore = statestore.NewStateStore()
logger = logging.New(ioutil.Discard, 0) logger = logging.New(ioutil.Discard, 0)
client, _, _ = newTestServer(t, testServerOptions{ client, _, _ = newTestServer(t, testServerOptions{
Storer: storer, Storer: storer,
Tags: tags.NewTags(mockStatestore, logger), Tags: tags.NewTags(mockStatestore, logger),
Logger: logging.New(ioutil.Discard, 5), Logger: logging.New(ioutil.Discard, 5),
PreventRedirect: true,
}) })
) )
...@@ -87,7 +90,9 @@ func TestDirs(t *testing.T) { ...@@ -87,7 +90,9 @@ func TestDirs(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
name string name string
wantIndexFilename string wantIndexFilename string
wantErrorFilename string
indexFilenameOption jsonhttptest.Option indexFilenameOption jsonhttptest.Option
errorFilenameOption jsonhttptest.Option
files []f // files in dir for test case files []f // files in dir for test case
}{ }{
{ {
...@@ -162,7 +167,7 @@ func TestDirs(t *testing.T) { ...@@ -162,7 +167,7 @@ func TestDirs(t *testing.T) {
{ {
name: "explicit index filename", name: "explicit index filename",
wantIndexFilename: "index.html", wantIndexFilename: "index.html",
indexFilenameOption: jsonhttptest.WithRequestHeader(api.SwarmIndextHeader, "index.html"), indexFilenameOption: jsonhttptest.WithRequestHeader(api.SwarmIndexDocumentHeader, "index.html"),
files: []f{ files: []f{
{ {
data: []byte("<h1>Swarm"), data: []byte("<h1>Swarm"),
...@@ -178,7 +183,7 @@ func TestDirs(t *testing.T) { ...@@ -178,7 +183,7 @@ func TestDirs(t *testing.T) {
{ {
name: "nested index filename", name: "nested index filename",
wantIndexFilename: "index.html", wantIndexFilename: "index.html",
indexFilenameOption: jsonhttptest.WithRequestHeader(api.SwarmIndextHeader, "index.html"), indexFilenameOption: jsonhttptest.WithRequestHeader(api.SwarmIndexDocumentHeader, "index.html"),
files: []f{ files: []f{
{ {
data: []byte("<h1>Swarm"), data: []byte("<h1>Swarm"),
...@@ -191,6 +196,33 @@ func TestDirs(t *testing.T) { ...@@ -191,6 +196,33 @@ func TestDirs(t *testing.T) {
}, },
}, },
}, },
{
name: "explicit index and error filename",
wantIndexFilename: "index.html",
wantErrorFilename: "error.html",
indexFilenameOption: jsonhttptest.WithRequestHeader(api.SwarmIndexDocumentHeader, "index.html"),
errorFilenameOption: jsonhttptest.WithRequestHeader(api.SwarmErrorDocumentHeader, "error.html"),
files: []f{
{
data: []byte("<h1>Swarm"),
name: "index.html",
dir: "",
reference: swarm.MustParseHexAddress("bcb1bfe15c36f1a529a241f4d0c593e5648aa6d40859790894c6facb41a6ef28"),
header: http.Header{
"Content-Type": {"text/html; charset=utf-8"},
},
},
{
data: []byte("<h2>404"),
name: "error.html",
dir: "",
reference: swarm.MustParseHexAddress("b1f309c095d650521b75760b23122a9c59c2b581af28fc6daaf9c58da86a204d"),
header: http.Header{
"Content-Type": {"text/html; charset=utf-8"},
},
},
},
},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
// tar all the test case files // tar all the test case files
...@@ -206,6 +238,9 @@ func TestDirs(t *testing.T) { ...@@ -206,6 +238,9 @@ func TestDirs(t *testing.T) {
if tc.indexFilenameOption != nil { if tc.indexFilenameOption != nil {
options = append(options, tc.indexFilenameOption) options = append(options, tc.indexFilenameOption)
} }
if tc.errorFilenameOption != nil {
options = append(options, tc.errorFilenameOption)
}
// verify directory tar upload response // verify directory tar upload response
jsonhttptest.Request(t, client, http.MethodPost, dirUploadResource, http.StatusOK, options...) jsonhttptest.Request(t, client, http.MethodPost, dirUploadResource, http.StatusOK, options...)
...@@ -271,15 +306,77 @@ func TestDirs(t *testing.T) { ...@@ -271,15 +306,77 @@ func TestDirs(t *testing.T) {
) )
} }
validateIsPermanentRedirect := func(t *testing.T, fromPath, toPath string) {
t.Helper()
expectedResponse := fmt.Sprintf("<a href=\"%s\">Permanent Redirect</a>.\n\n", bzzDownloadResource(resp.Reference.String(), toPath))
jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(resp.Reference.String(), fromPath), http.StatusPermanentRedirect,
jsonhttptest.WithExpectedResponse([]byte(expectedResponse)),
)
}
validateBzzPath := func(t *testing.T, fromPath, toPath string) {
t.Helper()
toEntry, err := verifyManifest.Lookup(toPath)
if err != nil {
t.Fatal(err)
}
var respBytes []byte
jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(toEntry.Reference().String()), http.StatusOK,
jsonhttptest.WithPutResponseBody(&respBytes),
)
jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(resp.Reference.String(), fromPath), http.StatusOK,
jsonhttptest.WithExpectedResponse(respBytes),
)
}
// check if each file can be located and read // check if each file can be located and read
for _, file := range tc.files { for _, file := range tc.files {
validateFile(t, file, path.Join(file.dir, file.name)) validateFile(t, file, path.Join(file.dir, file.name))
}
// if there is an index filename to be tested // check index filename
// try to download it using only the directory as the path if tc.wantIndexFilename != "" {
if file.name == tc.wantIndexFilename { entry, err := verifyManifest.Lookup(api.ManifestRootPath)
validateFile(t, file, file.dir) if err != nil {
t.Fatal(err)
}
manifestRootMetadata := entry.Metadata()
indexDocumentSuffixPath, ok := manifestRootMetadata[api.ManifestWebsiteIndexDocumentSuffixKey]
if !ok {
t.Fatalf("expected index filename '%s', did not find any", tc.wantIndexFilename)
} }
// check index suffix for each dir
for _, file := range tc.files {
if file.dir != "" {
validateIsPermanentRedirect(t, file.dir, file.dir+"/")
validateBzzPath(t, file.dir+"/", path.Join(file.dir, indexDocumentSuffixPath))
}
}
}
// check error filename
if tc.wantErrorFilename != "" {
entry, err := verifyManifest.Lookup(api.ManifestRootPath)
if err != nil {
t.Fatal(err)
}
manifestRootMetadata := entry.Metadata()
errorDocumentPath, ok := manifestRootMetadata[api.ManifestWebsiteErrorDocumentPathKey]
if !ok {
t.Fatalf("expected error filename '%s', did not find any", tc.wantErrorFilename)
}
// check error document
validateBzzPath(t, "_non_existent_file_path_", errorDocumentPath)
} }
}) })
......
...@@ -21,6 +21,12 @@ var ( ...@@ -21,6 +21,12 @@ var (
ContentTypeTar = contentTypeTar ContentTypeTar = contentTypeTar
) )
var (
ManifestRootPath = manifestRootPath
ManifestWebsiteIndexDocumentSuffixKey = manifestWebsiteIndexDocumentSuffixKey
ManifestWebsiteErrorDocumentPathKey = manifestWebsiteErrorDocumentPathKey
)
var ( var (
ErrNoResolver = errNoResolver ErrNoResolver = errNoResolver
ErrInvalidNameOrAddress = errInvalidNameOrAddress ErrInvalidNameOrAddress = errInvalidNameOrAddress
......
...@@ -33,6 +33,8 @@ type Interface interface { ...@@ -33,6 +33,8 @@ type Interface interface {
Remove(string) error Remove(string) error
// Lookup returns a manifest entry if one is found in the specified path. // Lookup returns a manifest entry if one is found in the specified path.
Lookup(string) (Entry, error) Lookup(string) (Entry, error)
// HasPrefix tests whether the specified prefix path exists.
HasPrefix(string) (bool, error)
// Store stores the manifest, returning the resulting address. // Store stores the manifest, returning the resulting address.
Store(context.Context, storage.ModePut) (swarm.Address, error) Store(context.Context, storage.ModePut) (swarm.Address, error)
} }
...@@ -41,6 +43,8 @@ type Interface interface { ...@@ -41,6 +43,8 @@ type Interface interface {
type Entry interface { type Entry interface {
// Reference returns the address of the file. // Reference returns the address of the file.
Reference() swarm.Address Reference() swarm.Address
// Metadata returns the metadata of the file.
Metadata() map[string]string
} }
// NewDefaultManifest creates a new manifest with default type. // NewDefaultManifest creates a new manifest with default type.
...@@ -87,15 +91,21 @@ func NewManifestReference( ...@@ -87,15 +91,21 @@ func NewManifestReference(
type manifestEntry struct { type manifestEntry struct {
reference swarm.Address reference swarm.Address
metadata map[string]string
} }
// NewEntry creates a new manifest entry. // NewEntry creates a new manifest entry.
func NewEntry(reference swarm.Address) Entry { func NewEntry(reference swarm.Address, metadata map[string]string) Entry {
return &manifestEntry{ return &manifestEntry{
reference: reference, reference: reference,
metadata: metadata,
} }
} }
func (e *manifestEntry) Reference() swarm.Address { func (e *manifestEntry) Reference() swarm.Address {
return e.reference return e.reference
} }
func (e *manifestEntry) Metadata() map[string]string {
return e.metadata
}
...@@ -68,7 +68,7 @@ func (m *mantarayManifest) Add(path string, entry Entry) error { ...@@ -68,7 +68,7 @@ func (m *mantarayManifest) Add(path string, entry Entry) error {
p := []byte(path) p := []byte(path)
e := entry.Reference().Bytes() e := entry.Reference().Bytes()
return m.trie.Add(p, e, m.loader) return m.trie.Add(p, e, entry.Metadata(), m.loader)
} }
func (m *mantarayManifest) Remove(path string) error { func (m *mantarayManifest) Remove(path string) error {
...@@ -88,18 +88,28 @@ func (m *mantarayManifest) Remove(path string) error { ...@@ -88,18 +88,28 @@ func (m *mantarayManifest) Remove(path string) error {
func (m *mantarayManifest) Lookup(path string) (Entry, error) { func (m *mantarayManifest) Lookup(path string) (Entry, error) {
p := []byte(path) p := []byte(path)
ref, err := m.trie.Lookup(p, m.loader) node, err := m.trie.LookupNode(p, m.loader)
if err != nil { if err != nil {
return nil, ErrNotFound return nil, ErrNotFound
} }
address := swarm.NewAddress(ref) if !node.IsValueType() {
return nil, ErrNotFound
}
address := swarm.NewAddress(node.Entry())
entry := NewEntry(address) entry := NewEntry(address, node.Metadata())
return entry, nil return entry, nil
} }
func (m *mantarayManifest) HasPrefix(prefix string) (bool, error) {
p := []byte(prefix)
return m.trie.HasPrefix(p, m.loader)
}
func (m *mantarayManifest) Store(ctx context.Context, mode storage.ModePut) (swarm.Address, error) { func (m *mantarayManifest) Store(ctx context.Context, mode storage.ModePut) (swarm.Address, error) {
saver := newMantaraySaver(ctx, m.encrypted, m.storer, mode) saver := newMantaraySaver(ctx, m.encrypted, m.storer, mode)
......
...@@ -66,7 +66,7 @@ func (m *simpleManifest) Type() string { ...@@ -66,7 +66,7 @@ func (m *simpleManifest) Type() string {
func (m *simpleManifest) Add(path string, entry Entry) error { func (m *simpleManifest) Add(path string, entry Entry) error {
e := entry.Reference().String() e := entry.Reference().String()
return m.manifest.Add(path, e) return m.manifest.Add(path, e, entry.Metadata())
} }
func (m *simpleManifest) Remove(path string) error { func (m *simpleManifest) Remove(path string) error {
...@@ -94,11 +94,15 @@ func (m *simpleManifest) Lookup(path string) (Entry, error) { ...@@ -94,11 +94,15 @@ func (m *simpleManifest) Lookup(path string) (Entry, error) {
return nil, fmt.Errorf("parse swarm address: %w", err) return nil, fmt.Errorf("parse swarm address: %w", err)
} }
entry := NewEntry(address) entry := NewEntry(address, n.Metadata())
return entry, nil return entry, nil
} }
func (m *simpleManifest) HasPrefix(prefix string) (bool, error) {
return m.manifest.HasPrefix(prefix), nil
}
func (m *simpleManifest) Store(ctx context.Context, mode storage.ModePut) (swarm.Address, error) { func (m *simpleManifest) Store(ctx context.Context, mode storage.ModePut) (swarm.Address, error) {
data, err := m.manifest.MarshalBinary() data, err := m.manifest.MarshalBinary()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment