diff --git a/cmd/filenode.go b/cmd/filenode.go index 7d8ada8..384c105 100644 --- a/cmd/filenode.go +++ b/cmd/filenode.go @@ -34,6 +34,7 @@ import ( "github.com/anyproto/any-sync-filenode/filenode" "github.com/anyproto/any-sync-filenode/index" "github.com/anyproto/any-sync-filenode/redisprovider" + "github.com/anyproto/any-sync-filenode/stat" // import this to keep govvv in go.mod on mod tidy _ "github.com/ahmetb/govvv/integration-test/app-different-package/mypkg" @@ -106,6 +107,7 @@ func main() { func Bootstrap(a *app.App) { a.Register(account.New()). + Register(stat.New()). Register(metric.New()). Register(nodeconfsource.New()). Register(nodeconfstore.New()). diff --git a/filenode/filenode.go b/filenode/filenode.go index cc56410..97c936f 100644 --- a/filenode/filenode.go +++ b/filenode/filenode.go @@ -231,17 +231,34 @@ func (fn *fileNode) SpaceInfo(ctx context.Context, spaceId string) (info *filepr return } -func (fn *fileNode) AccountInfo(ctx context.Context) (info *fileproto.AccountInfoResponse, err error) { - info = &fileproto.AccountInfoResponse{} +func (fn *fileNode) BatchAccountInfo(ctx context.Context, identities []string) ([]*fileproto.AccountInfoResponse, error) { + accountInfos := make([]*fileproto.AccountInfoResponse, 0, len(identities)) + for _, identity := range identities { + accountInfo, err := fn.accountInfo(ctx, identity) + if err != nil { + return nil, err + } + accountInfos = append(accountInfos, accountInfo) + } + return accountInfos, nil +} +func (fn *fileNode) AccountInfo(ctx context.Context, identity string) (*fileproto.AccountInfoResponse, error) { + return fn.accountInfo(ctx, identity) +} + +func (fn *fileNode) AccountInfoCtx(ctx context.Context) (info *fileproto.AccountInfoResponse, err error) { identity, err := peer.CtxPubKey(ctx) if err != nil { return nil, fileprotoerr.ErrForbidden } - groupId := identity.Account() + return fn.accountInfo(ctx, groupId) +} - groupInfo, err := fn.index.GroupInfo(ctx, groupId) +func (fn *fileNode) accountInfo(ctx context.Context, identity string) (info *fileproto.AccountInfoResponse, err error) { + info = &fileproto.AccountInfoResponse{} + groupInfo, err := fn.index.GroupInfo(ctx, identity) if err != nil { return nil, err } @@ -250,7 +267,7 @@ func (fn *fileNode) AccountInfo(ctx context.Context) (info *fileproto.AccountInf info.LimitBytes = groupInfo.Limit info.AccountLimitBytes = groupInfo.AccountLimit for _, spaceId := range groupInfo.SpaceIds { - spaceInfo, err := fn.spaceInfo(ctx, index.Key{GroupId: groupId, SpaceId: spaceId}, groupInfo) + spaceInfo, err := fn.spaceInfo(ctx, index.Key{GroupId: identity, SpaceId: spaceId}, groupInfo) if err != nil { return nil, err } diff --git a/filenode/rpchandler.go b/filenode/rpchandler.go index 5746350..b8c6e6d 100644 --- a/filenode/rpchandler.go +++ b/filenode/rpchandler.go @@ -239,7 +239,7 @@ func (r rpcHandler) AccountInfo(ctx context.Context, req *fileproto.AccountInfoR zap.Error(err), ) }() - if resp, err = r.f.AccountInfo(ctx); err != nil { + if resp, err = r.f.AccountInfoCtx(ctx); err != nil { return } return diff --git a/stat/identity.go b/stat/identity.go new file mode 100644 index 0000000..0998757 --- /dev/null +++ b/stat/identity.go @@ -0,0 +1,86 @@ +package stat + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/anyproto/any-sync/app" + "github.com/anyproto/any-sync/commonfile/fileproto" +) + +const CName = "stat.identity" + +type accountInfoProvider interface { + AccountInfo(ctx context.Context, identity string) (*fileproto.AccountInfoResponse, error) + BatchAccountInfo(ctx context.Context, identities []string) ([]*fileproto.AccountInfoResponse, error) +} + +type Stat interface { + app.ComponentRunnable +} + +func New() Stat { + return &identityStat{} +} + +type identityStat struct { + accountInfoProvider accountInfoProvider +} + +func (i *identityStat) Init(a *app.App) (err error) { + i.accountInfoProvider = app.MustComponent[accountInfoProvider](a) + return +} + +func (i *identityStat) Name() (name string) { + return CName +} + +func (i *identityStat) Run(ctx context.Context) (err error) { + http.HandleFunc("/stat/identity/{identity}", func(writer http.ResponseWriter, request *http.Request) { + identity := request.PathValue("identity") + if identity == "" { + http.Error(writer, "identity is empty", http.StatusBadRequest) + return + } + accountInfo, err := i.accountInfoProvider.AccountInfo(request.Context(), identity) + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + return + } + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusOK) + err = json.NewEncoder(writer).Encode(accountInfo) + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + return + } + }) + http.HandleFunc("/stat/identities", func(writer http.ResponseWriter, request *http.Request) { + data := struct { + Ids []string `json:"ids"` + }{} + if err := json.NewDecoder(request.Body).Decode(&data); err != nil { + http.Error(writer, "invalid JSON", http.StatusBadRequest) + return + } + accountInfos, err := i.accountInfoProvider.BatchAccountInfo(request.Context(), data.Ids) + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + return + } + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusOK) + err = json.NewEncoder(writer).Encode(accountInfos) + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + return + } + }) + return nil +} + +func (i *identityStat) Close(ctx context.Context) (err error) { + return +}