Commit 4abf0717 authored by Nemanja Zbiljić's avatar Nemanja Zbiljić Committed by GitHub

Simplify public tags API response (#1148)

- add API endpoint for listing tags from store
- rename 'swarm-tag-uid' header to 'swarm-tag'
- rename field 'stored' to 'processed'
- estimate total number of chunks during upload
parent 11631c51
...@@ -40,7 +40,7 @@ paths: ...@@ -40,7 +40,7 @@ paths:
- Bytes - Bytes
parameters: parameters:
- in: header - in: header
name: swarm-tag-uid name: swarm-tag
schema: schema:
$ref: 'SwarmCommon.yaml#/components/schemas/Uid' $ref: 'SwarmCommon.yaml#/components/schemas/Uid'
required: false required: false
...@@ -140,7 +140,7 @@ paths: ...@@ -140,7 +140,7 @@ paths:
- Chunk - Chunk
parameters: parameters:
- in: header - in: header
name: swarm-tag-uid name: swarm-tag
schema: schema:
$ref: 'SwarmCommon.yaml#/components/schemas/Uid' $ref: 'SwarmCommon.yaml#/components/schemas/Uid'
required: false required: false
...@@ -192,7 +192,7 @@ paths: ...@@ -192,7 +192,7 @@ paths:
required: false required: false
description: Filename description: Filename
- in: header - in: header
name: swarm-tag-uid name: swarm-tag
schema: schema:
$ref: 'SwarmCommon.yaml#/components/schemas/Uid' $ref: 'SwarmCommon.yaml#/components/schemas/Uid'
required: false required: false
...@@ -280,7 +280,7 @@ paths: ...@@ -280,7 +280,7 @@ paths:
- Collection - Collection
parameters: parameters:
- in: header - in: header
name: swarm-tag-uid name: swarm-tag
schema: schema:
$ref: 'SwarmCommon.yaml#/components/schemas/Uid' $ref: 'SwarmCommon.yaml#/components/schemas/Uid'
required: false required: false
...@@ -412,6 +412,23 @@ paths: ...@@ -412,6 +412,23 @@ paths:
description: Default response description: Default response
'/tags': '/tags':
get:
summary: Get list of tags
tags:
- Tag
responses:
'200':
description: List of tags
content:
application/json:
schema:
$ref: 'SwarmCommon.yaml#/components/schemas/TagsList'
'403':
$ref: 'SwarmCommon.yaml#/components/responses/403'
'500':
$ref: 'SwarmCommon.yaml#/components/responses/500'
default:
description: Default response
post: post:
summary: 'Create Tag' summary: 'Create Tag'
tags: tags:
......
...@@ -166,12 +166,24 @@ components: ...@@ -166,12 +166,24 @@ components:
NewTagRequest: NewTagRequest:
type: object type: object
properties: properties:
name:
type: string
address: address:
$ref: '#/components/schemas/SwarmAddress' $ref: '#/components/schemas/SwarmAddress'
NewTagResponse: NewTagResponse:
type: object
properties:
uid:
$ref: '#/components/schemas/Uid'
startedAt:
$ref: '#/components/schemas/DateTime'
total:
type: integer
processed:
type: integer
synced:
type: integer
NewTagDebugResponse:
type: object type: object
properties: properties:
total: total:
...@@ -188,15 +200,19 @@ components: ...@@ -188,15 +200,19 @@ components:
type: integer type: integer
uid: uid:
$ref: '#/components/schemas/Uid' $ref: '#/components/schemas/Uid'
anonymous:
type: boolean
name:
type: string
address: address:
$ref: '#/components/schemas/SwarmAddress' $ref: '#/components/schemas/SwarmAddress'
startedAt: startedAt:
$ref: '#/components/schemas/DateTime' $ref: '#/components/schemas/DateTime'
TagsList:
type: object
properties:
tags:
type: array
items:
$ref: '#/components/schemas/NewTagResponse'
P2PUnderlay: P2PUnderlay:
type: string type: string
example: "/ip4/127.0.0.1/tcp/1634/p2p/16Uiu2HAmTm17toLDaPYzRyjKn27iCB76yjKnJ5DjQXneFmifFvaX" example: "/ip4/127.0.0.1/tcp/1634/p2p/16Uiu2HAmTm17toLDaPYzRyjKn27iCB76yjKnJ5DjQXneFmifFvaX"
......
...@@ -572,4 +572,32 @@ paths: ...@@ -572,4 +572,32 @@ paths:
'500': '500':
$ref: 'SwarmCommon.yaml#/components/responses/500' $ref: 'SwarmCommon.yaml#/components/responses/500'
default: default:
description: Default response description: Default response
\ No newline at end of file
'/tags/{uid}':
get:
summary: 'Get Tag information using Uid'
tags:
- Tag
parameters:
- in: path
name: uid
schema:
$ref: 'SwarmCommon.yaml#/components/schemas/Uid'
required: true
description: Uid
responses:
'200':
description: Tag info
content:
application/json:
schema:
$ref: 'SwarmCommon.yaml#/components/schemas/NewTagDebugResponse'
'400':
$ref: 'SwarmCommon.yaml#/components/responses/400'
'403':
$ref: 'SwarmCommon.yaml#/components/responses/403'
'500':
$ref: 'SwarmCommon.yaml#/components/responses/500'
default:
description: Default response
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"math"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
...@@ -30,7 +31,7 @@ import ( ...@@ -30,7 +31,7 @@ import (
const ( const (
SwarmPinHeader = "Swarm-Pin" SwarmPinHeader = "Swarm-Pin"
SwarmTagUidHeader = "Swarm-Tag-Uid" SwarmTagHeader = "Swarm-Tag"
SwarmEncryptHeader = "Swarm-Encrypt" SwarmEncryptHeader = "Swarm-Encrypt"
SwarmIndexDocumentHeader = "Swarm-Index-Document" SwarmIndexDocumentHeader = "Swarm-Index-Document"
SwarmErrorDocumentHeader = "Swarm-Error-Document" SwarmErrorDocumentHeader = "Swarm-Error-Document"
...@@ -132,9 +133,7 @@ func (s *server) Close() error { ...@@ -132,9 +133,7 @@ func (s *server) Close() error {
func (s *server) getOrCreateTag(tagUid string) (*tags.Tag, bool, error) { func (s *server) getOrCreateTag(tagUid string) (*tags.Tag, bool, error) {
// if tag ID is not supplied, create a new tag // if tag ID is not supplied, create a new tag
if tagUid == "" { if tagUid == "" {
tagName := fmt.Sprintf("unnamed_tag_%d", time.Now().Unix()) tag, err := s.Tags.Create(0)
var err error
tag, err := s.Tags.Create(tagName, 0)
if err != nil { if err != nil {
return nil, false, fmt.Errorf("cannot create tag: %w", err) return nil, false, fmt.Errorf("cannot create tag: %w", err)
} }
...@@ -273,3 +272,33 @@ func requestPipelineFn(s storage.Storer, r *http.Request) pipelineFunc { ...@@ -273,3 +272,33 @@ func requestPipelineFn(s storage.Storer, r *http.Request) pipelineFunc {
return builder.FeedPipeline(ctx, pipe, r, l) return builder.FeedPipeline(ctx, pipe, r, l)
} }
} }
// calculateNumberOfChunks calculates the number of chunks in an arbitrary
// content length.
func calculateNumberOfChunks(contentLength int64, isEncrypted bool) int64 {
if contentLength <= swarm.ChunkSize {
return 1
}
branchingFactor := swarm.Branches
if isEncrypted {
branchingFactor = swarm.EncryptedBranches
}
dataChunks := math.Ceil(float64(contentLength) / float64(swarm.ChunkSize))
totalChunks := dataChunks
intermediate := dataChunks / float64(branchingFactor)
for intermediate > 1 {
totalChunks += math.Ceil(intermediate)
intermediate = intermediate / float64(branchingFactor)
}
return int64(totalChunks) + 1
}
func requestCalculateNumberOfChunks(r *http.Request) int64 {
if !strings.Contains(r.Header.Get(contentTypeHeader), "multipart") && r.ContentLength > 0 {
return calculateNumberOfChunks(r.ContentLength, requestEncrypt(r))
}
return 0
}
...@@ -181,3 +181,40 @@ func TestParseName(t *testing.T) { ...@@ -181,3 +181,40 @@ func TestParseName(t *testing.T) {
}) })
} }
} }
// TestCalculateNumberOfChunks is a unit test for
// the chunk-number-according-to-content-length calculation.
func TestCalculateNumberOfChunks(t *testing.T) {
for _, tc := range []struct{ len, chunks int64 }{
{len: 1000, chunks: 1},
{len: 5000, chunks: 3},
{len: 10000, chunks: 4},
{len: 100000, chunks: 26},
{len: 1000000, chunks: 248},
{len: 325839339210, chunks: 79550620 + 621490 + 4856 + 38 + 1},
} {
res := api.CalculateNumberOfChunks(tc.len, false)
if res != tc.chunks {
t.Fatalf("expected result for %d bytes to be %d got %d", tc.len, tc.chunks, res)
}
}
}
// TestCalculateNumberOfChunksEncrypted is a unit test for
// the chunk-number-according-to-content-length calculation with encryption
// (branching factor=64)
func TestCalculateNumberOfChunksEncrypted(t *testing.T) {
for _, tc := range []struct{ len, chunks int64 }{
{len: 1000, chunks: 1},
{len: 5000, chunks: 3},
{len: 10000, chunks: 4},
{len: 100000, chunks: 26},
{len: 1000000, chunks: 245 + 4 + 1},
{len: 325839339210, chunks: 79550620 + 1242979 + 19422 + 304 + 5 + 1},
} {
res := api.CalculateNumberOfChunks(tc.len, true)
if res != tc.chunks {
t.Fatalf("expected result for %d bytes to be %d got %d", tc.len, tc.chunks, res)
}
}
}
...@@ -12,6 +12,7 @@ import ( ...@@ -12,6 +12,7 @@ import (
"github.com/ethersphere/bee/pkg/jsonhttp" "github.com/ethersphere/bee/pkg/jsonhttp"
"github.com/ethersphere/bee/pkg/sctx" "github.com/ethersphere/bee/pkg/sctx"
"github.com/ethersphere/bee/pkg/swarm" "github.com/ethersphere/bee/pkg/swarm"
"github.com/ethersphere/bee/pkg/tags"
"github.com/ethersphere/bee/pkg/tracing" "github.com/ethersphere/bee/pkg/tracing"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
...@@ -24,7 +25,7 @@ type bytesPostResponse struct { ...@@ -24,7 +25,7 @@ type bytesPostResponse struct {
func (s *server) bytesUploadHandler(w http.ResponseWriter, r *http.Request) { func (s *server) bytesUploadHandler(w http.ResponseWriter, r *http.Request) {
logger := tracing.NewLoggerWithTraceID(r.Context(), s.Logger) logger := tracing.NewLoggerWithTraceID(r.Context(), s.Logger)
tag, created, err := s.getOrCreateTag(r.Header.Get(SwarmTagUidHeader)) tag, created, err := s.getOrCreateTag(r.Header.Get(SwarmTagHeader))
if err != nil { if err != nil {
logger.Debugf("bytes upload: get or create tag: %v", err) logger.Debugf("bytes upload: get or create tag: %v", err)
logger.Error("bytes upload: get or create tag") logger.Error("bytes upload: get or create tag")
...@@ -32,6 +33,19 @@ func (s *server) bytesUploadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -32,6 +33,19 @@ func (s *server) bytesUploadHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
if !created {
// only in the case when tag is sent via header (i.e. not created by this request)
if estimatedTotalChunks := requestCalculateNumberOfChunks(r); estimatedTotalChunks > 0 {
err = tag.IncN(tags.TotalChunks, estimatedTotalChunks)
if err != nil {
s.Logger.Debugf("bytes upload: increment tag: %v", err)
s.Logger.Error("bytes upload: increment tag")
jsonhttp.InternalServerError(w, "increment tag")
return
}
}
}
// Add the tag to the context // Add the tag to the context
ctx := sctx.SetTag(r.Context(), tag) ctx := sctx.SetTag(r.Context(), tag)
...@@ -52,8 +66,8 @@ func (s *server) bytesUploadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -52,8 +66,8 @@ func (s *server) bytesUploadHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
} }
w.Header().Set(SwarmTagUidHeader, fmt.Sprint(tag.Uid)) w.Header().Set(SwarmTagHeader, fmt.Sprint(tag.Uid))
w.Header().Set("Access-Control-Expose-Headers", SwarmTagUidHeader) w.Header().Set("Access-Control-Expose-Headers", SwarmTagHeader)
jsonhttp.OK(w, bytesPostResponse{ jsonhttp.OK(w, bytesPostResponse{
Reference: address, Reference: address,
}) })
......
...@@ -34,7 +34,7 @@ func (s *server) chunkUploadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -34,7 +34,7 @@ func (s *server) chunkUploadHandler(w http.ResponseWriter, r *http.Request) {
err error err error
) )
if h := r.Header.Get(SwarmTagUidHeader); h != "" { if h := r.Header.Get(SwarmTagHeader); h != "" {
tag, err = s.getTag(h) tag, err = s.getTag(h)
if err != nil { if err != nil {
s.Logger.Debugf("chunk upload: get tag: %v", err) s.Logger.Debugf("chunk upload: get tag: %v", err)
...@@ -119,10 +119,10 @@ func (s *server) chunkUploadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -119,10 +119,10 @@ func (s *server) chunkUploadHandler(w http.ResponseWriter, r *http.Request) {
jsonhttp.InternalServerError(w, "increment tag") jsonhttp.InternalServerError(w, "increment tag")
return return
} }
w.Header().Set(SwarmTagUidHeader, fmt.Sprint(tag.Uid)) w.Header().Set(SwarmTagHeader, fmt.Sprint(tag.Uid))
} }
w.Header().Set("Access-Control-Expose-Headers", SwarmTagUidHeader) w.Header().Set("Access-Control-Expose-Headers", SwarmTagHeader)
jsonhttp.OK(w, chunkAddressResponse{Reference: address}) jsonhttp.OK(w, chunkAddressResponse{Reference: address})
} }
......
...@@ -26,6 +26,7 @@ import ( ...@@ -26,6 +26,7 @@ import (
"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/swarm"
"github.com/ethersphere/bee/pkg/tags"
"github.com/ethersphere/bee/pkg/tracing" "github.com/ethersphere/bee/pkg/tracing"
) )
...@@ -51,7 +52,7 @@ func (s *server) dirUploadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -51,7 +52,7 @@ func (s *server) dirUploadHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
tag, created, err := s.getOrCreateTag(r.Header.Get(SwarmTagUidHeader)) tag, created, err := s.getOrCreateTag(r.Header.Get(SwarmTagHeader))
if err != nil { if err != nil {
logger.Debugf("dir upload: get or create tag: %v", err) logger.Debugf("dir upload: get or create tag: %v", err)
logger.Error("dir upload: get or create tag") logger.Error("dir upload: get or create tag")
...@@ -64,7 +65,7 @@ func (s *server) dirUploadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -64,7 +65,7 @@ func (s *server) dirUploadHandler(w http.ResponseWriter, r *http.Request) {
p := requestPipelineFn(s.Storer, r) p := requestPipelineFn(s.Storer, r)
encrypt := requestEncrypt(r) encrypt := requestEncrypt(r)
l := loadsave.New(s.Storer, requestModePut(r), encrypt) l := loadsave.New(s.Storer, requestModePut(r), encrypt)
reference, err := storeDir(ctx, encrypt, r.Body, s.Logger, p, l, r.Header.Get(SwarmIndexDocumentHeader), r.Header.Get(SwarmErrorDocumentHeader)) reference, err := storeDir(ctx, encrypt, r.Body, s.Logger, p, l, r.Header.Get(SwarmIndexDocumentHeader), r.Header.Get(SwarmErrorDocumentHeader), tag, created)
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")
...@@ -80,7 +81,7 @@ func (s *server) dirUploadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -80,7 +81,7 @@ func (s *server) dirUploadHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
} }
w.Header().Set(SwarmTagUidHeader, fmt.Sprint(tag.Uid)) w.Header().Set(SwarmTagHeader, fmt.Sprint(tag.Uid))
jsonhttp.OK(w, fileUploadResponse{ jsonhttp.OK(w, fileUploadResponse{
Reference: reference, Reference: reference,
}) })
...@@ -104,7 +105,7 @@ func validateRequest(r *http.Request) error { ...@@ -104,7 +105,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, encrypt bool, reader io.ReadCloser, log logging.Logger, p pipelineFunc, ls file.LoadSaver, indexFilename string, errorFilename string) (swarm.Address, error) { func storeDir(ctx context.Context, encrypt bool, reader io.ReadCloser, log logging.Logger, p pipelineFunc, ls file.LoadSaver, indexFilename string, errorFilename string, tag *tags.Tag, tagCreated bool) (swarm.Address, error) {
logger := tracing.NewLoggerWithTraceID(ctx, log) logger := tracing.NewLoggerWithTraceID(ctx, log)
dirManifest, err := manifest.NewDefaultManifest(ls, encrypt) dirManifest, err := manifest.NewDefaultManifest(ls, encrypt)
...@@ -159,7 +160,19 @@ func storeDir(ctx context.Context, encrypt bool, reader io.ReadCloser, log loggi ...@@ -159,7 +160,19 @@ func storeDir(ctx context.Context, encrypt bool, reader io.ReadCloser, log loggi
contentType: contentType, contentType: contentType,
reader: tarReader, reader: tarReader,
} }
fileReference, err := storeFile(ctx, fileInfo, p)
if !tagCreated {
// only in the case when tag is sent via header (i.e. not created by this request)
// for each file
if estimatedTotalChunks := calculateNumberOfChunks(fileInfo.size, encrypt); estimatedTotalChunks > 0 {
err = tag.IncN(tags.TotalChunks, estimatedTotalChunks)
if err != nil {
return swarm.ZeroAddress, fmt.Errorf("increment tag: %w", err)
}
}
}
fileReference, err := storeFile(ctx, fileInfo, p, encrypt, tag, tagCreated)
if err != nil { if err != nil {
return swarm.ZeroAddress, fmt.Errorf("store dir file: %w", err) return swarm.ZeroAddress, fmt.Errorf("store dir file: %w", err)
} }
...@@ -195,8 +208,23 @@ func storeDir(ctx context.Context, encrypt bool, reader io.ReadCloser, log loggi ...@@ -195,8 +208,23 @@ func storeDir(ctx context.Context, encrypt bool, reader io.ReadCloser, log loggi
} }
} }
storeSizeFn := []manifest.StoreSizeFunc{}
if !tagCreated {
// only in the case when tag is sent via header (i.e. not created by this request)
// each content that is saved for manifest
storeSizeFn = append(storeSizeFn, func(dataSize int64) error {
if estimatedTotalChunks := calculateNumberOfChunks(dataSize, encrypt); estimatedTotalChunks > 0 {
err = tag.IncN(tags.TotalChunks, estimatedTotalChunks)
if err != nil {
return fmt.Errorf("increment tag: %w", err)
}
}
return nil
})
}
// save manifest // save manifest
manifestBytesReference, err := dirManifest.Store(ctx) manifestBytesReference, err := dirManifest.Store(ctx, storeSizeFn...)
if err != nil { if err != nil {
return swarm.ZeroAddress, fmt.Errorf("store manifest: %w", err) return swarm.ZeroAddress, fmt.Errorf("store manifest: %w", err)
} }
...@@ -209,6 +237,17 @@ func storeDir(ctx context.Context, encrypt bool, reader io.ReadCloser, log loggi ...@@ -209,6 +237,17 @@ func storeDir(ctx context.Context, encrypt bool, reader io.ReadCloser, log loggi
return swarm.ZeroAddress, fmt.Errorf("metadata marshal: %w", err) return swarm.ZeroAddress, fmt.Errorf("metadata marshal: %w", err)
} }
if !tagCreated {
// we have additional chunks:
// - for manifest file metadata (1 or more) -> we use estimation function
// - for manifest file collection entry (1)
estimatedTotalChunks := calculateNumberOfChunks(int64(len(metadataBytes)), encrypt)
err = tag.IncN(tags.TotalChunks, estimatedTotalChunks+1)
if err != nil {
return swarm.ZeroAddress, fmt.Errorf("increment tag: %w", err)
}
}
mr, err := p(ctx, bytes.NewReader(metadataBytes), int64(len(metadataBytes))) mr, err := p(ctx, bytes.NewReader(metadataBytes), int64(len(metadataBytes)))
if err != nil { if err != nil {
return swarm.ZeroAddress, fmt.Errorf("split metadata: %w", err) return swarm.ZeroAddress, fmt.Errorf("split metadata: %w", err)
...@@ -231,7 +270,7 @@ func storeDir(ctx context.Context, encrypt bool, reader io.ReadCloser, log loggi ...@@ -231,7 +270,7 @@ func storeDir(ctx context.Context, encrypt bool, reader io.ReadCloser, log loggi
// storeFile uploads the given file and returns its reference // storeFile uploads the given file and returns its reference
// this function was extracted from `fileUploadHandler` and should eventually replace its current code // this function was extracted from `fileUploadHandler` and should eventually replace its current code
func storeFile(ctx context.Context, fileInfo *fileUploadInfo, p pipelineFunc) (swarm.Address, error) { func storeFile(ctx context.Context, fileInfo *fileUploadInfo, p pipelineFunc, encrypt bool, tag *tags.Tag, tagCreated bool) (swarm.Address, error) {
// first store the file and get its reference // first store the file and get its reference
fr, err := p(ctx, fileInfo.reader, fileInfo.size) fr, err := p(ctx, fileInfo.reader, fileInfo.size)
if err != nil { if err != nil {
...@@ -251,6 +290,17 @@ func storeFile(ctx context.Context, fileInfo *fileUploadInfo, p pipelineFunc) (s ...@@ -251,6 +290,17 @@ func storeFile(ctx context.Context, fileInfo *fileUploadInfo, p pipelineFunc) (s
return swarm.ZeroAddress, fmt.Errorf("metadata marshal: %w", err) return swarm.ZeroAddress, fmt.Errorf("metadata marshal: %w", err)
} }
if !tagCreated {
// here we have additional chunks:
// - for metadata (1 or more) -> we use estimation function
// - for collection entry (1)
estimatedTotalChunks := calculateNumberOfChunks(int64(len(metadataBytes)), encrypt)
err = tag.IncN(tags.TotalChunks, estimatedTotalChunks+1)
if err != nil {
return swarm.ZeroAddress, fmt.Errorf("increment tag: %w", err)
}
}
mr, err := p(ctx, bytes.NewReader(metadataBytes), int64(len(metadataBytes))) mr, err := p(ctx, bytes.NewReader(metadataBytes), int64(len(metadataBytes)))
if err != nil { if err != nil {
return swarm.ZeroAddress, fmt.Errorf("split metadata: %w", err) return swarm.ZeroAddress, fmt.Errorf("split metadata: %w", err)
......
...@@ -15,6 +15,7 @@ type ( ...@@ -15,6 +15,7 @@ type (
FileUploadResponse = fileUploadResponse FileUploadResponse = fileUploadResponse
TagResponse = tagResponse TagResponse = tagResponse
TagRequest = tagRequest TagRequest = tagRequest
ListTagsResponse = listTagsResponse
PinnedChunk = pinnedChunk PinnedChunk = pinnedChunk
ListPinnedChunksResponse = listPinnedChunksResponse ListPinnedChunksResponse = listPinnedChunksResponse
UpdatePinCounter = updatePinCounter UpdatePinCounter = updatePinCounter
...@@ -38,3 +39,7 @@ var ( ...@@ -38,3 +39,7 @@ var (
func (s *Server) ResolveNameOrAddress(str string) (swarm.Address, error) { func (s *Server) ResolveNameOrAddress(str string) (swarm.Address, error) {
return s.resolveNameOrAddress(str) return s.resolveNameOrAddress(str)
} }
func CalculateNumberOfChunks(contentLength int64, isEncrypted bool) int64 {
return calculateNumberOfChunks(contentLength, isEncrypted)
}
...@@ -26,6 +26,7 @@ import ( ...@@ -26,6 +26,7 @@ import (
"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"
"github.com/ethersphere/bee/pkg/tags"
"github.com/ethersphere/bee/pkg/tracing" "github.com/ethersphere/bee/pkg/tracing"
"github.com/ethersphere/langos" "github.com/ethersphere/langos"
"github.com/gorilla/mux" "github.com/gorilla/mux"
...@@ -60,7 +61,7 @@ func (s *server) fileUploadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -60,7 +61,7 @@ func (s *server) fileUploadHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
tag, created, err := s.getOrCreateTag(r.Header.Get(SwarmTagUidHeader)) tag, created, err := s.getOrCreateTag(r.Header.Get(SwarmTagHeader))
if err != nil { if err != nil {
logger.Debugf("file upload: get or create tag: %v", err) logger.Debugf("file upload: get or create tag: %v", err)
logger.Error("file upload: get or create tag") logger.Error("file upload: get or create tag")
...@@ -68,6 +69,19 @@ func (s *server) fileUploadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -68,6 +69,19 @@ func (s *server) fileUploadHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
if !created {
// only in the case when tag is sent via header (i.e. not created by this request)
if estimatedTotalChunks := requestCalculateNumberOfChunks(r); estimatedTotalChunks > 0 {
err = tag.IncN(tags.TotalChunks, estimatedTotalChunks)
if err != nil {
s.Logger.Debugf("file upload: increment tag: %v", err)
s.Logger.Error("file upload: increment tag")
jsonhttp.InternalServerError(w, "increment tag")
return
}
}
}
// Add the tag to the context // Add the tag to the context
ctx := sctx.SetTag(r.Context(), tag) ctx := sctx.SetTag(r.Context(), tag)
...@@ -176,6 +190,23 @@ func (s *server) fileUploadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -176,6 +190,23 @@ func (s *server) fileUploadHandler(w http.ResponseWriter, r *http.Request) {
jsonhttp.InternalServerError(w, "metadata marshal error") jsonhttp.InternalServerError(w, "metadata marshal error")
return return
} }
if !created {
// only in the case when tag is sent via header (i.e. not created by this request)
// here we have additional chunks:
// - for metadata (1 or more) -> we use estimation function
// - for collection entry (1)
estimatedTotalChunks := calculateNumberOfChunks(int64(len(metadataBytes)), requestEncrypt(r))
err = tag.IncN(tags.TotalChunks, estimatedTotalChunks+1)
if err != nil {
s.Logger.Debugf("file upload: increment tag: %v", err)
s.Logger.Error("file upload: increment tag")
jsonhttp.InternalServerError(w, "increment tag")
return
}
}
mr, err := p(ctx, bytes.NewReader(metadataBytes), int64(len(metadataBytes))) mr, err := p(ctx, bytes.NewReader(metadataBytes), int64(len(metadataBytes)))
if err != nil { if err != nil {
logger.Debugf("file upload: metadata store, file %q: %v", fileName, err) logger.Debugf("file upload: metadata store, file %q: %v", fileName, err)
...@@ -210,8 +241,8 @@ func (s *server) fileUploadHandler(w http.ResponseWriter, r *http.Request) { ...@@ -210,8 +241,8 @@ func (s *server) fileUploadHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
w.Header().Set("ETag", fmt.Sprintf("%q", reference.String())) w.Header().Set("ETag", fmt.Sprintf("%q", reference.String()))
w.Header().Set(SwarmTagUidHeader, fmt.Sprint(tag.Uid)) w.Header().Set(SwarmTagHeader, fmt.Sprint(tag.Uid))
w.Header().Set("Access-Control-Expose-Headers", SwarmTagUidHeader) w.Header().Set("Access-Control-Expose-Headers", SwarmTagHeader)
jsonhttp.OK(w, fileUploadResponse{ jsonhttp.OK(w, fileUploadResponse{
Reference: reference, Reference: reference,
}) })
......
...@@ -119,6 +119,7 @@ func (s *server) setupRouting() { ...@@ -119,6 +119,7 @@ func (s *server) setupRouting() {
handle(router, "/tags", web.ChainHandlers( handle(router, "/tags", web.ChainHandlers(
s.gatewayModeForbidEndpointHandler, s.gatewayModeForbidEndpointHandler,
web.FinalHandler(jsonhttp.MethodHandler{ web.FinalHandler(jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.listTagsHandler),
"POST": web.ChainHandlers( "POST": web.ChainHandlers(
jsonhttp.NewMaxBodyBytesHandler(1024), jsonhttp.NewMaxBodyBytesHandler(1024),
web.FinalHandlerFunc(s.createTagHandler), web.FinalHandlerFunc(s.createTagHandler),
......
...@@ -7,7 +7,6 @@ package api ...@@ -7,7 +7,6 @@ package api
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strconv" "strconv"
...@@ -20,35 +19,28 @@ import ( ...@@ -20,35 +19,28 @@ import (
) )
type tagRequest struct { type tagRequest struct {
Name string `json:"name,omitempty"`
Address swarm.Address `json:"address,omitempty"` Address swarm.Address `json:"address,omitempty"`
} }
type tagResponse struct { type tagResponse struct {
Total int64 `json:"total"` Uid uint32 `json:"uid"`
Split int64 `json:"split"` StartedAt time.Time `json:"startedAt"`
Seen int64 `json:"seen"` Total int64 `json:"total"`
Stored int64 `json:"stored"` Processed int64 `json:"processed"`
Sent int64 `json:"sent"` Synced int64 `json:"synced"`
Synced int64 `json:"synced"` }
Uid uint32 `json:"uid"`
Name string `json:"name"` type listTagsResponse struct {
Address swarm.Address `json:"address"` Tags []tagResponse `json:"tags"`
StartedAt time.Time `json:"startedAt"`
} }
func newTagResponse(tag *tags.Tag) tagResponse { func newTagResponse(tag *tags.Tag) tagResponse {
return tagResponse{ return tagResponse{
Total: tag.Total,
Split: tag.Split,
Seen: tag.Seen,
Stored: tag.Stored,
Sent: tag.Sent,
Synced: tag.Synced,
Uid: tag.Uid, Uid: tag.Uid,
Name: tag.Name,
Address: tag.Address,
StartedAt: tag.StartedAt, StartedAt: tag.StartedAt,
Total: tag.Total,
Processed: tag.Stored,
Synced: tag.Seen + tag.Synced,
} }
} }
...@@ -75,11 +67,7 @@ func (s *server) createTagHandler(w http.ResponseWriter, r *http.Request) { ...@@ -75,11 +67,7 @@ func (s *server) createTagHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
if tagr.Name == "" { tag, err := s.Tags.Create(0)
tagr.Name = fmt.Sprintf("unnamed_tag_%d", time.Now().Unix())
}
tag, err := s.Tags.Create(tagr.Name, 0)
if err != nil { if err != nil {
s.Logger.Debugf("create tag: tag create error: %v", err) s.Logger.Debugf("create tag: tag create error: %v", err)
s.Logger.Error("create tag: tag create error") s.Logger.Error("create tag: tag create error")
...@@ -204,3 +192,44 @@ func (s *server) doneSplitHandler(w http.ResponseWriter, r *http.Request) { ...@@ -204,3 +192,44 @@ func (s *server) doneSplitHandler(w http.ResponseWriter, r *http.Request) {
} }
jsonhttp.OK(w, "ok") jsonhttp.OK(w, "ok")
} }
func (s *server) listTagsHandler(w http.ResponseWriter, r *http.Request) {
var (
err error
offset, limit = 0, 100 // default offset is 0, default limit 100
)
if v := r.URL.Query().Get("offset"); v != "" {
offset, err = strconv.Atoi(v)
if err != nil {
s.Logger.Debugf("list tags: parse offset: %v", err)
s.Logger.Errorf("list tags: bad offset")
jsonhttp.BadRequest(w, "bad offset")
}
}
if v := r.URL.Query().Get("limit"); v != "" {
limit, err = strconv.Atoi(v)
if err != nil {
s.Logger.Debugf("list tags: parse limit: %v", err)
s.Logger.Errorf("list tags: bad limit")
jsonhttp.BadRequest(w, "bad limit")
}
}
tagList, err := s.Tags.ListAll(r.Context(), offset, limit)
if err != nil {
s.Logger.Debugf("list tags: listing: %v", err)
s.Logger.Errorf("list tags: listing")
jsonhttp.InternalServerError(w, err)
return
}
tags := make([]tagResponse, len(tagList))
for i, t := range tagList {
tags[i] = newTagResponse(t)
}
jsonhttp.OK(w, listTagsResponse{
Tags: tags,
})
}
...@@ -9,8 +9,8 @@ import ( ...@@ -9,8 +9,8 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"sort"
"strconv" "strconv"
"strings"
"testing" "testing"
"github.com/ethersphere/bee/pkg/logging" "github.com/ethersphere/bee/pkg/logging"
...@@ -41,7 +41,6 @@ func TestTags(t *testing.T) { ...@@ -41,7 +41,6 @@ func TestTags(t *testing.T) {
chunksResource = "/chunks" chunksResource = "/chunks"
tagsResource = "/tags" tagsResource = "/tags"
chunk = testingc.GenerateTestRandomChunk() chunk = testingc.GenerateTestRandomChunk()
someTagName = "file.jpg"
mockStatestore = statestore.NewStateStore() mockStatestore = statestore.NewStateStore()
logger = logging.New(ioutil.Discard, 0) logger = logging.New(ioutil.Discard, 0)
tag = tags.NewTags(mockStatestore, logger) tag = tags.NewTags(mockStatestore, logger)
...@@ -51,30 +50,21 @@ func TestTags(t *testing.T) { ...@@ -51,30 +50,21 @@ func TestTags(t *testing.T) {
}) })
) )
t.Run("create unnamed tag", func(t *testing.T) { // list tags without anything pinned
tr := api.TagResponse{} t.Run("list tags zero", func(t *testing.T) {
jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated, jsonhttptest.Request(t, client, http.MethodGet, tagsResource, http.StatusOK,
jsonhttptest.WithJSONRequestBody(api.TagRequest{}), jsonhttptest.WithExpectedJSONResponse(api.ListTagsResponse{
jsonhttptest.WithUnmarshalJSONResponse(&tr), Tags: []api.TagResponse{},
}),
) )
if !strings.Contains(tr.Name, "unnamed_tag_") {
t.Fatalf("expected tag name to contain %s but is %s instead", "unnamed_tag_", tr.Name)
}
}) })
t.Run("create tag with name", func(t *testing.T) { t.Run("create tag", func(t *testing.T) {
tr := api.TagResponse{} tr := api.TagResponse{}
jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated, jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated,
jsonhttptest.WithJSONRequestBody(api.TagRequest{ jsonhttptest.WithJSONRequestBody(api.TagRequest{}),
Name: someTagName,
}),
jsonhttptest.WithUnmarshalJSONResponse(&tr), jsonhttptest.WithUnmarshalJSONResponse(&tr),
) )
if tr.Name != someTagName {
t.Fatalf("expected tag name to be %s but is %s instead", someTagName, tr.Name)
}
}) })
t.Run("create tag with invalid id", func(t *testing.T) { t.Run("create tag with invalid id", func(t *testing.T) {
...@@ -84,7 +74,7 @@ func TestTags(t *testing.T) { ...@@ -84,7 +74,7 @@ func TestTags(t *testing.T) {
Message: "cannot get tag", Message: "cannot get tag",
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
}), }),
jsonhttptest.WithRequestHeader(api.SwarmTagUidHeader, "invalid_id.jpg"), // the value should be uint32 jsonhttptest.WithRequestHeader(api.SwarmTagHeader, "invalid_id.jpg"), // the value should be uint32
) )
}) })
...@@ -110,16 +100,10 @@ func TestTags(t *testing.T) { ...@@ -110,16 +100,10 @@ func TestTags(t *testing.T) {
// create a tag using the API // create a tag using the API
tr := api.TagResponse{} tr := api.TagResponse{}
jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated, jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated,
jsonhttptest.WithJSONRequestBody(api.TagResponse{ jsonhttptest.WithJSONRequestBody(api.TagRequest{}),
Name: someTagName,
}),
jsonhttptest.WithUnmarshalJSONResponse(&tr), jsonhttptest.WithUnmarshalJSONResponse(&tr),
) )
if tr.Name != someTagName {
t.Fatalf("sent tag name %s does not match received tag name %s", someTagName, tr.Name)
}
_ = jsonhttptest.Request(t, client, http.MethodPost, chunksResource, http.StatusOK, _ = jsonhttptest.Request(t, client, http.MethodPost, chunksResource, http.StatusOK,
jsonhttptest.WithRequestBody(bytes.NewReader(chunk.Data())), jsonhttptest.WithRequestBody(bytes.NewReader(chunk.Data())),
jsonhttptest.WithExpectedJSONResponse(api.ChunkAddressResponse{Reference: chunk.Address()}), jsonhttptest.WithExpectedJSONResponse(api.ChunkAddressResponse{Reference: chunk.Address()}),
...@@ -128,13 +112,49 @@ func TestTags(t *testing.T) { ...@@ -128,13 +112,49 @@ func TestTags(t *testing.T) {
rcvdHeaders := jsonhttptest.Request(t, client, http.MethodPost, chunksResource, http.StatusOK, rcvdHeaders := jsonhttptest.Request(t, client, http.MethodPost, chunksResource, http.StatusOK,
jsonhttptest.WithRequestBody(bytes.NewReader(chunk.Data())), jsonhttptest.WithRequestBody(bytes.NewReader(chunk.Data())),
jsonhttptest.WithExpectedJSONResponse(api.ChunkAddressResponse{Reference: chunk.Address()}), jsonhttptest.WithExpectedJSONResponse(api.ChunkAddressResponse{Reference: chunk.Address()}),
jsonhttptest.WithRequestHeader(api.SwarmTagUidHeader, strconv.FormatUint(uint64(tr.Uid), 10)), jsonhttptest.WithRequestHeader(api.SwarmTagHeader, strconv.FormatUint(uint64(tr.Uid), 10)),
) )
isTagFoundInResponse(t, rcvdHeaders, &tr) isTagFoundInResponse(t, rcvdHeaders, &tr)
tagValueTest(t, tr.Uid, 1, 1, 1, 0, 0, 0, swarm.ZeroAddress, client) tagValueTest(t, tr.Uid, 1, 1, 1, 0, 0, 0, swarm.ZeroAddress, client)
}) })
t.Run("list tags", func(t *testing.T) {
// list all current tags
var resp api.ListTagsResponse
jsonhttptest.Request(t, client, http.MethodGet, tagsResource, http.StatusOK,
jsonhttptest.WithUnmarshalJSONResponse(&resp),
)
// create 2 new tags
tRes1 := api.TagResponse{}
jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated,
jsonhttptest.WithJSONRequestBody(api.TagRequest{}),
jsonhttptest.WithUnmarshalJSONResponse(&tRes1),
)
tRes2 := api.TagResponse{}
jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated,
jsonhttptest.WithJSONRequestBody(api.TagRequest{}),
jsonhttptest.WithUnmarshalJSONResponse(&tRes2),
)
expectedTags := []api.TagResponse{
tRes1,
tRes2,
}
expectedTags = append(expectedTags, resp.Tags...)
sort.Slice(expectedTags, func(i, j int) bool { return expectedTags[i].Uid < expectedTags[j].Uid })
// check if listing returns expected tags
jsonhttptest.Request(t, client, http.MethodGet, tagsResource, http.StatusOK,
jsonhttptest.WithExpectedJSONResponse(api.ListTagsResponse{
Tags: expectedTags,
}),
)
})
t.Run("delete tag error", func(t *testing.T) { t.Run("delete tag error", func(t *testing.T) {
// try to delete invalid tag // try to delete invalid tag
jsonhttptest.Request(t, client, http.MethodDelete, tagsResource+"/foobar", http.StatusBadRequest, jsonhttptest.Request(t, client, http.MethodDelete, tagsResource+"/foobar", http.StatusBadRequest,
...@@ -157,9 +177,7 @@ func TestTags(t *testing.T) { ...@@ -157,9 +177,7 @@ func TestTags(t *testing.T) {
// create a tag through API // create a tag through API
tRes := api.TagResponse{} tRes := api.TagResponse{}
jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated, jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated,
jsonhttptest.WithJSONRequestBody(api.TagResponse{ jsonhttptest.WithJSONRequestBody(api.TagRequest{}),
Name: someTagName,
}),
jsonhttptest.WithUnmarshalJSONResponse(&tRes), jsonhttptest.WithUnmarshalJSONResponse(&tRes),
) )
...@@ -201,9 +219,7 @@ func TestTags(t *testing.T) { ...@@ -201,9 +219,7 @@ func TestTags(t *testing.T) {
// create a tag through API // create a tag through API
tRes := api.TagResponse{} tRes := api.TagResponse{}
jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated, jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated,
jsonhttptest.WithJSONRequestBody(api.TagResponse{ jsonhttptest.WithJSONRequestBody(api.TagRequest{}),
Name: someTagName,
}),
jsonhttptest.WithUnmarshalJSONResponse(&tRes), jsonhttptest.WithUnmarshalJSONResponse(&tRes),
) )
tagId := tRes.Uid tagId := tRes.Uid
...@@ -214,7 +230,7 @@ func TestTags(t *testing.T) { ...@@ -214,7 +230,7 @@ func TestTags(t *testing.T) {
// upload content with tag // upload content with tag
jsonhttptest.Request(t, client, http.MethodPost, chunksResource, http.StatusOK, jsonhttptest.Request(t, client, http.MethodPost, chunksResource, http.StatusOK,
jsonhttptest.WithRequestBody(bytes.NewReader(chunk.Data())), jsonhttptest.WithRequestBody(bytes.NewReader(chunk.Data())),
jsonhttptest.WithRequestHeader(api.SwarmTagUidHeader, fmt.Sprint(tagId)), jsonhttptest.WithRequestHeader(api.SwarmTagHeader, fmt.Sprint(tagId)),
) )
// call done split // call done split
...@@ -256,7 +272,7 @@ func TestTags(t *testing.T) { ...@@ -256,7 +272,7 @@ func TestTags(t *testing.T) {
jsonhttptest.WithRequestHeader("Content-Type", "application/octet-stream"), jsonhttptest.WithRequestHeader("Content-Type", "application/octet-stream"),
) )
tagId, err := strconv.Atoi(respHeaders.Get(api.SwarmTagUidHeader)) tagId, err := strconv.Atoi(respHeaders.Get(api.SwarmTagHeader))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -278,7 +294,7 @@ func TestTags(t *testing.T) { ...@@ -278,7 +294,7 @@ func TestTags(t *testing.T) {
jsonhttptest.WithRequestHeader("Content-Type", api.ContentTypeTar), jsonhttptest.WithRequestHeader("Content-Type", api.ContentTypeTar),
) )
tagId, err := strconv.Atoi(respHeaders.Get(api.SwarmTagUidHeader)) tagId, err := strconv.Atoi(respHeaders.Get(api.SwarmTagHeader))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -289,17 +305,12 @@ func TestTags(t *testing.T) { ...@@ -289,17 +305,12 @@ func TestTags(t *testing.T) {
// create a tag using the API // create a tag using the API
tr := api.TagResponse{} tr := api.TagResponse{}
jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated, jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated,
jsonhttptest.WithJSONRequestBody(api.TagResponse{ jsonhttptest.WithJSONRequestBody(api.TagRequest{}),
Name: someTagName,
}),
jsonhttptest.WithUnmarshalJSONResponse(&tr), jsonhttptest.WithUnmarshalJSONResponse(&tr),
) )
if tr.Name != someTagName {
t.Fatalf("sent tag name %s does not match received tag name %s", someTagName, tr.Name)
}
sentHeaders := make(http.Header) sentHeaders := make(http.Header)
sentHeaders.Set(api.SwarmTagUidHeader, strconv.FormatUint(uint64(tr.Uid), 10)) sentHeaders.Set(api.SwarmTagHeader, strconv.FormatUint(uint64(tr.Uid), 10))
g := mockbytes.New(0, mockbytes.MockTypeStandard).WithModulus(255) g := mockbytes.New(0, mockbytes.MockTypeStandard).WithModulus(255)
dataChunk, err := g.SequentialBytes(swarm.ChunkSize) dataChunk, err := g.SequentialBytes(swarm.ChunkSize)
...@@ -318,7 +329,7 @@ func TestTags(t *testing.T) { ...@@ -318,7 +329,7 @@ func TestTags(t *testing.T) {
jsonhttptest.WithExpectedJSONResponse(fileUploadResponse{ jsonhttptest.WithExpectedJSONResponse(fileUploadResponse{
Reference: rootAddress, Reference: rootAddress,
}), }),
jsonhttptest.WithRequestHeader(api.SwarmTagUidHeader, strconv.FormatUint(uint64(tr.Uid), 10)), jsonhttptest.WithRequestHeader(api.SwarmTagHeader, strconv.FormatUint(uint64(tr.Uid), 10)),
) )
id := isTagFoundInResponse(t, rcvdHeaders, nil) id := isTagFoundInResponse(t, rcvdHeaders, nil)
...@@ -330,7 +341,7 @@ func TestTags(t *testing.T) { ...@@ -330,7 +341,7 @@ func TestTags(t *testing.T) {
if tagToVerify.Uid != tr.Uid { if tagToVerify.Uid != tr.Uid {
t.Fatalf("expected tag id to be %d but is %d", tagToVerify.Uid, tr.Uid) t.Fatalf("expected tag id to be %d but is %d", tagToVerify.Uid, tr.Uid)
} }
tagValueTest(t, id, 3, 3, 1, 0, 0, 0, swarm.ZeroAddress, client) tagValueTest(t, id, 3, 3, 1, 0, 0, 3, swarm.ZeroAddress, client)
}) })
} }
...@@ -339,7 +350,7 @@ func TestTags(t *testing.T) { ...@@ -339,7 +350,7 @@ func TestTags(t *testing.T) {
func isTagFoundInResponse(t *testing.T, headers http.Header, tr *api.TagResponse) uint32 { func isTagFoundInResponse(t *testing.T, headers http.Header, tr *api.TagResponse) uint32 {
t.Helper() t.Helper()
idStr := headers.Get(api.SwarmTagUidHeader) idStr := headers.Get(api.SwarmTagHeader)
if idStr == "" { if idStr == "" {
t.Fatalf("could not find tag id header in chunk upload response") t.Fatalf("could not find tag id header in chunk upload response")
} }
...@@ -363,26 +374,13 @@ func tagValueTest(t *testing.T, id uint32, split, stored, seen, sent, synced, to ...@@ -363,26 +374,13 @@ func tagValueTest(t *testing.T, id uint32, split, stored, seen, sent, synced, to
jsonhttptest.WithUnmarshalJSONResponse(&tag), jsonhttptest.WithUnmarshalJSONResponse(&tag),
) )
if tag.Split != split { if tag.Processed != stored {
t.Errorf("tag split count mismatch. got %d want %d", tag.Split, split) t.Errorf("tag processed count mismatch. got %d want %d", tag.Processed, stored)
} }
if tag.Stored != stored { if tag.Synced != seen+synced {
t.Errorf("tag stored count mismatch. got %d want %d", tag.Stored, stored) t.Errorf("tag synced count mismatch. got %d want %d (seen: %d, synced: %d)", tag.Synced, seen+synced, seen, synced)
}
if tag.Seen != seen {
t.Errorf("tag seen count mismatch. got %d want %d", tag.Seen, seen)
}
if tag.Sent != sent {
t.Errorf("tag sent count mismatch. got %d want %d", tag.Sent, sent)
}
if tag.Synced != synced {
t.Errorf("tag synced count mismatch. got %d want %d", tag.Synced, synced)
} }
if tag.Total != total { if tag.Total != total {
t.Errorf("tag total count mismatch. got %d want %d", tag.Total, total) t.Errorf("tag total count mismatch. got %d want %d", tag.Total, total)
} }
if !tag.Address.Equal(address) {
t.Errorf("address mismatch: expected %s got %s", address.String(), tag.Address.String())
}
} }
...@@ -25,6 +25,7 @@ type ( ...@@ -25,6 +25,7 @@ type (
SwapCashoutResponse = swapCashoutResponse SwapCashoutResponse = swapCashoutResponse
SwapCashoutStatusResponse = swapCashoutStatusResponse SwapCashoutStatusResponse = swapCashoutStatusResponse
SwapCashoutStatusResult = swapCashoutStatusResult SwapCashoutStatusResult = swapCashoutStatusResult
TagResponse = tagResponse
) )
var ( var (
......
...@@ -145,6 +145,10 @@ func (s *server) setupRouting() { ...@@ -145,6 +145,10 @@ func (s *server) setupRouting() {
}) })
} }
router.Handle("/tags/{id}", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.getTagHandler),
})
baseRouter.Handle("/", web.ChainHandlers( baseRouter.Handle("/", web.ChainHandlers(
httpaccess.NewHTTPAccessLogHandler(s.Logger, logrus.InfoLevel, s.Tracer, "debug api access"), httpaccess.NewHTTPAccessLogHandler(s.Logger, logrus.InfoLevel, s.Tracer, "debug api access"),
handlers.CompressHandler, handlers.CompressHandler,
......
// Copyright 2021 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 debugapi
import (
"errors"
"net/http"
"strconv"
"time"
"github.com/ethersphere/bee/pkg/jsonhttp"
"github.com/ethersphere/bee/pkg/swarm"
"github.com/ethersphere/bee/pkg/tags"
"github.com/gorilla/mux"
)
type tagResponse struct {
Total int64 `json:"total"`
Split int64 `json:"split"`
Seen int64 `json:"seen"`
Stored int64 `json:"stored"`
Sent int64 `json:"sent"`
Synced int64 `json:"synced"`
Uid uint32 `json:"uid"`
Address swarm.Address `json:"address"`
StartedAt time.Time `json:"startedAt"`
}
func newTagResponse(tag *tags.Tag) tagResponse {
return tagResponse{
Total: tag.Total,
Split: tag.Split,
Seen: tag.Seen,
Stored: tag.Stored,
Sent: tag.Sent,
Synced: tag.Synced,
Uid: tag.Uid,
Address: tag.Address,
StartedAt: tag.StartedAt,
}
}
func (s *server) getTagHandler(w http.ResponseWriter, r *http.Request) {
idStr := mux.Vars(r)["id"]
id, err := strconv.Atoi(idStr)
if err != nil {
s.Logger.Debugf("get tag: parse id %s: %v", idStr, err)
s.Logger.Error("get tag: parse id")
jsonhttp.BadRequest(w, "invalid id")
return
}
tag, err := s.Tags.Get(uint32(id))
if err != nil {
if errors.Is(err, tags.ErrNotFound) {
s.Logger.Debugf("get tag: tag not present: %v, id %s", err, idStr)
s.Logger.Error("get tag: tag not present")
jsonhttp.NotFound(w, "tag not present")
return
}
s.Logger.Debugf("get tag: tag %v: %v", idStr, err)
s.Logger.Errorf("get tag: %v", idStr)
jsonhttp.InternalServerError(w, "cannot get tag")
return
}
w.Header().Set("Cache-Control", "no-cache, private, max-age=0")
jsonhttp.OK(w, newTagResponse(tag))
}
// Copyright 2021 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 debugapi_test
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"testing"
"github.com/ethersphere/bee/pkg/debugapi"
"github.com/ethersphere/bee/pkg/jsonhttp/jsonhttptest"
"github.com/ethersphere/bee/pkg/logging"
statestore "github.com/ethersphere/bee/pkg/statestore/mock"
"github.com/ethersphere/bee/pkg/storage"
"github.com/ethersphere/bee/pkg/storage/mock"
testingc "github.com/ethersphere/bee/pkg/storage/testing"
"github.com/ethersphere/bee/pkg/swarm"
"github.com/ethersphere/bee/pkg/tags"
)
func tagsWithIdResource(id uint32) string { return fmt.Sprintf("/tags/%d", id) }
func TestTags(t *testing.T) {
var (
logger = logging.New(ioutil.Discard, 0)
chunk = testingc.GenerateTestRandomChunk()
mockStorer = mock.NewStorer()
mockStatestore = statestore.NewStateStore()
tagsStore = tags.NewTags(mockStatestore, logger)
testServer = newTestServer(t, testServerOptions{
Storer: mockStorer,
Tags: tagsStore,
})
)
_, err := mockStorer.Put(context.Background(), storage.ModePutUpload, chunk)
if err != nil {
t.Fatal(err)
}
t.Run("all", func(t *testing.T) {
tag, err := tagsStore.Create(0)
if err != nil {
t.Fatal(err)
}
_ = tag.Inc(tags.StateSplit)
_ = tag.Inc(tags.StateStored)
_ = tag.Inc(tags.StateSeen)
_ = tag.Inc(tags.StateSent)
_ = tag.Inc(tags.StateSynced)
_, err = tag.DoneSplit(chunk.Address())
if err != nil {
t.Fatal(err)
}
tagValueTest(t, tag.Uid, 1, 1, 1, 1, 1, 1, chunk.Address(), testServer.Client)
})
}
func tagValueTest(t *testing.T, id uint32, split, stored, seen, sent, synced, total int64, address swarm.Address, client *http.Client) {
t.Helper()
tag := debugapi.TagResponse{}
jsonhttptest.Request(t, client, http.MethodGet, tagsWithIdResource(id), http.StatusOK,
jsonhttptest.WithUnmarshalJSONResponse(&tag),
)
if tag.Split != split {
t.Errorf("tag split count mismatch. got %d want %d", tag.Split, split)
}
if tag.Stored != stored {
t.Errorf("tag stored count mismatch. got %d want %d", tag.Stored, stored)
}
if tag.Seen != seen {
t.Errorf("tag seen count mismatch. got %d want %d", tag.Seen, seen)
}
if tag.Sent != sent {
t.Errorf("tag sent count mismatch. got %d want %d", tag.Sent, sent)
}
if tag.Synced != synced {
t.Errorf("tag synced count mismatch. got %d want %d", tag.Synced, synced)
}
if tag.Total != total {
t.Errorf("tag total count mismatch. got %d want %d", tag.Total, total)
}
if !tag.Address.Equal(address) {
t.Errorf("address mismatch: expected %s got %s", address.String(), tag.Address.String())
}
}
...@@ -40,7 +40,7 @@ func TestModeSetSyncNormalTag(t *testing.T) { ...@@ -40,7 +40,7 @@ func TestModeSetSyncNormalTag(t *testing.T) {
logger := logging.New(ioutil.Discard, 0) logger := logging.New(ioutil.Discard, 0)
db := newTestDB(t, &Options{Tags: tags.NewTags(mockStatestore, logger)}) db := newTestDB(t, &Options{Tags: tags.NewTags(mockStatestore, logger)})
tag, err := db.tags.Create("test", 1) tag, err := db.tags.Create(1)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
......
...@@ -27,6 +27,10 @@ var ( ...@@ -27,6 +27,10 @@ var (
ErrMissingReference = errors.New("manifest: missing reference") ErrMissingReference = errors.New("manifest: missing reference")
) )
// StoreSizeFunc is a callback on every content size that will be stored by
// the Store function.
type StoreSizeFunc func(int64) error
// Interface for operations with manifest. // Interface for operations with manifest.
type Interface interface { type Interface interface {
// Type returns manifest implementation type information // Type returns manifest implementation type information
...@@ -40,7 +44,7 @@ type Interface interface { ...@@ -40,7 +44,7 @@ type Interface interface {
// HasPrefix tests whether the specified prefix path exists. // HasPrefix tests whether the specified prefix path exists.
HasPrefix(context.Context, string) (bool, error) HasPrefix(context.Context, string) (bool, error)
// Store stores the manifest, returning the resulting address. // Store stores the manifest, returning the resulting address.
Store(context.Context) (swarm.Address, error) Store(context.Context, ...StoreSizeFunc) (swarm.Address, error)
// IterateAddresses is used to iterate over chunks addresses for // IterateAddresses is used to iterate over chunks addresses for
// the manifest. // the manifest.
IterateAddresses(context.Context, swarm.AddressIterFunc) error IterateAddresses(context.Context, swarm.AddressIterFunc) error
......
...@@ -107,8 +107,18 @@ func (m *mantarayManifest) HasPrefix(ctx context.Context, prefix string) (bool, ...@@ -107,8 +107,18 @@ func (m *mantarayManifest) HasPrefix(ctx context.Context, prefix string) (bool,
return m.trie.HasPrefix(ctx, p, m.ls) return m.trie.HasPrefix(ctx, p, m.ls)
} }
func (m *mantarayManifest) Store(ctx context.Context) (swarm.Address, error) { func (m *mantarayManifest) Store(ctx context.Context, storeSizeFn ...StoreSizeFunc) (swarm.Address, error) {
err := m.trie.Save(ctx, m.ls) var ls mantaray.LoadSaver
if len(storeSizeFn) > 0 {
ls = &mantarayLoadSaver{
ls: m.ls,
storeSizeFn: storeSizeFn,
}
} else {
ls = m.ls
}
err := m.trie.Save(ctx, ls)
if err != nil { if err != nil {
return swarm.ZeroAddress, fmt.Errorf("manifest save error: %w", err) return swarm.ZeroAddress, fmt.Errorf("manifest save error: %w", err)
} }
...@@ -159,3 +169,24 @@ func (m *mantarayManifest) IterateAddresses(ctx context.Context, fn swarm.Addres ...@@ -159,3 +169,24 @@ func (m *mantarayManifest) IterateAddresses(ctx context.Context, fn swarm.Addres
return nil return nil
} }
type mantarayLoadSaver struct {
ls file.LoadSaver
storeSizeFn []StoreSizeFunc
}
func (ls *mantarayLoadSaver) Load(ctx context.Context, ref []byte) ([]byte, error) {
return ls.ls.Load(ctx, ref)
}
func (ls *mantarayLoadSaver) Save(ctx context.Context, data []byte) ([]byte, error) {
dataLen := int64(len(data))
for i := range ls.storeSizeFn {
err := ls.storeSizeFn[i](dataLen)
if err != nil {
return nil, fmt.Errorf("manifest store size func: %w", err)
}
}
return ls.ls.Save(ctx, data)
}
...@@ -88,12 +88,22 @@ func (m *simpleManifest) HasPrefix(_ context.Context, prefix string) (bool, erro ...@@ -88,12 +88,22 @@ func (m *simpleManifest) HasPrefix(_ context.Context, prefix string) (bool, erro
return m.manifest.HasPrefix(prefix), nil return m.manifest.HasPrefix(prefix), nil
} }
func (m *simpleManifest) Store(ctx context.Context) (swarm.Address, error) { func (m *simpleManifest) Store(ctx context.Context, storeSizeFn ...StoreSizeFunc) (swarm.Address, error) {
data, err := m.manifest.MarshalBinary() data, err := m.manifest.MarshalBinary()
if err != nil { if err != nil {
return swarm.ZeroAddress, fmt.Errorf("manifest marshal error: %w", err) return swarm.ZeroAddress, fmt.Errorf("manifest marshal error: %w", err)
} }
if len(storeSizeFn) > 0 {
dataLen := int64(len(data))
for i := range storeSizeFn {
err = storeSizeFn[i](dataLen)
if err != nil {
return swarm.ZeroAddress, fmt.Errorf("manifest store size func: %w", err)
}
}
}
ref, err := m.ls.Save(ctx, data) ref, err := m.ls.Save(ctx, data)
if err != nil { if err != nil {
return swarm.ZeroAddress, fmt.Errorf("manifest save error: %w", err) return swarm.ZeroAddress, fmt.Errorf("manifest save error: %w", err)
......
...@@ -82,7 +82,7 @@ func TestSendChunkToSyncWithTag(t *testing.T) { ...@@ -82,7 +82,7 @@ func TestSendChunkToSyncWithTag(t *testing.T) {
mtags, p, storer := createPusher(t, triggerPeer, pushSyncService, mock.WithClosestPeer(closestPeer)) mtags, p, storer := createPusher(t, triggerPeer, pushSyncService, mock.WithClosestPeer(closestPeer))
defer storer.Close() defer storer.Close()
ta, err := mtags.Create("test", 1) ta, err := mtags.Create(1)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
......
...@@ -111,7 +111,7 @@ func TestPushChunkToClosest(t *testing.T) { ...@@ -111,7 +111,7 @@ func TestPushChunkToClosest(t *testing.T) {
psPivot, storerPivot, pivotTags, pivotAccounting := createPushSyncNode(t, pivotNode, recorder, nil, mock.WithClosestPeer(closestPeer)) psPivot, storerPivot, pivotTags, pivotAccounting := createPushSyncNode(t, pivotNode, recorder, nil, mock.WithClosestPeer(closestPeer))
defer storerPivot.Close() defer storerPivot.Close()
ta, err := pivotTags.Create("test", 1) ta, err := pivotTags.Create(1)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -230,7 +230,7 @@ func TestPushChunkToNextClosest(t *testing.T) { ...@@ -230,7 +230,7 @@ func TestPushChunkToNextClosest(t *testing.T) {
) )
defer storerPivot.Close() defer storerPivot.Close()
ta, err := pivotTags.Create("test", 1) ta, err := pivotTags.Create(1)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
......
...@@ -19,6 +19,7 @@ const ( ...@@ -19,6 +19,7 @@ const (
SpanSize = 8 SpanSize = 8
SectionSize = 32 SectionSize = 32
Branches = 128 Branches = 128
EncryptedBranches = Branches / 2
BmtBranches = 128 BmtBranches = 128
ChunkSize = SectionSize * Branches ChunkSize = SectionSize * Branches
HashSize = 32 HashSize = 32
......
...@@ -60,7 +60,6 @@ type Tag struct { ...@@ -60,7 +60,6 @@ type Tag struct {
Synced int64 // number of chunks synced with proof Synced int64 // number of chunks synced with proof
Uid uint32 // a unique identifier for this tag Uid uint32 // a unique identifier for this tag
Name string // a name tag for this tag
Address swarm.Address // the associated swarm hash for this tag Address swarm.Address // the associated swarm hash for this tag
StartedAt time.Time // tag started to calculate ETA StartedAt time.Time // tag started to calculate ETA
...@@ -73,10 +72,9 @@ type Tag struct { ...@@ -73,10 +72,9 @@ type Tag struct {
} }
// NewTag creates a new tag, and returns it // NewTag creates a new tag, and returns it
func NewTag(ctx context.Context, uid uint32, s string, total int64, tracer *tracing.Tracer, stateStore storage.StateStorer, logger logging.Logger) *Tag { func NewTag(ctx context.Context, uid uint32, total int64, tracer *tracing.Tracer, stateStore storage.StateStorer, logger logging.Logger) *Tag {
t := &Tag{ t := &Tag{
Uid: uid, Uid: uid,
Name: s,
StartedAt: time.Now(), StartedAt: time.Now(),
Total: total, Total: total,
stateStore: stateStore, stateStore: stateStore,
...@@ -102,7 +100,7 @@ func (t *Tag) FinishRootSpan() { ...@@ -102,7 +100,7 @@ func (t *Tag) FinishRootSpan() {
} }
// IncN increments the count for a state // IncN increments the count for a state
func (t *Tag) IncN(state State, n int) error { func (t *Tag) IncN(state State, n int64) error {
var v *int64 var v *int64
switch state { switch state {
case TotalChunks: case TotalChunks:
...@@ -118,7 +116,7 @@ func (t *Tag) IncN(state State, n int) error { ...@@ -118,7 +116,7 @@ func (t *Tag) IncN(state State, n int) error {
case StateSynced: case StateSynced:
v = &t.Synced v = &t.Synced
} }
atomic.AddInt64(v, int64(n)) atomic.AddInt64(v, n)
// check if syncing is over and persist the tag // check if syncing is over and persist the tag
if state == StateSynced { if state == StateSynced {
...@@ -259,7 +257,6 @@ func (tag *Tag) MarshalBinary() (data []byte, err error) { ...@@ -259,7 +257,6 @@ func (tag *Tag) MarshalBinary() (data []byte, err error) {
n = binary.PutVarint(intBuffer, int64(len(tag.Address.Bytes()))) n = binary.PutVarint(intBuffer, int64(len(tag.Address.Bytes())))
buffer = append(buffer, intBuffer[:n]...) buffer = append(buffer, intBuffer[:n]...)
buffer = append(buffer, tag.Address.Bytes()...) buffer = append(buffer, tag.Address.Bytes()...)
buffer = append(buffer, []byte(tag.Name)...)
return buffer, nil return buffer, nil
} }
...@@ -288,7 +285,6 @@ func (tag *Tag) UnmarshalBinary(buffer []byte) error { ...@@ -288,7 +285,6 @@ func (tag *Tag) UnmarshalBinary(buffer []byte) error {
if t > 0 { if t > 0 {
tag.Address = swarm.NewAddress(buffer[:t]) tag.Address = swarm.NewAddress(buffer[:t])
} }
tag.Name = string(buffer[t:])
return nil return nil
} }
......
...@@ -176,8 +176,7 @@ func TestTagsMultipleConcurrentIncrementsSyncMap(t *testing.T) { ...@@ -176,8 +176,7 @@ func TestTagsMultipleConcurrentIncrementsSyncMap(t *testing.T) {
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
wg.Add(10 * 5 * n) wg.Add(10 * 5 * n)
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
s := string([]byte{uint8(i)}) tag, err := ts.Create(int64(n))
tag, err := ts.Create(s, int64(n))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -223,7 +222,7 @@ func TestTagsMultipleConcurrentIncrementsSyncMap(t *testing.T) { ...@@ -223,7 +222,7 @@ func TestTagsMultipleConcurrentIncrementsSyncMap(t *testing.T) {
func TestMarshallingWithAddr(t *testing.T) { func TestMarshallingWithAddr(t *testing.T) {
mockStatestore := statestore.NewStateStore() mockStatestore := statestore.NewStateStore()
logger := logging.New(ioutil.Discard, 0) logger := logging.New(ioutil.Discard, 0)
tg := NewTag(context.Background(), 111, "test/tag", 10, nil, mockStatestore, logger) tg := NewTag(context.Background(), 111, 10, nil, mockStatestore, logger)
tg.Address = swarm.NewAddress([]byte{0, 1, 2, 3, 4, 5, 6}) tg.Address = swarm.NewAddress([]byte{0, 1, 2, 3, 4, 5, 6})
for _, f := range allStates { for _, f := range allStates {
...@@ -248,10 +247,6 @@ func TestMarshallingWithAddr(t *testing.T) { ...@@ -248,10 +247,6 @@ func TestMarshallingWithAddr(t *testing.T) {
t.Fatalf("tag uids not equal. want %d got %d", tg.Uid, unmarshalledTag.Uid) t.Fatalf("tag uids not equal. want %d got %d", tg.Uid, unmarshalledTag.Uid)
} }
if unmarshalledTag.Name != tg.Name {
t.Fatalf("tag names not equal. want %s got %s", tg.Name, unmarshalledTag.Name)
}
for _, state := range allStates { for _, state := range allStates {
uv, tv := unmarshalledTag.Get(state), tg.Get(state) uv, tv := unmarshalledTag.Get(state), tg.Get(state)
if uv != tv { if uv != tv {
...@@ -260,7 +255,7 @@ func TestMarshallingWithAddr(t *testing.T) { ...@@ -260,7 +255,7 @@ func TestMarshallingWithAddr(t *testing.T) {
} }
if unmarshalledTag.TotalCounter() != tg.TotalCounter() { if unmarshalledTag.TotalCounter() != tg.TotalCounter() {
t.Fatalf("tag names not equal. want %d got %d", tg.TotalCounter(), unmarshalledTag.TotalCounter()) t.Fatalf("tag total counters not equal. want %d got %d", tg.TotalCounter(), unmarshalledTag.TotalCounter())
} }
if len(unmarshalledTag.Address.Bytes()) != len(tg.Address.Bytes()) { if len(unmarshalledTag.Address.Bytes()) != len(tg.Address.Bytes()) {
...@@ -276,7 +271,7 @@ func TestMarshallingWithAddr(t *testing.T) { ...@@ -276,7 +271,7 @@ func TestMarshallingWithAddr(t *testing.T) {
func TestMarshallingNoAddr(t *testing.T) { func TestMarshallingNoAddr(t *testing.T) {
mockStatestore := statestore.NewStateStore() mockStatestore := statestore.NewStateStore()
logger := logging.New(ioutil.Discard, 0) logger := logging.New(ioutil.Discard, 0)
tg := NewTag(context.Background(), 111, "test/tag", 10, nil, mockStatestore, logger) tg := NewTag(context.Background(), 111, 10, nil, mockStatestore, logger)
for _, f := range allStates { for _, f := range allStates {
err := tg.Inc(f) err := tg.Inc(f)
if err != nil { if err != nil {
...@@ -299,10 +294,6 @@ func TestMarshallingNoAddr(t *testing.T) { ...@@ -299,10 +294,6 @@ func TestMarshallingNoAddr(t *testing.T) {
t.Fatalf("tag uids not equal. want %d got %d", tg.Uid, unmarshalledTag.Uid) t.Fatalf("tag uids not equal. want %d got %d", tg.Uid, unmarshalledTag.Uid)
} }
if unmarshalledTag.Name != tg.Name {
t.Fatalf("tag names not equal. want %s got %s", tg.Name, unmarshalledTag.Name)
}
for _, state := range allStates { for _, state := range allStates {
uv, tv := unmarshalledTag.Get(state), tg.Get(state) uv, tv := unmarshalledTag.Get(state), tg.Get(state)
if uv != tv { if uv != tv {
...@@ -311,7 +302,7 @@ func TestMarshallingNoAddr(t *testing.T) { ...@@ -311,7 +302,7 @@ func TestMarshallingNoAddr(t *testing.T) {
} }
if unmarshalledTag.TotalCounter() != tg.TotalCounter() { if unmarshalledTag.TotalCounter() != tg.TotalCounter() {
t.Fatalf("tag names not equal. want %d got %d", tg.TotalCounter(), unmarshalledTag.TotalCounter()) t.Fatalf("tag total counters not equal. want %d got %d", tg.TotalCounter(), unmarshalledTag.TotalCounter())
} }
if len(unmarshalledTag.Address.Bytes()) != len(tg.Address.Bytes()) { if len(unmarshalledTag.Address.Bytes()) != len(tg.Address.Bytes()) {
......
...@@ -22,6 +22,7 @@ import ( ...@@ -22,6 +22,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"math/rand" "math/rand"
"sort"
"strconv" "strconv"
"sync" "sync"
"time" "time"
...@@ -31,6 +32,10 @@ import ( ...@@ -31,6 +32,10 @@ import (
"github.com/ethersphere/bee/pkg/swarm" "github.com/ethersphere/bee/pkg/swarm"
) )
const (
maxPage = 1000 // hard limit of page size
)
var ( var (
TagUidFunc = rand.Uint32 TagUidFunc = rand.Uint32
ErrNotFound = errors.New("tag not found") ErrNotFound = errors.New("tag not found")
...@@ -52,10 +57,10 @@ func NewTags(stateStore storage.StateStorer, logger logging.Logger) *Tags { ...@@ -52,10 +57,10 @@ func NewTags(stateStore storage.StateStorer, logger logging.Logger) *Tags {
} }
} }
// Create creates a new tag, stores it by the name and returns it // Create creates a new tag, stores it by the UID and returns it
// it returns an error if the tag with this name already exists // it returns an error if the tag with this UID already exists
func (ts *Tags) Create(s string, total int64) (*Tag, error) { func (ts *Tags) Create(total int64) (*Tag, error) {
t := NewTag(context.Background(), TagUidFunc(), s, total, nil, ts.stateStore, ts.logger) t := NewTag(context.Background(), TagUidFunc(), total, nil, ts.stateStore, ts.logger)
if _, loaded := ts.tags.LoadOrStore(t.Uid, t); loaded { if _, loaded := ts.tags.LoadOrStore(t.Uid, t); loaded {
return nil, errExists return nil, errExists
...@@ -157,6 +162,79 @@ func (ts *Tags) UnmarshalJSON(value []byte) error { ...@@ -157,6 +162,79 @@ func (ts *Tags) UnmarshalJSON(value []byte) error {
return err return err
} }
func (ts *Tags) ListAll(ctx context.Context, offset, limit int) (t []*Tag, err error) {
if limit > maxPage {
limit = maxPage
}
// range sync.Map first
allTags := ts.All()
sort.Slice(allTags, func(i, j int) bool { return allTags[i].Uid < allTags[j].Uid })
for _, tag := range allTags {
if offset > 0 {
offset--
continue
}
t = append(t, tag)
limit--
if limit == 0 {
break
}
}
if limit == 0 {
return
}
// and then from statestore
err = ts.stateStore.Iterate("tags_", func(key, value []byte) (stop bool, err error) {
if offset > 0 {
offset--
return false, nil
}
var ta *Tag
ta, err = decodeTagValueFromStore(value)
if err != nil {
return true, err
}
if _, ok := ts.tags.Load(ta.Uid); ok {
// tag was already returned from sync.Map
return false, nil
}
t = append(t, ta)
limit--
if limit == 0 {
return true, nil
}
return false, nil
})
return t, err
}
func decodeTagValueFromStore(value []byte) (*Tag, error) {
var data []byte
err := json.Unmarshal(value, &data)
if err != nil {
return nil, err
}
var ta Tag
err = ta.UnmarshalBinary(data)
if err != nil {
return nil, err
}
return &ta, nil
}
// getTagFromStore get a given tag from the state store. // getTagFromStore get a given tag from the state store.
func (ts *Tags) getTagFromStore(uid uint32) (*Tag, error) { func (ts *Tags) getTagFromStore(uid uint32) (*Tag, error) {
key := "tags_" + strconv.Itoa(int(uid)) key := "tags_" + strconv.Itoa(int(uid))
......
...@@ -17,7 +17,9 @@ ...@@ -17,7 +17,9 @@
package tags package tags
import ( import (
"context"
"io/ioutil" "io/ioutil"
"sort"
"testing" "testing"
"github.com/ethersphere/bee/pkg/logging" "github.com/ethersphere/bee/pkg/logging"
...@@ -29,10 +31,10 @@ func TestAll(t *testing.T) { ...@@ -29,10 +31,10 @@ func TestAll(t *testing.T) {
mockStatestore := statestore.NewStateStore() mockStatestore := statestore.NewStateStore()
logger := logging.New(ioutil.Discard, 0) logger := logging.New(ioutil.Discard, 0)
ts := NewTags(mockStatestore, logger) ts := NewTags(mockStatestore, logger)
if _, err := ts.Create("1", 1); err != nil { if _, err := ts.Create(1); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if _, err := ts.Create("2", 1); err != nil { if _, err := ts.Create(1); err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -50,7 +52,7 @@ func TestAll(t *testing.T) { ...@@ -50,7 +52,7 @@ func TestAll(t *testing.T) {
t.Fatalf("expected tag 1 Total to be 1 got %d", n) t.Fatalf("expected tag 1 Total to be 1 got %d", n)
} }
if _, err := ts.Create("3", 1); err != nil { if _, err := ts.Create(1); err != nil {
t.Fatal(err) t.Fatal(err)
} }
all = ts.All() all = ts.All()
...@@ -60,11 +62,83 @@ func TestAll(t *testing.T) { ...@@ -60,11 +62,83 @@ func TestAll(t *testing.T) {
} }
} }
func TestListAll(t *testing.T) {
mockStatestore := statestore.NewStateStore()
logger := logging.New(ioutil.Discard, 0)
ts1 := NewTags(mockStatestore, logger)
// create few tags
for i := 0; i < 5; i++ {
if _, err := ts1.Create(1); err != nil {
t.Fatal(err)
}
}
// tags are from sync.Map
tagList1, err := ts1.ListAll(context.Background(), 0, 5)
if err != nil {
t.Fatal(err)
}
if len(tagList1) != 5 {
t.Fatalf("want %d tags but got %d", 5, len(tagList1))
}
// save all returned tags to statestore
for _, tag := range tagList1 {
err = tag.saveTag()
if err != nil {
t.Fatal(err)
}
}
// use new tags object
ts2 := NewTags(mockStatestore, logger)
// create few more tags in new tags object
for i := 0; i < 5; i++ {
if _, err := ts2.Create(1); err != nil {
t.Fatal(err)
}
}
// first tags are from sync.Map
tagList2, err := ts2.ListAll(context.Background(), 0, 5)
if err != nil {
t.Fatal(err)
}
if len(tagList2) != 5 {
t.Fatalf("want %d tags but got %d", 5, len(tagList2))
}
// now tags are returned from statestore
tagList3, err := ts2.ListAll(context.Background(), 5, 5)
if err != nil {
t.Fatal(err)
}
if len(tagList3) != 5 {
t.Fatalf("want %d tags but got %d", 5, len(tagList2))
}
// where they are not sorted
sort.Slice(tagList3, func(i, j int) bool { return tagList3[i].Uid < tagList3[j].Uid })
// and are the same as ones returned from first tags object
for i := range tagList3 {
if tagList1[i].Uid != tagList3[i].Uid {
t.Fatalf("expected tag %d, but got %d", tagList1[i].Uid, tagList3[i].Uid)
}
}
}
func TestPersistence(t *testing.T) { func TestPersistence(t *testing.T) {
mockStatestore := statestore.NewStateStore() mockStatestore := statestore.NewStateStore()
logger := logging.New(ioutil.Discard, 0) logger := logging.New(ioutil.Discard, 0)
ts := NewTags(mockStatestore, logger) ts := NewTags(mockStatestore, logger)
ta, err := ts.Create("one", 1) ta, err := ts.Create(1)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
......
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