Commit 370a0e97 authored by Nemanja Zbiljić's avatar Nemanja Zbiljić Committed by GitHub

Add support for multiple manifest types (#591)

parent 6177d16c
...@@ -8,6 +8,7 @@ require ( ...@@ -8,6 +8,7 @@ require (
github.com/coreos/go-semver v0.3.0 github.com/coreos/go-semver v0.3.0
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
github.com/ethersphere/bmt v0.1.2 github.com/ethersphere/bmt v0.1.2
github.com/ethersphere/manifest v0.1.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
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
......
...@@ -110,6 +110,8 @@ github.com/ethersphere/bmt v0.1.1 h1:vwHSJwnDyzJ0fqP3YQBDk+/vqdAfulfRGJesQ5kL2ps ...@@ -110,6 +110,8 @@ github.com/ethersphere/bmt v0.1.1 h1:vwHSJwnDyzJ0fqP3YQBDk+/vqdAfulfRGJesQ5kL2ps
github.com/ethersphere/bmt v0.1.1/go.mod h1:fqRBDmYwn3lX2MH4lkImXQgFWeNP8ikLkS/hgi/HRws= github.com/ethersphere/bmt v0.1.1/go.mod h1:fqRBDmYwn3lX2MH4lkImXQgFWeNP8ikLkS/hgi/HRws=
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.1.0 h1:uVlFzAZk5SqyzjHzDgF3rNuDA4CdbJQ8fVHS4pN0iHY=
github.com/ethersphere/manifest v0.1.0/go.mod h1:eV7hOz2c5R1ol+SpBYdS5EUG6ubh11CQe6lFAn5q+q4=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
......
...@@ -7,6 +7,7 @@ package api ...@@ -7,6 +7,7 @@ package api
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
...@@ -17,17 +18,11 @@ import ( ...@@ -17,17 +18,11 @@ import (
"github.com/ethersphere/bee/pkg/file" "github.com/ethersphere/bee/pkg/file"
"github.com/ethersphere/bee/pkg/file/joiner" "github.com/ethersphere/bee/pkg/file/joiner"
"github.com/ethersphere/bee/pkg/jsonhttp" "github.com/ethersphere/bee/pkg/jsonhttp"
"github.com/ethersphere/bee/pkg/manifest/jsonmanifest" "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/swarm"
) )
const (
// ManifestContentType represents content type used for noting that specific
// file should be processed as manifest
ManifestContentType = "application/bzz-manifest+json"
)
func (s *server) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) { func (s *server) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) {
targets := r.URL.Query().Get("targets") targets := r.URL.Query().Get("targets")
r = r.WithContext(sctx.SetTargets(r.Context(), targets)) r = r.WithContext(sctx.SetTargets(r.Context(), targets))
...@@ -74,8 +69,8 @@ func (s *server) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -74,8 +69,8 @@ func (s *server) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) {
jsonhttp.NotFound(w, nil) jsonhttp.NotFound(w, nil)
return return
} }
metadata := &entry.Metadata{} manifestMetadata := &entry.Metadata{}
err = json.Unmarshal(buf.Bytes(), metadata) err = json.Unmarshal(buf.Bytes(), manifestMetadata)
if err != nil { if err != nil {
s.Logger.Debugf("bzz download: unmarshal metadata %s: %v", address, err) s.Logger.Debugf("bzz download: unmarshal metadata %s: %v", address, err)
s.Logger.Errorf("bzz download: unmarshal metadata %s", address) s.Logger.Errorf("bzz download: unmarshal metadata %s", address)
...@@ -84,55 +79,35 @@ func (s *server) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -84,55 +79,35 @@ func (s *server) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) {
} }
// we are expecting manifest Mime type here // we are expecting manifest Mime type here
if ManifestContentType != metadata.MimeType { m, err := manifest.NewManifestReference(
ctx,
manifestMetadata.MimeType,
e.Reference(),
toDecrypt,
s.Storer,
)
if err != nil {
s.Logger.Debugf("bzz download: not manifest %s: %v", address, err) s.Logger.Debugf("bzz download: not manifest %s: %v", address, err)
s.Logger.Error("bzz download: not manifest") s.Logger.Error("bzz download: not manifest")
jsonhttp.BadRequest(w, "not manifest") jsonhttp.BadRequest(w, "not manifest")
return return
} }
// read manifest content me, err := m.Lookup(path)
buf = bytes.NewBuffer(nil)
_, err = file.JoinReadAll(ctx, j, e.Reference(), buf, toDecrypt)
if err != nil {
s.Logger.Debugf("bzz download: data join %s: %v", address, err)
s.Logger.Errorf("bzz download: data join %s", address)
jsonhttp.NotFound(w, nil)
return
}
manifest := jsonmanifest.NewManifest()
err = manifest.UnmarshalBinary(buf.Bytes())
if err != nil {
s.Logger.Debugf("bzz download: unmarshal manifest %s: %v", address, err)
s.Logger.Errorf("bzz download: unmarshal manifest %s", address)
jsonhttp.InternalServerError(w, "error unmarshaling manifest")
return
}
me, err := manifest.Entry(path)
if err != nil { if err != nil {
s.Logger.Debugf("bzz download: invalid path %s/%s: %v", address, path, err) s.Logger.Debugf("bzz download: invalid path %s/%s: %v", address, path, err)
s.Logger.Error("bzz download: invalid path") s.Logger.Error("bzz download: invalid path")
jsonhttp.BadRequest(w, "invalid path address")
if errors.Is(err, manifest.ErrNotFound) {
jsonhttp.NotFound(w, "path address not found")
} else {
jsonhttp.BadRequest(w, "invalid path address")
}
return return
} }
manifestEntryAddress := me.Reference() manifestEntryAddress := me.Reference()
var additionalHeaders http.Header
// copy header from manifest
if me.Header() != nil {
additionalHeaders = me.Header().Clone()
} else {
additionalHeaders = http.Header{}
}
// include filename
if me.Name() != "" {
additionalHeaders.Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", me.Name()))
}
// read file entry // read file entry
buf = bytes.NewBuffer(nil) buf = bytes.NewBuffer(nil)
_, err = file.JoinReadAll(ctx, j, manifestEntryAddress, buf, toDecrypt) _, err = file.JoinReadAll(ctx, j, manifestEntryAddress, buf, toDecrypt)
...@@ -151,6 +126,29 @@ func (s *server) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -151,6 +126,29 @@ func (s *server) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
// read file metadata
buf = bytes.NewBuffer(nil)
_, err = file.JoinReadAll(ctx, j, fe.Metadata(), buf, toDecrypt)
if err != nil {
s.Logger.Debugf("bzz download: read file metadata %s: %v", address, err)
s.Logger.Errorf("bzz download: read file metadata %s", address)
jsonhttp.NotFound(w, nil)
return
}
fileMetadata := &entry.Metadata{}
err = json.Unmarshal(buf.Bytes(), fileMetadata)
if err != nil {
s.Logger.Debugf("bzz download: unmarshal metadata %s: %v", address, err)
s.Logger.Errorf("bzz download: unmarshal metadata %s", address)
jsonhttp.InternalServerError(w, "error unmarshaling metadata")
return
}
additionalHeaders := http.Header{
"Content-Disposition": {fmt.Sprintf("inline; filename=\"%s\"", fileMetadata.Filename)},
"Content-Type": {fileMetadata.MimeType},
}
fileEntryAddress := fe.Reference() fileEntryAddress := fe.Reference()
s.downloadHandler(w, r, fileEntryAddress, additionalHeaders) s.downloadHandler(w, r, fileEntryAddress, additionalHeaders)
......
...@@ -15,14 +15,13 @@ import ( ...@@ -15,14 +15,13 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/ethersphere/bee/pkg/api"
"github.com/ethersphere/bee/pkg/collection/entry" "github.com/ethersphere/bee/pkg/collection/entry"
"github.com/ethersphere/bee/pkg/file" "github.com/ethersphere/bee/pkg/file"
"github.com/ethersphere/bee/pkg/file/splitter" "github.com/ethersphere/bee/pkg/file/splitter"
"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/jsonmanifest" "github.com/ethersphere/bee/pkg/manifest"
"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"
...@@ -69,6 +68,7 @@ func TestBzz(t *testing.T) { ...@@ -69,6 +68,7 @@ func TestBzz(t *testing.T) {
} }
fileMetadata := entry.NewMetadata(fileName) fileMetadata := entry.NewMetadata(fileName)
fileMetadata.MimeType = "text/html; charset=utf-8"
fileMetadataBytes, err := json.Marshal(fileMetadata) fileMetadataBytes, err := json.Marshal(fileMetadata)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
...@@ -91,24 +91,26 @@ func TestBzz(t *testing.T) { ...@@ -91,24 +91,26 @@ func TestBzz(t *testing.T) {
// save manifest // save manifest
jsonManifest := jsonmanifest.NewManifest() m, err := manifest.NewDefaultManifest(false, storer)
if err != nil {
t.Fatal(err)
}
e := jsonmanifest.NewEntry(fileReference, fileName, http.Header{"Content-Type": {"text/html", "charset=utf-8"}}) e := manifest.NewEntry(fileReference)
jsonManifest.Add(filePath, e)
manifestFileBytes, err := jsonManifest.MarshalBinary() err = m.Add(filePath, e)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
fr, err := file.SplitWriteAll(context.Background(), sp, bytes.NewReader(manifestFileBytes), int64(len(manifestFileBytes)), false) manifestBytesReference, err := m.Store(context.Background(), storage.ModePutUpload)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
m := entry.NewMetadata(fileName) metadata := entry.NewMetadata(manifestBytesReference.String())
m.MimeType = api.ManifestContentType metadata.MimeType = m.Type()
metadataBytes, err := json.Marshal(m) metadataBytes, err := json.Marshal(metadata)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -118,8 +120,8 @@ func TestBzz(t *testing.T) { ...@@ -118,8 +120,8 @@ func TestBzz(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// now join both references (mr,fr) to create an entry and store it. // now join both references (fr,mr) to create an entry and store it.
newEntry := entry.New(fr, mr) newEntry := entry.New(manifestBytesReference, mr)
manifestFileEntryBytes, err := newEntry.MarshalBinary() manifestFileEntryBytes, err := newEntry.MarshalBinary()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
...@@ -152,10 +154,10 @@ func TestBzz(t *testing.T) { ...@@ -152,10 +154,10 @@ func TestBzz(t *testing.T) {
// check on invalid path // check on invalid path
jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifestFileReference.String(), missingFilePath), http.StatusBadRequest, jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifestFileReference.String(), missingFilePath), http.StatusNotFound,
jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
Message: "invalid path address", Message: "path address not found",
Code: http.StatusBadRequest, Code: http.StatusNotFound,
}), }),
) )
}) })
......
...@@ -22,7 +22,7 @@ import ( ...@@ -22,7 +22,7 @@ import (
"github.com/ethersphere/bee/pkg/file/splitter" "github.com/ethersphere/bee/pkg/file/splitter"
"github.com/ethersphere/bee/pkg/jsonhttp" "github.com/ethersphere/bee/pkg/jsonhttp"
"github.com/ethersphere/bee/pkg/logging" "github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/manifest/jsonmanifest" "github.com/ethersphere/bee/pkg/manifest"
"github.com/ethersphere/bee/pkg/sctx" "github.com/ethersphere/bee/pkg/sctx"
"github.com/ethersphere/bee/pkg/storage" "github.com/ethersphere/bee/pkg/storage"
"github.com/ethersphere/bee/pkg/swarm" "github.com/ethersphere/bee/pkg/swarm"
...@@ -94,12 +94,20 @@ func validateRequest(r *http.Request) (context.Context, error) { ...@@ -94,12 +94,20 @@ func validateRequest(r *http.Request) (context.Context, 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, logger logging.Logger) (swarm.Address, error) { func storeDir(ctx context.Context, reader io.ReadCloser, s storage.Storer, mode storage.ModePut, logger logging.Logger) (swarm.Address, error) {
dirManifest := jsonmanifest.NewManifest() v := ctx.Value(toEncryptContextKey{})
toEncrypt, _ := v.(bool) // default is false
dirManifest, err := manifest.NewDefaultManifest(toEncrypt, s)
if err != nil {
return swarm.ZeroAddress, err
}
// set up HTTP body reader // set up HTTP body reader
tarReader := tar.NewReader(reader) tarReader := tar.NewReader(reader)
defer reader.Close() defer reader.Close()
filesAdded := 0
// iterate through the files in the supplied tar // iterate through the files in the supplied tar
for { for {
fileHeader, err := tarReader.Next() fileHeader, err := tarReader.Next()
...@@ -133,42 +141,54 @@ func storeDir(ctx context.Context, reader io.ReadCloser, s storage.Storer, mode ...@@ -133,42 +141,54 @@ 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)
// create manifest entry for uploaded file // add file entry to dir manifest
headers := http.Header{} err = dirManifest.Add(filePath, manifest.NewEntry(fileReference))
headers.Set("Content-Type", contentType) if err != nil {
fileEntry := jsonmanifest.NewEntry(fileReference, fileName, headers) return swarm.ZeroAddress, fmt.Errorf("add to manifest: %w", err)
}
filesAdded++
}
// add entry to dir manifest // check if files were uploaded through the manifest
dirManifest.Add(filePath, fileEntry) if filesAdded == 0 {
return swarm.ZeroAddress, fmt.Errorf("no files in tar")
} }
// check if files were uploaded by querying manifest length // save manifest
if dirManifest.Length() == 0 { manifestBytesReference, err := dirManifest.Store(ctx, mode)
return swarm.ZeroAddress, fmt.Errorf("no files added from tar") if err != nil {
return swarm.ZeroAddress, fmt.Errorf("store manifest: %w", err)
} }
// upload manifest // store the manifest metadata and get its reference
// first, serialize into byte array m := entry.NewMetadata(manifestBytesReference.String())
b, err := dirManifest.MarshalBinary() m.MimeType = dirManifest.Type()
metadataBytes, err := json.Marshal(m)
if err != nil { if err != nil {
return swarm.ZeroAddress, fmt.Errorf("manifest serialize: %w", err) return swarm.ZeroAddress, fmt.Errorf("metadata marshal: %w", err)
} }
// set up reader for manifest file upload sp := splitter.NewSimpleSplitter(s, mode)
r := bytes.NewReader(b) mr, err := file.SplitWriteAll(ctx, sp, bytes.NewReader(metadataBytes), int64(len(metadataBytes)), toEncrypt)
if err != nil {
return swarm.ZeroAddress, fmt.Errorf("split metadata: %w", err)
}
// then, upload manifest // now join both references (fr, mr) to create an entry and store it
manifestFileInfo := &fileUploadInfo{ e := entry.New(manifestBytesReference, mr)
size: r.Size(), fileEntryBytes, err := e.MarshalBinary()
contentType: ManifestContentType, if err != nil {
reader: r, return swarm.ZeroAddress, fmt.Errorf("entry marshal: %w", err)
} }
manifestReference, err := storeFile(ctx, manifestFileInfo, s, mode)
sp = splitter.NewSimpleSplitter(s, mode)
manifestFileReference, err := file.SplitWriteAll(ctx, sp, bytes.NewReader(fileEntryBytes), int64(len(fileEntryBytes)), toEncrypt)
if err != nil { if err != nil {
return swarm.ZeroAddress, fmt.Errorf("store manifest: %w", err) return swarm.ZeroAddress, fmt.Errorf("split entry: %w", err)
} }
return manifestReference, nil return manifestFileReference, nil
} }
// storeFile uploads the given file and returns its reference // storeFile uploads the given file and returns its reference
......
...@@ -7,16 +7,21 @@ package api_test ...@@ -7,16 +7,21 @@ package api_test
import ( import (
"archive/tar" "archive/tar"
"bytes" "bytes"
"context"
"encoding/json"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"path" "path"
"testing" "testing"
"github.com/ethersphere/bee/pkg/api" "github.com/ethersphere/bee/pkg/api"
"github.com/ethersphere/bee/pkg/collection/entry"
"github.com/ethersphere/bee/pkg/file"
"github.com/ethersphere/bee/pkg/file/joiner"
"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/jsonmanifest" "github.com/ethersphere/bee/pkg/manifest"
"github.com/ethersphere/bee/pkg/storage/mock" "github.com/ethersphere/bee/pkg/storage/mock"
"github.com/ethersphere/bee/pkg/swarm" "github.com/ethersphere/bee/pkg/swarm"
"github.com/ethersphere/bee/pkg/tags" "github.com/ethersphere/bee/pkg/tags"
...@@ -26,8 +31,9 @@ func TestDirs(t *testing.T) { ...@@ -26,8 +31,9 @@ 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 }
storer = mock.NewStorer()
client = newTestServer(t, testServerOptions{ client = newTestServer(t, testServerOptions{
Storer: mock.NewStorer(), Storer: storer,
Tags: tags.NewTags(), Tags: tags.NewTags(),
Logger: logging.New(ioutil.Discard, 5), Logger: logging.New(ioutil.Discard, 5),
}) })
...@@ -82,7 +88,7 @@ func TestDirs(t *testing.T) { ...@@ -82,7 +88,7 @@ func TestDirs(t *testing.T) {
}{ }{
{ {
name: "non-nested files without extension", name: "non-nested files without extension",
expectedHash: "3609d0521d34469ecbffc1d2401ce7a34c7c54bb63e8d23933ef0073015aa9e7", expectedHash: "685f591d0482a57e172aecb7f58babd7eb50fcb8411f875cae5c7b96fa44ff82",
files: []f{ files: []f{
{ {
data: []byte("first file data"), data: []byte("first file data"),
...@@ -106,7 +112,7 @@ func TestDirs(t *testing.T) { ...@@ -106,7 +112,7 @@ func TestDirs(t *testing.T) {
}, },
{ {
name: "nested files with extension", name: "nested files with extension",
expectedHash: "983869d469f0eab1f1bb6c2daeac1fdf476968246410b3001e59e9f2e0236da0", expectedHash: "9e4e53c1764f2379408ffe019c097cbfcb8a0ba93587b52126a4e3e9d5b8556f",
files: []f{ files: []f{
{ {
data: []byte("robots text"), data: []byte("robots text"),
...@@ -142,31 +148,76 @@ func TestDirs(t *testing.T) { ...@@ -142,31 +148,76 @@ func TestDirs(t *testing.T) {
// tar all the test case files // tar all the test case files
tarReader := tarFiles(t, tc.files) tarReader := tarFiles(t, tc.files)
var respBytes []byte
// verify directory tar upload response // verify directory tar upload response
jsonhttptest.Request(t, client, http.MethodPost, dirUploadResource, http.StatusOK, jsonhttptest.Request(t, client, http.MethodPost, dirUploadResource, http.StatusOK,
jsonhttptest.WithRequestBody(tarReader), jsonhttptest.WithRequestBody(tarReader),
jsonhttptest.WithExpectedJSONResponse(api.FileUploadResponse{
Reference: swarm.MustParseHexAddress(tc.expectedHash),
}),
jsonhttptest.WithRequestHeader("Content-Type", api.ContentTypeTar), jsonhttptest.WithRequestHeader("Content-Type", api.ContentTypeTar),
jsonhttptest.WithPutResponseBody(&respBytes),
) )
// create expected manifest read := bytes.NewReader(respBytes)
expectedManifest := jsonmanifest.NewManifest()
for _, file := range tc.files { // get the reference as everytime it will change because of random encryption key
e := jsonmanifest.NewEntry(file.reference, file.name, file.header) var resp api.FileUploadResponse
expectedManifest.Add(path.Join(file.dir, file.name), e) err := json.NewDecoder(read).Decode(&resp)
if err != nil {
t.Fatal(err)
}
if tc.expectedHash != resp.Reference.String() {
t.Fatalf("expected file reference to match %s, got %x", tc.expectedHash, resp.Reference)
} }
b, err := expectedManifest.MarshalBinary() // read manifest metadata
j := joiner.NewSimpleJoiner(storer)
buf := bytes.NewBuffer(nil)
_, err = file.JoinReadAll(context.Background(), j, resp.Reference, buf, false)
if err != nil {
t.Fatal(err)
}
e := &entry.Entry{}
err = e.UnmarshalBinary(buf.Bytes())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// verify directory upload manifest through files api // verify manifest content
jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(tc.expectedHash), http.StatusOK, verifyManifest, err := manifest.NewManifestReference(
jsonhttptest.WithExpectedResponse(b), context.Background(),
manifest.DefaultManifestType,
e.Reference(),
false,
storer,
) )
if err != nil {
t.Fatal(err)
}
// check if each file can be located and read
for _, file := range tc.files {
filePath := path.Join(file.dir, file.name)
entry, err := verifyManifest.Lookup(filePath)
if err != nil {
t.Fatal(err)
}
fileReference := entry.Reference()
if !bytes.Equal(file.reference.Bytes(), fileReference.Bytes()) {
t.Fatalf("expected file reference to match %x, got %x", file.reference, fileReference)
}
jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(fileReference.String()), http.StatusOK,
jsonhttptest.WithExpectedResponse(file.data),
jsonhttptest.WithRequestHeader("Content-Type", file.header.Get("Content-Type")),
)
}
}) })
} }
} }
......
...@@ -277,7 +277,7 @@ func TestRangeRequests(t *testing.T) { ...@@ -277,7 +277,7 @@ func TestRangeRequests(t *testing.T) {
uploadEndpoint: "/dirs", uploadEndpoint: "/dirs",
downloadEndpoint: "/bzz", downloadEndpoint: "/bzz",
filepath: "/ipsum/lorem.txt", filepath: "/ipsum/lorem.txt",
reference: "c1e596eebc9b39fea8f790b6ede4a294bf336e17b0cb7cd64ec54edc5c4ec0e2", reference: "d2b1ab6fb26c1570712ca33efb30f8cbbaa994d5b85e1cf6f782bcae430eabaf",
reader: tarFiles(t, []f{ reader: tarFiles(t, []f{
{ {
data: data, data: data,
......
...@@ -460,7 +460,7 @@ func TestTags(t *testing.T) { ...@@ -460,7 +460,7 @@ func TestTags(t *testing.T) {
name: "binary-file", name: "binary-file",
}}) }})
expectedHash := swarm.MustParseHexAddress("9e5acfbfeb7e074d4c79f5f9922e8a25990dad267d0ea7becaaad07b47fb2a87") expectedHash := swarm.MustParseHexAddress("ebcfbfac0e9a4fa4483491875f9486107a799e54cd832d0aacc59b1125b4b71f")
expectedResponse := api.FileUploadResponse{Reference: expectedHash} expectedResponse := api.FileUploadResponse{Reference: expectedHash}
respHeaders := jsonhttptest.Request(t, client, http.MethodPost, dirResource, http.StatusOK, respHeaders := jsonhttptest.Request(t, client, http.MethodPost, dirResource, http.StatusOK,
......
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package jsonmanifest
import (
"net/http"
"github.com/ethersphere/bee/pkg/manifest"
"github.com/ethersphere/bee/pkg/swarm"
)
// verify jsonEntry implements manifest.Entry.
var _ manifest.Entry = (*jsonEntry)(nil)
// jsonEntry is a JSON representation of a single manifest entry for a jsonManifest.
type jsonEntry struct {
R swarm.Address `json:"reference"`
N string `json:"name"`
H http.Header `json:"header"`
}
// NewEntry creates a new jsonEntry struct and returns it.
func NewEntry(reference swarm.Address, name string, headers http.Header) manifest.Entry {
return &jsonEntry{
R: reference,
N: name,
H: headers,
}
}
// Reference returns the address of the file in the entry.
func (me *jsonEntry) Reference() swarm.Address {
return me.R
}
// Name returns the name of the file in the entry.
func (me *jsonEntry) Name() string {
return me.N
}
// Header returns the HTTP header for the file in the manifest entry.
func (me *jsonEntry) Header() http.Header {
return me.H
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package jsonmanifest_test
import (
"net/http"
"path/filepath"
"reflect"
"testing"
"github.com/ethersphere/bee/pkg/manifest"
"github.com/ethersphere/bee/pkg/manifest/jsonmanifest"
"github.com/ethersphere/bee/pkg/swarm"
"github.com/ethersphere/bee/pkg/swarm/test"
)
var testCases = []testCase{
{
name: "empty-manifest",
entries: nil,
},
{
name: "one-entry",
entries: []e{
{
reference: test.RandomAddress(),
path: "entry-1",
header: http.Header{},
},
},
},
{
name: "two-entries",
entries: []e{
{
reference: test.RandomAddress(),
path: "entry-1.txt",
header: http.Header{"Content-Type": {"text/plain; charset=utf-8"}},
},
{
reference: test.RandomAddress(),
path: "entry-2.png",
header: http.Header{"Content-Type": {"image/png"}},
},
},
},
{
name: "nested-entries",
entries: []e{
{
reference: test.RandomAddress(),
path: "text/robots.txt",
header: http.Header{"Content-Type": {"text/plain; charset=utf-8"}},
},
{
reference: test.RandomAddress(),
path: "img/1.png",
header: http.Header{"Content-Type": {"image/png"}},
},
{
reference: test.RandomAddress(),
path: "img/2.jpg",
header: http.Header{"Content-Type": {"image/jpg"}},
},
{
reference: test.RandomAddress(),
path: "readme.md",
header: http.Header{"Content-Type": {"text/markdown; charset=UTF-8"}},
},
},
},
}
// TestEntries tests the Add, Length and Entry functions.
// This test will add multiple entries to a manifest, checking that they are correctly retrieved each time,
// and that the length of the manifest is as expected.
// It will verify that the manifest length remains unchanged when replacing entries or removing inexistent ones.
// Finally, it will remove all entries in the manifest, checking that they are correctly not found each time,
// and that the length of the manifest is as expected.
func TestEntries(t *testing.T) {
tc := testCases[len(testCases)-1] // get non-trivial test case
m := jsonmanifest.NewManifest()
checkLength(t, m, 0)
// add entries
for i, e := range tc.entries {
_, name := filepath.Split(e.path)
entry := jsonmanifest.NewEntry(e.reference, name, e.header)
m.Add(e.path, entry)
checkLength(t, m, i+1)
checkEntry(t, m, entry, e.path)
}
manifestLen := m.Length()
// replace entry
lastEntry := tc.entries[len(tc.entries)-1]
_, name := filepath.Split(lastEntry.path)
newEntry := jsonmanifest.NewEntry(test.RandomAddress(), name, lastEntry.header)
m.Add(lastEntry.path, newEntry)
checkLength(t, m, manifestLen) // length should not have changed
checkEntry(t, m, newEntry, lastEntry.path)
// remove entries
m.Remove("invalid/path.ext") // try removing inexistent entry
checkLength(t, m, manifestLen) // length should not have changed
for i, e := range tc.entries {
m.Remove(e.path)
entry, err := m.Entry(e.path)
if entry != nil || err != manifest.ErrNotFound {
t.Fatalf("expected path %v not to be present in the manifest, but it was found", e.path)
}
checkLength(t, m, manifestLen-i-1)
}
}
// checkLength verifies that the given manifest length and integer match.
func checkLength(t *testing.T, m manifest.Interface, length int) {
if m.Length() != length {
t.Fatalf("expected length to be %d, but is %d instead", length, m.Length())
}
}
// checkEntry verifies that an entry is equal to the one retrieved from the given manifest and path.
func checkEntry(t *testing.T, m manifest.Interface, entry manifest.Entry, path string) {
re, err := m.Entry(path)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(entry, re) {
t.Fatalf("original and retrieved entry are not equal: %v, %v", entry, re)
}
}
// TestEntryModification verifies that manifest entries are not modifiable from outside of the manifest.
// This test will add a single entry to a manifest, retrieve it, and modify it.
// After, it will re-retrieve that same entry from the manifest, and check that it has not changed.
func TestEntryModification(t *testing.T) {
m := jsonmanifest.NewManifest()
e := jsonmanifest.NewEntry(test.RandomAddress(), "single_entry.png", http.Header{"Content-Type": {"image/png"}})
m.Add("", e)
re, err := m.Entry("")
if err != nil {
t.Fatal(err)
}
re.Header().Add("Content-Type", "text/plain; charset=utf-8") // modify retrieved entry
rre, err := m.Entry("") // re-retrieve entry
if err != nil {
t.Fatal(err)
}
if reflect.DeepEqual(rre, re) {
t.Fatalf("manifest entry %v was unexpectedly modified externally", rre)
}
}
// TestMarshal verifies that created manifests are successfully marshalled and unmarshalled.
// This function wil add all test case entries to a manifest and marshal it.
// After, it will unmarshal the result, and verify that it is equal to the original manifest.
func TestMarshal(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
m := jsonmanifest.NewManifest()
for _, e := range tc.entries {
_, name := filepath.Split(e.path)
entry := jsonmanifest.NewEntry(e.reference, name, e.header)
m.Add(e.path, entry)
}
b, err := m.MarshalBinary()
if err != nil {
t.Fatal(err)
}
um := jsonmanifest.NewManifest()
if err := um.UnmarshalBinary(b); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(m, um) {
t.Fatalf("marshalled and unmarshalled manifests are not equal: %v, %v", m, um)
}
})
}
}
// struct for manifest test cases
type testCase struct {
name string
entries []e // entries to add to manifest
}
// struct for manifest entries for test cases
type e struct {
reference swarm.Address
path string
header http.Header
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package jsonmanifest
import (
"encoding/json"
"sync"
"github.com/ethersphere/bee/pkg/manifest"
)
// verify jsonManifest implements manifest.Interface.
var _ manifest.Interface = (*jsonManifest)(nil)
// jsonManifest is a JSON representation of a manifest.
// It stores manifest entries in a map based on string keys.
type jsonManifest struct {
entriesMu sync.RWMutex // mutex for accessing the entries map
Entries map[string]*jsonEntry `json:"entries,omitempty"`
}
// NewManifest creates a new jsonManifest struct and returns a pointer to it.
func NewManifest() manifest.Interface {
return &jsonManifest{
Entries: make(map[string]*jsonEntry),
}
}
// Add adds a manifest entry to the specified path.
func (m *jsonManifest) Add(path string, entry manifest.Entry) {
m.entriesMu.Lock()
defer m.entriesMu.Unlock()
m.Entries[path] = &jsonEntry{
R: entry.Reference(),
N: entry.Name(),
H: entry.Header(),
}
}
// Remove removes a manifest entry on the specified path.
func (m *jsonManifest) Remove(path string) {
m.entriesMu.Lock()
defer m.entriesMu.Unlock()
delete(m.Entries, path)
}
// Entry returns a manifest entry if one is found in the specified path.
func (m *jsonManifest) Entry(path string) (manifest.Entry, error) {
m.entriesMu.RLock()
defer m.entriesMu.RUnlock()
entry, ok := m.Entries[path]
if !ok {
return nil, manifest.ErrNotFound
}
// return a copy to prevent external modification
return NewEntry(entry.Reference(), entry.Name(), entry.Header().Clone()), nil
}
// Length returns an implementation-specific count of elements in the manifest.
// For jsonManifest, this means the number of all the existing entries.
func (m *jsonManifest) Length() int {
m.entriesMu.RLock()
defer m.entriesMu.RUnlock()
return len(m.Entries)
}
// MarshalBinary implements encoding.BinaryMarshaler.
func (m *jsonManifest) MarshalBinary() ([]byte, error) {
m.entriesMu.RLock()
defer m.entriesMu.RUnlock()
return json.Marshal(m)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
func (m *jsonManifest) UnmarshalBinary(b []byte) error {
m.entriesMu.Lock()
defer m.entriesMu.Unlock()
return json.Unmarshal(b, m)
}
...@@ -5,36 +5,93 @@ ...@@ -5,36 +5,93 @@
package manifest package manifest
import ( import (
"encoding" "context"
"errors" "errors"
"net/http"
"github.com/ethersphere/bee/pkg/storage"
"github.com/ethersphere/bee/pkg/swarm" "github.com/ethersphere/bee/pkg/swarm"
) )
// ErrNotFound is returned when an Entry is not found in the manifest. const DefaultManifestType = ManifestSimpleContentType
var ErrNotFound = errors.New("manifest: not found")
var (
// ErrNotFound is returned when an Entry is not found in the manifest.
ErrNotFound = errors.New("manifest: not found")
// ErrInvalidManifestType is returned when an unknown manifest type
// is provided to the function.
ErrInvalidManifestType = errors.New("manifest: invalid type")
)
// Interface for operations with manifest. // Interface for operations with manifest.
type Interface interface { type Interface interface {
// Type returns manifest implementation type information
Type() string
// Add a manifest entry to the specified path. // Add a manifest entry to the specified path.
Add(string, Entry) Add(string, Entry) error
// Remove a manifest entry on the specified path. // Remove a manifest entry on the specified path.
Remove(string) Remove(string) error
// Entry 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.
Entry(string) (Entry, error) Lookup(string) (Entry, error)
// Length returns an implementation-specific count of elements in the manifest. // Store stores the manifest, returning the resulting address.
Length() int Store(context.Context, storage.ModePut) (swarm.Address, error)
encoding.BinaryMarshaler
encoding.BinaryUnmarshaler
} }
// Entry represents a single manifest entry. // Entry represents a single manifest entry.
type Entry interface { type Entry interface {
// Reference returns the address of the file in the entry. // Reference returns the address of the file.
Reference() swarm.Address Reference() swarm.Address
// Name returns the name of the file in the entry. }
Name() string
// Header returns the HTTP header for the file in the manifest entry. // NewDefaultManifest creates a new manifest with default type.
Header() http.Header func NewDefaultManifest(
encrypted bool,
storer storage.Storer,
) (Interface, error) {
return NewManifest(DefaultManifestType, encrypted, storer)
}
// NewManifest creates a new manifest.
func NewManifest(
manifestType string,
encrypted bool,
storer storage.Storer,
) (Interface, error) {
switch manifestType {
case ManifestSimpleContentType:
return NewSimpleManifest(encrypted, storer)
default:
return nil, ErrInvalidManifestType
}
}
// NewManifestReference loads existing manifest.
func NewManifestReference(
ctx context.Context,
manifestType string,
reference swarm.Address,
encrypted bool,
storer storage.Storer,
) (Interface, error) {
switch manifestType {
case ManifestSimpleContentType:
return NewSimpleManifestReference(ctx, reference, encrypted, storer)
default:
return nil, ErrInvalidManifestType
}
}
type manifestEntry struct {
reference swarm.Address
}
// NewEntry creates a new manifest entry.
func NewEntry(reference swarm.Address) Entry {
return &manifestEntry{
reference: reference,
}
}
func (e *manifestEntry) Reference() swarm.Address {
return e.reference
} }
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package manifest
import (
"bytes"
"context"
"errors"
"fmt"
"github.com/ethersphere/bee/pkg/file"
"github.com/ethersphere/bee/pkg/file/joiner"
"github.com/ethersphere/bee/pkg/file/splitter"
"github.com/ethersphere/bee/pkg/storage"
"github.com/ethersphere/bee/pkg/swarm"
"github.com/ethersphere/manifest/simple"
)
const (
// ManifestSimpleContentType represents content type used for noting that
// specific file should be processed as 'simple' manifest
ManifestSimpleContentType = "application/bzz-manifest-simple+json"
)
type simpleManifest struct {
manifest simple.Manifest
encrypted bool
storer storage.Storer
}
// NewSimpleManifest creates a new simple manifest.
func NewSimpleManifest(
encrypted bool,
storer storage.Storer,
) (Interface, error) {
return &simpleManifest{
manifest: simple.NewManifest(),
encrypted: encrypted,
storer: storer,
}, nil
}
// NewSimpleManifestReference loads existing simple manifest.
func NewSimpleManifestReference(
ctx context.Context,
reference swarm.Address,
encrypted bool,
storer storage.Storer,
) (Interface, error) {
m := &simpleManifest{
manifest: simple.NewManifest(),
encrypted: encrypted,
storer: storer,
}
err := m.load(ctx, reference)
return m, err
}
func (m *simpleManifest) Type() string {
return ManifestSimpleContentType
}
func (m *simpleManifest) Add(path string, entry Entry) error {
e := entry.Reference().String()
return m.manifest.Add(path, e)
}
func (m *simpleManifest) Remove(path string) error {
err := m.manifest.Remove(path)
if err != nil {
if errors.Is(err, simple.ErrNotFound) {
return ErrNotFound
}
return err
}
return nil
}
func (m *simpleManifest) Lookup(path string) (Entry, error) {
n, err := m.manifest.Lookup(path)
if err != nil {
return nil, ErrNotFound
}
address, err := swarm.ParseHexAddress(n.Reference())
if err != nil {
return nil, fmt.Errorf("parse swarm address: %w", err)
}
entry := NewEntry(address)
return entry, nil
}
func (m *simpleManifest) Store(ctx context.Context, mode storage.ModePut) (swarm.Address, error) {
data, err := m.manifest.MarshalBinary()
if err != nil {
return swarm.ZeroAddress, fmt.Errorf("manifest marshal error: %w", err)
}
sp := splitter.NewSimpleSplitter(m.storer, mode)
address, err := file.SplitWriteAll(ctx, sp, bytes.NewReader(data), int64(len(data)), m.encrypted)
if err != nil {
return swarm.ZeroAddress, fmt.Errorf("manifest save error: %w", err)
}
return address, nil
}
func (m *simpleManifest) load(ctx context.Context, reference swarm.Address) error {
j := joiner.NewSimpleJoiner(m.storer)
buf := bytes.NewBuffer(nil)
_, err := file.JoinReadAll(ctx, j, reference, buf, m.encrypted)
if err != nil {
return fmt.Errorf("manifest load error: %w", err)
}
err = m.manifest.UnmarshalBinary(buf.Bytes())
if err != nil {
return fmt.Errorf("manifest unmarshal error: %w", err)
}
return nil
}
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