From 8c3c3782f5b253e4588a0a79c15c8fda702c533a Mon Sep 17 00:00:00 2001
From: Pavel Pogodaev
Date: Fri, 5 May 2023 11:19:35 +0300
Subject: [PATCH] [#30] add object name resolving
Signed-off-by: Pavel Pogodaev
---
.gitignore | 1 +
Makefile | 9 +-
api/layer/tree_service.go | 22 +++
api/tree.go | 17 ++
app.go | 41 +++--
config/config.env | 3 +
config/config.yaml | 4 +
downloader/download.go | 59 ++++++-
downloader/head.go | 14 +-
go.mod | 19 ++-
go.sum | 26 +--
internal/frostfs/services/tree_client_grpc.go | 114 +++++++++++++
.../services/tree_client_grpc_signature.go | 29 ++++
settings.go | 3 +
syncTree.sh | 21 +++
tokens/bearer-token.go | 11 ++
tree/tree.go | 156 ++++++++++++++++++
17 files changed, 507 insertions(+), 42 deletions(-)
create mode 100644 api/layer/tree_service.go
create mode 100644 api/tree.go
create mode 100644 internal/frostfs/services/tree_client_grpc.go
create mode 100644 internal/frostfs/services/tree_client_grpc_signature.go
create mode 100755 syncTree.sh
create mode 100644 tree/tree.go
diff --git a/.gitignore b/.gitignore
index c4a98d8..c1ca465 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ bin
temp
/plugins/
/vendor/
+internal/frostfs/services/tree
.test.env
*~
diff --git a/Makefile b/Makefile
index eb74c3e..aeea3a8 100755
--- a/Makefile
+++ b/Makefile
@@ -15,6 +15,7 @@ METRICS_DUMP_OUT ?= ./metrics-dump.json
BINDIR = bin
DIRS = $(BINDIR)
BINS = $(BINDIR)/frostfs-http-gw
+SYNCDIR = internal/frostfs/services/tree
.PHONY: all $(BINS) $(DIRS) dep docker/ test cover fmt image image-push dirty-image lint docker/lint pre-commit unpre-commit version clean
@@ -27,8 +28,7 @@ PKG_VERSION ?= $(shell echo $(VERSION) | sed "s/^v//" | \
# Make all binaries
all: $(BINS)
-
-$(BINS): $(DIRS) dep
+$(BINS): sync-tree $(DIRS) dep
@echo "⇒ Build $@"
CGO_ENABLED=0 \
go build -v -trimpath \
@@ -39,6 +39,10 @@ $(DIRS):
@echo "⇒ Ensure dir: $@"
@mkdir -p $@
+# Synchronize tree service
+sync-tree:
+ @./syncTree.sh
+
# Pull go dependencies
dep:
@printf "⇒ Download requirements: "
@@ -132,6 +136,7 @@ version:
clean:
rm -rf vendor
rm -rf $(BINDIR)
+ rm -rf $(SYNCDIR)
# Package for Debian
debpackage:
diff --git a/api/layer/tree_service.go b/api/layer/tree_service.go
new file mode 100644
index 0000000..9852257
--- /dev/null
+++ b/api/layer/tree_service.go
@@ -0,0 +1,22 @@
+package layer
+
+import (
+ "context"
+ "errors"
+
+ "git.frostfs.info/TrueCloudLab/frostfs-http-gw/api"
+ cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
+)
+
+// TreeService provide interface to interact with tree service using s3 data models.
+type TreeService interface {
+ GetLatestVersion(ctx context.Context, cnrID *cid.ID, objectName string) (*api.NodeVersion, error)
+}
+
+var (
+ // ErrNodeNotFound is returned from Tree service in case of not found error.
+ ErrNodeNotFound = errors.New("not found")
+
+ // ErrNodeAccessDenied is returned from Tree service in case of access denied error.
+ ErrNodeAccessDenied = errors.New("access denied")
+)
diff --git a/api/tree.go b/api/tree.go
new file mode 100644
index 0000000..4d16cc7
--- /dev/null
+++ b/api/tree.go
@@ -0,0 +1,17 @@
+package api
+
+import (
+ oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
+)
+
+// NodeVersion represent node from tree service.
+type NodeVersion struct {
+ BaseNodeVersion
+ DeleteMarker bool
+}
+
+// BaseNodeVersion is minimal node info from tree service.
+// Basically used for "system" object.
+type BaseNodeVersion struct {
+ OID oid.ID
+}
diff --git a/app.go b/app.go
index 487fdae..a989c90 100644
--- a/app.go
+++ b/app.go
@@ -2,7 +2,6 @@ package main
import (
"context"
- "crypto/ecdsa"
"fmt"
"net/http"
"os"
@@ -14,9 +13,11 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/pkg/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/downloader"
+ "git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/frostfs/services"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/metrics"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/resolver"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/response"
+ "git.frostfs.info/TrueCloudLab/frostfs-http-gw/tree"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/uploader"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/utils"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
@@ -30,6 +31,8 @@ import (
"github.com/spf13/viper"
"github.com/valyala/fasthttp"
"go.uber.org/zap"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
)
type (
@@ -37,6 +40,7 @@ type (
log *zap.Logger
logLevel zap.AtomicLevel
pool *pool.Pool
+ key *keys.PrivateKey
owner *user.ID
cfg *viper.Viper
webServer *fasthttp.Server
@@ -93,7 +97,6 @@ func WithConfig(c *viper.Viper) Option {
func newApp(ctx context.Context, opt ...Option) App {
var (
- key *ecdsa.PrivateKey
err error
)
@@ -120,17 +123,17 @@ func newApp(ctx context.Context, opt ...Option) App {
a.webServer.DisablePreParseMultipartForm = true
a.webServer.StreamRequestBody = a.cfg.GetBool(cfgWebStreamRequestBody)
// -- -- -- -- -- -- -- -- -- -- -- -- -- --
- key, err = getFrostFSKey(a)
+ a.key, err = getFrostFSKey(a)
if err != nil {
a.log.Fatal("failed to get frostfs credentials", zap.Error(err))
}
var owner user.ID
- user.IDFromKey(&owner, key.PublicKey)
+ user.IDFromKey(&owner, a.key.PrivateKey.PublicKey)
a.owner = &owner
var prm pool.InitParameters
- prm.SetKey(key)
+ prm.SetKey(&a.key.PrivateKey)
prm.SetNodeDialTimeout(a.cfg.GetDuration(cfgConTimeout))
prm.SetNodeStreamTimeout(a.cfg.GetDuration(cfgStreamTimeout))
prm.SetHealthcheckTimeout(a.cfg.GetDuration(cfgReqTimeout))
@@ -277,7 +280,7 @@ func remove(list []string, element string) []string {
return list
}
-func getFrostFSKey(a *app) (*ecdsa.PrivateKey, error) {
+func getFrostFSKey(a *app) (*keys.PrivateKey, error) {
walletPath := a.cfg.GetString(cfgWalletPath)
if len(walletPath) == 0 {
@@ -286,7 +289,7 @@ func getFrostFSKey(a *app) (*ecdsa.PrivateKey, error) {
if err != nil {
return nil, err
}
- return &key.PrivateKey, nil
+ return key, nil
}
w, err := wallet.NewWalletFromFile(walletPath)
if err != nil {
@@ -304,7 +307,7 @@ func getFrostFSKey(a *app) (*ecdsa.PrivateKey, error) {
return getKeyFromWallet(w, address, password)
}
-func getKeyFromWallet(w *wallet.Wallet, addrStr string, password *string) (*ecdsa.PrivateKey, error) {
+func getKeyFromWallet(w *wallet.Wallet, addrStr string, password *string) (*keys.PrivateKey, error) {
var addr util.Uint160
var err error
@@ -334,7 +337,7 @@ func getKeyFromWallet(w *wallet.Wallet, addrStr string, password *string) (*ecds
return nil, fmt.Errorf("couldn't decrypt account: %w", err)
}
- return &acc.PrivateKey().PrivateKey, nil
+ return acc.PrivateKey(), nil
}
func (a *app) Wait() {
@@ -351,8 +354,9 @@ func (a *app) setHealthStatus() {
}
func (a *app) Serve(ctx context.Context) {
+ treeClient := a.initTree(ctx)
uploadRoutes := uploader.New(ctx, a.AppParams(), a.settings.Uploader)
- downloadRoutes := downloader.New(ctx, a.AppParams(), a.settings.Downloader)
+ downloadRoutes := downloader.New(ctx, a.AppParams(), a.settings.Downloader, treeClient)
// Configure router.
a.configureRouter(uploadRoutes, downloadRoutes)
@@ -475,8 +479,8 @@ func (a *app) configureRouter(uploadRoutes *uploader.Uploader, downloadRoutes *d
}
r.POST("/upload/{cid}", a.logger(uploadRoutes.Upload))
a.log.Info("added path /upload/{cid}")
- r.GET("/get/{cid}/{oid}", a.logger(downloadRoutes.DownloadByAddress))
- r.HEAD("/get/{cid}/{oid}", a.logger(downloadRoutes.HeadByAddress))
+ r.GET("/get/{cid}/{oid:*}", a.logger(downloadRoutes.DownloadByAddressOrBucketName))
+ r.HEAD("/get/{cid}/{oid:*}", a.logger(downloadRoutes.HeadByAddressOrBucketName))
a.log.Info("added path /get/{cid}/{oid}")
r.GET("/get_by_attribute/{cid}/{attr_key}/{attr_val:*}", a.logger(downloadRoutes.DownloadByAttribute))
r.HEAD("/get_by_attribute/{cid}/{attr_key}/{attr_val:*}", a.logger(downloadRoutes.HeadByAttribute))
@@ -565,6 +569,19 @@ func (a *app) serverIndex(address string) int {
return -1
}
+func (a *app) initTree(ctx context.Context) *tree.Tree {
+ treeServiceEndpoint := a.cfg.GetString(cfgTreeServiceEndpoint)
+ grpcDialOpt := grpc.WithTransportCredentials(insecure.NewCredentials())
+ treeGRPCClient, err := services.NewTreeServiceClientGRPC(ctx, treeServiceEndpoint, a.key, grpcDialOpt)
+ if err != nil {
+ a.log.Fatal("failed to create tree service", zap.Error(err))
+ }
+ treeService := tree.NewTree(treeGRPCClient)
+ a.log.Info("init tree service", zap.String("endpoint", treeServiceEndpoint))
+
+ return treeService
+}
+
func (a *app) initTracing(ctx context.Context) {
instanceID := ""
if len(a.servers) > 0 {
diff --git a/config/config.env b/config/config.env
index 2d5ea94..4dc9bb2 100644
--- a/config/config.env
+++ b/config/config.env
@@ -93,6 +93,9 @@ HTTP_GW_POOL_ERROR_THRESHOLD=100
# Enable zip compression to download files by common prefix.
HTTP_GW_ZIP_COMPRESSION=false
+# Endpoint of the tree service. Must be provided. Can be one of the node address (from the `peers` section).
+HTTP_GW_TREE_SERVICE=grpc://s01.frostfs.devenv:8080
+
HTTP_GW_TRACING_ENABLED=true
HTTP_GW_TRACING_ENDPOINT="localhost:4317"
HTTP_GW_TRACING_EXPORTER="otlp_grpc"
\ No newline at end of file
diff --git a/config/config.yaml b/config/config.yaml
index 510cb43..a71c69d 100644
--- a/config/config.yaml
+++ b/config/config.yaml
@@ -93,6 +93,10 @@ resolve_order:
upload_header:
use_default_timestamp: false # Create timestamp for object if it isn't provided by header.
+# Endpoint of the tree service. Must be provided. Can be one of the node address (from the `peers` section).
+tree:
+ service: 127.0.0.1:8080
+
connect_timeout: 5s # Timeout to dial node.
stream_timeout: 10s # Timeout for individual operations in streaming RPC.
request_timeout: 5s # Timeout to check node health during rebalance.
diff --git a/downloader/download.go b/downloader/download.go
index 7d29a26..b7c242b 100644
--- a/downloader/download.go
+++ b/downloader/download.go
@@ -18,6 +18,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/resolver"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/response"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/tokens"
+ "git.frostfs.info/TrueCloudLab/frostfs-http-gw/tree"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/utils"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
@@ -212,6 +213,7 @@ type Downloader struct {
pool *pool.Pool
containerResolver *resolver.ContainerResolver
settings *Settings
+ tree *tree.Tree
}
// Settings stores reloading parameters, so it has to provide atomic getters and setters.
@@ -228,13 +230,14 @@ func (s *Settings) SetZipCompression(val bool) {
}
// New creates an instance of Downloader using specified options.
-func New(ctx context.Context, params *utils.AppParams, settings *Settings) *Downloader {
+func New(ctx context.Context, params *utils.AppParams, settings *Settings, tree *tree.Tree) *Downloader {
return &Downloader{
appCtx: ctx,
log: params.Logger,
pool: params.Pool,
settings: settings,
containerResolver: params.Resolver,
+ tree: tree,
}
}
@@ -245,9 +248,16 @@ func (d *Downloader) newRequest(ctx *fasthttp.RequestCtx, log *zap.Logger) *requ
}
}
-// DownloadByAddress handles download requests using simple cid/oid format.
-func (d *Downloader) DownloadByAddress(c *fasthttp.RequestCtx) {
- d.byAddress(c, receiveFile)
+// DownloadByAddressOrBucketName handles download requests using simple cid/oid or bucketname/key format.
+func (d *Downloader) DownloadByAddressOrBucketName(c *fasthttp.RequestCtx) {
+ test, _ := c.UserValue("oid").(string)
+ var id oid.ID
+ err := id.DecodeString(test)
+ if err != nil {
+ d.byBucketname(c, receiveFile)
+ } else {
+ d.byAddress(c, receiveFile)
+ }
}
// byAddress is a wrapper for function (e.g. request.headObject, request.receiveFile) that
@@ -290,6 +300,47 @@ func (d *Downloader) byAddress(c *fasthttp.RequestCtx, f func(context.Context, r
f(ctx, *d.newRequest(c, log), d.pool, addr)
}
+// byBucketname is a wrapper for function (e.g. request.headObject, request.receiveFile) that
+// prepares request and object address to it.
+func (d *Downloader) byBucketname(c *fasthttp.RequestCtx, f func(context.Context, request, *pool.Pool, oid.Address)) {
+ var (
+ bucketname = c.UserValue("cid").(string)
+ key = c.UserValue("oid").(string)
+ log = d.log.With(zap.String("bucketname", bucketname), zap.String("key", key))
+ )
+
+ cnrID, err := utils.GetContainerID(d.appCtx, bucketname, d.containerResolver)
+ if err != nil {
+ log.Error("wrong container id", zap.Error(err))
+ response.Error(c, "wrong container id", fasthttp.StatusBadRequest)
+ return
+ }
+
+ ctx, err := tokens.StoreBearerTokenAppCtx(c, d.appCtx)
+ if err != nil {
+ log.Error("could not fetch and store bearer token", zap.Error(err))
+ response.Error(c, "could not fetch and store bearer token: "+err.Error(), fasthttp.StatusBadRequest)
+ return
+ }
+
+ foundOid, err := d.tree.GetLatestVersion(ctx, cnrID, key)
+ if err != nil {
+ log.Error("object wasn't found", zap.Error(err))
+ response.Error(c, "object wasn't found", fasthttp.StatusNotFound)
+ return
+ }
+ if foundOid.DeleteMarker {
+ log.Error("object was deleted")
+ response.Error(c, "object deleted", fasthttp.StatusNotFound)
+ return
+ }
+ var addr oid.Address
+ addr.SetContainer(*cnrID)
+ addr.SetObject(foundOid.OID)
+
+ f(ctx, *d.newRequest(c, log), d.pool, addr)
+}
+
// DownloadByAttribute handles attribute-based download requests.
func (d *Downloader) DownloadByAttribute(c *fasthttp.RequestCtx) {
d.byAttribute(c, receiveFile)
diff --git a/downloader/head.go b/downloader/head.go
index 9745019..a81a275 100644
--- a/downloader/head.go
+++ b/downloader/head.go
@@ -121,9 +121,17 @@ func idsToResponse(resp *fasthttp.Response, obj *object.Object) {
resp.Header.Set(hdrContainerID, cnrID.String())
}
-// HeadByAddress handles head requests using simple cid/oid format.
-func (d *Downloader) HeadByAddress(c *fasthttp.RequestCtx) {
- d.byAddress(c, headObject)
+// HeadByAddressOrBucketName handles head requests using simple cid/oid or bucketname/key format.
+func (d *Downloader) HeadByAddressOrBucketName(c *fasthttp.RequestCtx) {
+ test, _ := c.UserValue("oid").(string)
+ var id oid.ID
+
+ err := id.DecodeString(test)
+ if err != nil {
+ d.byBucketname(c, headObject)
+ } else {
+ d.byAddress(c, headObject)
+ }
}
// HeadByAttribute handles attribute-based head requests.
diff --git a/go.mod b/go.mod
index fe0be0d..21737d4 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.18
require (
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.15.1-0.20230418080822-bd44a3f47b85
+ git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20230505094539-15b4287092bd
github.com/fasthttp/router v1.4.1
github.com/nspcc-dev/neo-go v0.101.0
@@ -18,11 +19,12 @@ require (
go.opentelemetry.io/otel/trace v1.14.0
go.uber.org/atomic v1.10.0
go.uber.org/zap v1.24.0
+ google.golang.org/grpc v1.53.0
+ google.golang.org/protobuf v1.28.1
)
require (
git.frostfs.info/TrueCloudLab/frostfs-contract v0.0.0-20230307110621-19a8ef2d02fb // indirect
- git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0 // indirect
git.frostfs.info/TrueCloudLab/hrw v1.2.0 // indirect
git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0 // indirect
git.frostfs.info/TrueCloudLab/tzhash v1.8.0 // indirect
@@ -56,7 +58,7 @@ require (
github.com/hashicorp/golang-lru v0.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
- github.com/klauspost/compress v1.15.0 // indirect
+ github.com/klauspost/compress v1.16.4 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -95,16 +97,15 @@ require (
go.opentelemetry.io/otel/sdk v1.14.0 // indirect
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
- golang.org/x/crypto v0.4.0 // indirect
+ golang.org/x/crypto v0.8.0 // indirect
golang.org/x/exp v0.0.0-20221227203929-1b447090c38c // indirect
- golang.org/x/net v0.7.0 // indirect
+ golang.org/x/net v0.9.0 // indirect
golang.org/x/sync v0.1.0 // indirect
- golang.org/x/sys v0.5.0 // indirect
- golang.org/x/term v0.5.0 // indirect
- golang.org/x/text v0.7.0 // indirect
+ golang.org/x/sys v0.7.0 // indirect
+ golang.org/x/term v0.7.0 // indirect
+ golang.org/x/text v0.9.0 // indirect
+ golang.org/x/time v0.3.0 // indirect
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
- google.golang.org/grpc v1.53.0 // indirect
- google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/go.sum b/go.sum
index 60ecb9f..90996f3 100644
--- a/go.sum
+++ b/go.sum
@@ -586,8 +586,9 @@ github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6
github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
-github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
+github.com/klauspost/compress v1.16.4 h1:91KN02FnsOYhuunwU4ssRe8lc2JosWmizWa91B5v1PU=
+github.com/klauspost/compress v1.16.4/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -1005,8 +1006,8 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
-golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
+golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
+golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1099,8 +1100,8 @@ golang.org/x/net v0.0.0-20211108170745-6635138e15ea/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
-golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1226,13 +1227,13 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210429154555-c04ba851c2a4/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
+golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1242,15 +1243,16 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
+golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
+golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180318012157-96caea41033d/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/internal/frostfs/services/tree_client_grpc.go b/internal/frostfs/services/tree_client_grpc.go
new file mode 100644
index 0000000..f25588a
--- /dev/null
+++ b/internal/frostfs/services/tree_client_grpc.go
@@ -0,0 +1,114 @@
+package services
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ grpcService "git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/frostfs/services/tree"
+ "git.frostfs.info/TrueCloudLab/frostfs-http-gw/tokens"
+ "git.frostfs.info/TrueCloudLab/frostfs-http-gw/tree"
+ "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
+ "google.golang.org/grpc"
+)
+
+type GetNodeByPathResponseInfoWrapper struct {
+ response *grpcService.GetNodeByPathResponse_Info
+}
+
+func (n GetNodeByPathResponseInfoWrapper) GetMeta() []tree.Meta {
+ res := make([]tree.Meta, len(n.response.Meta))
+ for i, value := range n.response.Meta {
+ res[i] = value
+ }
+ return res
+}
+
+type GetSubTreeResponseBodyWrapper struct {
+ response *grpcService.GetSubTreeResponse_Body
+}
+
+func (n GetSubTreeResponseBodyWrapper) GetMeta() []tree.Meta {
+ res := make([]tree.Meta, len(n.response.Meta))
+ for i, value := range n.response.Meta {
+ res[i] = value
+ }
+ return res
+}
+
+type ServiceClientGRPC struct {
+ key *keys.PrivateKey
+ conn *grpc.ClientConn
+ service grpcService.TreeServiceClient
+}
+
+func NewTreeServiceClientGRPC(ctx context.Context, addr string, key *keys.PrivateKey, grpcOpts ...grpc.DialOption) (*ServiceClientGRPC, error) {
+ conn, err := grpc.Dial(addr, grpcOpts...)
+ if err != nil {
+ return nil, fmt.Errorf("did not connect: %v", err)
+ }
+
+ c := grpcService.NewTreeServiceClient(conn)
+ if _, err = c.Healthcheck(ctx, &grpcService.HealthcheckRequest{}); err != nil {
+ return nil, fmt.Errorf("healthcheck: %w", err)
+ }
+
+ return &ServiceClientGRPC{
+ key: key,
+ conn: conn,
+ service: c,
+ }, nil
+}
+
+func (c *ServiceClientGRPC) GetNodes(ctx context.Context, p *tree.GetNodesParams) ([]tree.NodeResponse, error) {
+ request := &grpcService.GetNodeByPathRequest{
+ Body: &grpcService.GetNodeByPathRequest_Body{
+ ContainerId: p.CnrID[:],
+ TreeId: p.TreeID,
+ Path: p.Path,
+ Attributes: p.Meta,
+ PathAttribute: tree.FileNameKey,
+ LatestOnly: p.LatestOnly,
+ AllAttributes: p.AllAttrs,
+ BearerToken: getBearer(ctx),
+ },
+ }
+
+ if err := c.signRequest(request.Body, func(key, sign []byte) {
+ request.Signature = &grpcService.Signature{
+ Key: key,
+ Sign: sign,
+ }
+ }); err != nil {
+ return nil, err
+ }
+
+ resp, err := c.service.GetNodeByPath(ctx, request)
+ if err != nil {
+ return nil, handleError("failed to get node by path", err)
+ }
+
+ res := make([]tree.NodeResponse, len(resp.GetBody().GetNodes()))
+ for i, info := range resp.GetBody().GetNodes() {
+ res[i] = GetNodeByPathResponseInfoWrapper{info}
+ }
+
+ return res, nil
+}
+
+func getBearer(ctx context.Context) []byte {
+ token, err := tokens.LoadBearerToken(ctx)
+ if err != nil {
+ return nil
+ }
+ return token.Marshal()
+}
+
+func handleError(msg string, err error) error {
+ if strings.Contains(err.Error(), "not found") {
+ return fmt.Errorf("%w: %s", tree.ErrNodeNotFound, err.Error())
+ } else if strings.Contains(err.Error(), "is denied by") {
+ return fmt.Errorf("%w: %s", tree.ErrNodeAccessDenied, err.Error())
+ }
+ return fmt.Errorf("%s: %w", msg, err)
+}
diff --git a/internal/frostfs/services/tree_client_grpc_signature.go b/internal/frostfs/services/tree_client_grpc_signature.go
new file mode 100644
index 0000000..9dd38f9
--- /dev/null
+++ b/internal/frostfs/services/tree_client_grpc_signature.go
@@ -0,0 +1,29 @@
+/*REMOVE THIS AFTER SIGNATURE WILL BE AVAILABLE IN TREE CLIENT FROM FROSTFS NODE*/
+package services
+
+import (
+ crypto "git.frostfs.info/TrueCloudLab/frostfs-crypto"
+ "google.golang.org/protobuf/proto"
+)
+
+func (c *ServiceClientGRPC) signData(buf []byte, f func(key, sign []byte)) error {
+ // crypto package should not be used outside of API libraries (see neofs-node#491).
+ // For now tree service does not include into SDK Client nor SDK Pool, so there is no choice.
+ // When SDK library adopts Tree service client, this should be dropped.
+ sign, err := crypto.Sign(&c.key.PrivateKey, buf)
+ if err != nil {
+ return err
+ }
+
+ f(c.key.PublicKey().Bytes(), sign)
+ return nil
+}
+
+func (c *ServiceClientGRPC) signRequest(requestBody proto.Message, f func(key, sign []byte)) error {
+ buf, err := proto.Marshal(requestBody)
+ if err != nil {
+ return err
+ }
+
+ return c.signData(buf, f)
+}
diff --git a/settings.go b/settings.go
index 19188fd..87c56bc 100644
--- a/settings.go
+++ b/settings.go
@@ -59,6 +59,9 @@ const (
cfgRebalance = "rebalance_timer"
cfgPoolErrorThreshold = "pool_error_threshold"
+ // Grpc path to tree service.
+ cfgTreeServiceEndpoint = "tree.service"
+
// Logger.
cfgLoggerLevel = "logger.level"
diff --git a/syncTree.sh b/syncTree.sh
new file mode 100755
index 0000000..98d16a0
--- /dev/null
+++ b/syncTree.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+mkdir -p internal/frostfs/services/tree 2>/dev/null
+
+REVISION="f07d4158f50ed5c7f44cc0bc224c3d03edf27f3b"
+
+echo "tree service revision ${REVISION}"
+
+# regexp below find all link to source code files which end with ".pb.go" and retrieve the file names
+# we use `[^.]*` as non greedy workaround for `.*`
+FILES=$(curl -s https://git.frostfs.info/TrueCloudLab/frostfs-node/src/commit/${REVISION}/pkg/services/tree | sed -n "s,.*\"/TrueCloudLab/frostfs-node/src/commit/${REVISION}/pkg/services/tree/\([^.]*\.pb\.go\)\".*,\1,p")
+
+for file in $FILES; do
+ if [[ $file == *"frostfs"* ]]; then
+ echo "skip '$file'"
+ continue
+ else
+ echo "sync '$file' in tree service"
+ fi
+ curl -s "https://git.frostfs.info/TrueCloudLab/frostfs-node/raw/commit/${REVISION}/pkg/services/tree/${file}" -o "./internal/frostfs/services/tree/${file}"
+done
\ No newline at end of file
diff --git a/tokens/bearer-token.go b/tokens/bearer-token.go
index 6cd98ca..f43cb85 100644
--- a/tokens/bearer-token.go
+++ b/tokens/bearer-token.go
@@ -60,6 +60,17 @@ func StoreBearerToken(ctx *fasthttp.RequestCtx) error {
return nil
}
+// StoreBearerTokenAppCtx extracts a bearer token from the header or cookie and stores
+// it in the application context.
+func StoreBearerTokenAppCtx(ctx *fasthttp.RequestCtx, appCtx context.Context) (context.Context, error) {
+ tkn, err := fetchBearerToken(ctx)
+ if err != nil {
+ return nil, err
+ }
+ newCtx := context.WithValue(appCtx, bearerTokenKey, tkn)
+ return newCtx, nil
+}
+
// LoadBearerToken returns a bearer token stored in the context given (if it's
// present there).
func LoadBearerToken(ctx context.Context) (*bearer.Token, error) {
diff --git a/tree/tree.go b/tree/tree.go
new file mode 100644
index 0000000..84b6707
--- /dev/null
+++ b/tree/tree.go
@@ -0,0 +1,156 @@
+package tree
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "git.frostfs.info/TrueCloudLab/frostfs-http-gw/api"
+ "git.frostfs.info/TrueCloudLab/frostfs-http-gw/api/layer"
+ cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
+ oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
+)
+
+type (
+ Tree struct {
+ service ServiceClient
+ }
+
+ // ServiceClient is a client to interact with tree service.
+ // Each method must return ErrNodeNotFound or ErrNodeAccessDenied if relevant.
+ ServiceClient interface {
+ GetNodes(ctx context.Context, p *GetNodesParams) ([]NodeResponse, error)
+ }
+
+ treeNode struct {
+ ObjID oid.ID
+ Meta map[string]string
+ }
+
+ GetNodesParams struct {
+ CnrID cid.ID
+ TreeID string
+ Path []string
+ Meta []string
+ LatestOnly bool
+ AllAttrs bool
+ }
+)
+
+var (
+ // ErrNodeNotFound is returned from ServiceClient in case of not found error.
+ ErrNodeNotFound = layer.ErrNodeNotFound
+
+ // ErrNodeAccessDenied is returned from ServiceClient service in case of access denied error.
+ ErrNodeAccessDenied = layer.ErrNodeAccessDenied
+)
+
+const (
+ FileNameKey = "FileName"
+)
+
+const (
+ oidKV = "OID"
+
+ // keys for delete marker nodes.
+ isDeleteMarkerKV = "IsDeleteMarker"
+
+ // versionTree -- ID of a tree with object versions.
+ versionTree = "version"
+
+ separator = "/"
+)
+
+// NewTree creates instance of Tree using provided address and create grpc connection.
+func NewTree(service ServiceClient) *Tree {
+ return &Tree{service: service}
+}
+
+type Meta interface {
+ GetKey() string
+ GetValue() []byte
+}
+
+type NodeResponse interface {
+ GetMeta() []Meta
+}
+
+func newTreeNode(nodeInfo NodeResponse) (*treeNode, error) {
+ treeNode := &treeNode{
+ Meta: make(map[string]string, len(nodeInfo.GetMeta())),
+ }
+
+ for _, kv := range nodeInfo.GetMeta() {
+ switch kv.GetKey() {
+ case oidKV:
+ if err := treeNode.ObjID.DecodeString(string(kv.GetValue())); err != nil {
+ return nil, err
+ }
+ default:
+ treeNode.Meta[kv.GetKey()] = string(kv.GetValue())
+ }
+ }
+
+ return treeNode, nil
+}
+
+func (n *treeNode) Get(key string) (string, bool) {
+ value, ok := n.Meta[key]
+ return value, ok
+}
+
+func (n *treeNode) FileName() (string, bool) {
+ value, ok := n.Meta[FileNameKey]
+ return value, ok
+}
+
+func newNodeVersion(node NodeResponse) (*api.NodeVersion, error) {
+ treeNode, err := newTreeNode(node)
+ if err != nil {
+ return nil, fmt.Errorf("invalid tree node: %w", err)
+ }
+
+ return newNodeVersionFromTreeNode(treeNode), nil
+}
+
+func newNodeVersionFromTreeNode(treeNode *treeNode) *api.NodeVersion {
+ _, isDeleteMarker := treeNode.Get(isDeleteMarkerKV)
+
+ version := &api.NodeVersion{
+ BaseNodeVersion: api.BaseNodeVersion{
+ OID: treeNode.ObjID,
+ },
+ DeleteMarker: isDeleteMarker,
+ }
+
+ return version
+}
+
+func (c *Tree) GetLatestVersion(ctx context.Context, cnrID *cid.ID, objectName string) (*api.NodeVersion, error) {
+ meta := []string{oidKV, isDeleteMarkerKV}
+ path := pathFromName(objectName)
+
+ p := &GetNodesParams{
+ CnrID: *cnrID,
+ TreeID: versionTree,
+ Path: path,
+ Meta: meta,
+ LatestOnly: true,
+ AllAttrs: false,
+ }
+ nodes, err := c.service.GetNodes(ctx, p)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(nodes) == 0 {
+ return nil, layer.ErrNodeNotFound
+ }
+
+ return newNodeVersion(nodes[0])
+}
+
+// pathFromName splits name by '/'.
+func pathFromName(objectName string) []string {
+ return strings.Split(objectName, separator)
+}