From 67c0988ad38d95e4f7cdb0a6ed8f1f9d57d4ee8e Mon Sep 17 00:00:00 2001 From: zelig Date: Fri, 11 Dec 2015 12:49:52 +0000 Subject: [PATCH] integrate new rpc v2 and nodestack - rebase on #2035 --- cmd/utils/api.go | 3 +- common/chequebook/api.go | 29 ++ common/types.go | 9 - rpc/api/bzz.go | 278 ------------- rpc/api/bzz_args.go | 322 --------------- rpc/api/bzz_js.go | 95 ----- rpc/api/utils.go | 17 - rpc/shared/utils.go | 3 +- swarm/api/api.go | 478 +++++----------------- swarm/api/api_test.go | 233 +++-------- swarm/api/dns.go | 58 +++ swarm/api/filesystem.go | 258 ++++++++++++ swarm/api/filesystem_test.go | 180 ++++++++ swarm/api/http/roundtripper.go | 49 +++ swarm/api/{ => http}/roundtripper_test.go | 4 +- swarm/api/{http.go => http/server.go} | 26 +- swarm/api/manifest.go | 4 +- swarm/api/roundtripper.go | 22 - swarm/api/storage.go | 54 +++ swarm/api/storage_test.go | 32 ++ swarm/storage/chunkreader.go | 3 +- swarm/swarm.go | 54 ++- 22 files changed, 875 insertions(+), 1336 deletions(-) create mode 100644 common/chequebook/api.go delete mode 100644 rpc/api/bzz.go delete mode 100644 rpc/api/bzz_args.go delete mode 100644 rpc/api/bzz_js.go create mode 100644 swarm/api/dns.go create mode 100644 swarm/api/filesystem.go create mode 100644 swarm/api/filesystem_test.go create mode 100644 swarm/api/http/roundtripper.go rename swarm/api/{ => http}/roundtripper_test.go (95%) rename swarm/api/{http.go => http/server.go} (91%) delete mode 100644 swarm/api/roundtripper.go create mode 100644 swarm/api/storage.go create mode 100644 swarm/api/storage_test.go diff --git a/cmd/utils/api.go b/cmd/utils/api.go index 83c7d5eaa4..90f69fdf62 100644 --- a/cmd/utils/api.go +++ b/cmd/utils/api.go @@ -68,7 +68,6 @@ func (s *PublicNetApi) PeerCount() *rpc.HexNumber { return rpc.NewHexNumber(s.net.PeerCount()) } -// ProtocolVersion returns the current ethereum protocol version. func (s *PublicNetApi) Version() string { return fmt.Sprintf("%d", s.networkVersion) -} \ No newline at end of file +} diff --git a/common/chequebook/api.go b/common/chequebook/api.go new file mode 100644 index 0000000000..5507682530 --- /dev/null +++ b/common/chequebook/api.go @@ -0,0 +1,29 @@ +package chequebook + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +const Version = "1.0" + +type Api struct { + ch *Chequebook +} + +func NewApi(ch *Chequebook) *Api { + return &Api{ch} +} + +func (self *Api) Issue(beneficiary common.Address, amount *big.Int) (cheque *Cheque, err error) { + return self.ch.Issue(beneficiary, amount) +} + +func (self *Api) Cash(cheque *Cheque) (txhash string, err error) { + return self.ch.Cash(cheque) +} + +func (self *Api) Deposit(amount *big.Int) (txhash string, err error) { + return self.ch.Deposit(amount) +} diff --git a/common/types.go b/common/types.go index 4e5500c06c..58bd091346 100644 --- a/common/types.go +++ b/common/types.go @@ -188,15 +188,6 @@ func PP(value []byte) string { return fmt.Sprintf("%x...%x", value[:4], value[len(value)-4]) } -func (a *Address) MarshalJSON() (out []byte, err error) { - return []byte(quote(a.Hex())), nil -} - -func (m *Address) UnmarshalJSON(value []byte) error { - m.Set(HexToAddress(string(value[1 : len(value)-1]))) - return nil -} - func quote(s string) string { return `"` + s + `"` } diff --git a/rpc/api/bzz.go b/rpc/api/bzz.go deleted file mode 100644 index c258879857..0000000000 --- a/rpc/api/bzz.go +++ /dev/null @@ -1,278 +0,0 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with go-ethereum. If not, see . - -package api - -import ( - "encoding/json" - "fmt" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/rpc/codec" - "github.com/ethereum/go-ethereum/rpc/shared" - "github.com/ethereum/go-ethereum/swarm" -) - -const ( - BzzApiVersion = "1.0" -) - -// eth api provider -// See https://github.com/ethereum/wiki/wiki/JSON-RPC -type bzzApi struct { - swarm *swarm.Swarm - methods map[string]bzzhandler - codec codec.ApiCoder -} - -// eth callback handler -type bzzhandler func(*bzzApi, *shared.Request) (interface{}, error) - -var ( - bzzMapping = map[string]bzzhandler{ - "bzz_info": (*bzzApi).Info, - "bzz_issue": (*bzzApi).Issue, - "bzz_cash": (*bzzApi).Cash, - "bzz_deposit": (*bzzApi).Deposit, - "bzz_register": (*bzzApi).Register, - "bzz_resolve": (*bzzApi).Resolve, - "bzz_download": (*bzzApi).Download, - "bzz_upload": (*bzzApi).Upload, - "bzz_get": (*bzzApi).Get, - "bzz_put": (*bzzApi).Put, - "bzz_modify": (*bzzApi).Modify, - } -) - -func newSwarmOfflineError(method string) error { - return shared.NewNotAvailableError(method, "swarm offline") -} - -// create new bzzApi instance -func NewBzzApi(stack *node.Node, codec codec.Codec) *bzzApi { - var swarm *swarm.Swarm - stack.Service(&swarm) - return &bzzApi{swarm, bzzMapping, codec.New(nil)} -} - -// collection with supported methods -func (self *bzzApi) Methods() []string { - methods := make([]string, len(self.methods)) - i := 0 - for k := range self.methods { - methods[i] = k - i++ - } - return methods -} - -// Execute given request -func (self *bzzApi) Execute(req *shared.Request) (interface{}, error) { - if callback, ok := self.methods[req.Method]; ok { - return callback(self, req) - } - - return nil, shared.NewNotImplementedError(req.Method) -} - -func (self *bzzApi) Name() string { - return shared.BzzApiName -} - -func (self *bzzApi) ApiVersion() string { - return BzzApiVersion -} - -func (self *bzzApi) Info(req *shared.Request) (interface{}, error) { - s := self.swarm - if s == nil { - return nil, newSwarmOfflineError(req.Method) - } - return s.Api().Info(), nil -} - -func (self *bzzApi) Issue(req *shared.Request) (interface{}, error) { - s := self.swarm - if s == nil { - return nil, newSwarmOfflineError(req.Method) - } - - args := new(BzzIssueArgs) - if err := self.codec.Decode(req.Params, &args); err != nil { - return nil, shared.NewDecodeParamError(err.Error()) - } - - cheque, err := s.Api().Issue(common.HexToAddress(args.Beneficiary), args.Amount) - if err != nil { - return nil, shared.NewDecodeParamError(err.Error()) - } - - out, err := json.MarshalIndent(cheque, " ", "") - if err != nil { - return nil, shared.NewDecodeParamError(err.Error()) - } - - return string(out), nil -} - -func (self *bzzApi) Cash(req *shared.Request) (interface{}, error) { - s := self.swarm - if s == nil { - return nil, newSwarmOfflineError(req.Method) - } - - args := new(BzzCashArgs) - if err := self.codec.Decode(req.Params, &args); err != nil { - return nil, shared.NewDecodeParamError(err.Error()) - } - - return s.Api().Cash(args.Cheque) - -} - -func (self *bzzApi) Deposit(req *shared.Request) (interface{}, error) { - s := self.swarm - if s == nil { - return nil, newSwarmOfflineError(req.Method) - } - - args := new(BzzDepositArgs) - if err := self.codec.Decode(req.Params, &args); err != nil { - return nil, shared.NewDecodeParamError(err.Error()) - } - - return s.Api().Deposit(args.Amount) -} - -func (self *bzzApi) Register(req *shared.Request) (interface{}, error) { - s := self.swarm - if s == nil { - return nil, newSwarmOfflineError(req.Method) - } - - args := new(BzzRegisterArgs) - if err := self.codec.Decode(req.Params, &args); err != nil { - return nil, shared.NewDecodeParamError(err.Error()) - } - - err := s.Api().Register(common.HexToAddress(args.Address), args.Domain, common.HexToHash(args.ContentHash)) - return err == nil, err -} - -func (self *bzzApi) Resolve(req *shared.Request) (interface{}, error) { - s := self.swarm - if s == nil { - return nil, newSwarmOfflineError(req.Method) - } - - args := new(BzzResolveArgs) - if err := self.codec.Decode(req.Params, &args); err != nil { - return nil, shared.NewDecodeParamError(err.Error()) - } - - key, err := s.Api().Resolve(args.Domain) - return key.Hex(), err -} - -func (self *bzzApi) Download(req *shared.Request) (interface{}, error) { - - s := self.swarm - if s == nil { - return nil, newSwarmOfflineError(req.Method) - } - - args := new(BzzDownloadArgs) - if err := self.codec.Decode(req.Params, &args); err != nil { - return nil, shared.NewDecodeParamError(err.Error()) - } - - err := s.Api().Download(args.BzzPath, args.LocalPath) - return err == nil, err -} - -func (self *bzzApi) Upload(req *shared.Request) (interface{}, error) { - - s := self.swarm - if s == nil { - return nil, newSwarmOfflineError(req.Method) - } - - args := new(BzzUploadArgs) - if err := self.codec.Decode(req.Params, &args); err != nil { - return nil, shared.NewDecodeParamError(err.Error()) - } - - return s.Api().Upload(args.LocalPath, args.Index) -} - -func (self *bzzApi) Get(req *shared.Request) (interface{}, error) { - - s := self.swarm - if s == nil { - return nil, newSwarmOfflineError(req.Method) - } - - args := new(BzzGetArgs) - if err := self.codec.Decode(req.Params, &args); err != nil { - return nil, shared.NewDecodeParamError(err.Error()) - } - - var content []byte - var mimeType string - var status, size int - var err error - content, mimeType, status, size, err = s.Api().Get(args.Path) - - obj := map[string]string{ - "content": string(content), - "contentType": mimeType, - "status": fmt.Sprintf("%v", status), - "size": fmt.Sprintf("%v", size), - } - - return obj, err -} - -func (self *bzzApi) Put(req *shared.Request) (interface{}, error) { - - s := self.swarm - if s == nil { - return nil, newSwarmOfflineError(req.Method) - } - - args := new(BzzPutArgs) - if err := self.codec.Decode(req.Params, &args); err != nil { - return nil, shared.NewDecodeParamError(err.Error()) - } - - return s.Api().Put(args.Content, args.ContenType) -} - -func (self *bzzApi) Modify(req *shared.Request) (interface{}, error) { - - s := self.swarm - if s == nil { - return nil, newSwarmOfflineError(req.Method) - } - - args := new(BzzModifyArgs) - if err := self.codec.Decode(req.Params, &args); err != nil { - return nil, shared.NewDecodeParamError(err.Error()) - } - - return s.Api().Modify(args.RootHash, args.Path, args.ContentHash, args.ContentType) -} diff --git a/rpc/api/bzz_args.go b/rpc/api/bzz_args.go deleted file mode 100644 index 2aee255e15..0000000000 --- a/rpc/api/bzz_args.go +++ /dev/null @@ -1,322 +0,0 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with go-ethereum. If not, see . - -package api - -import ( - "encoding/json" - "math/big" - - "github.com/ethereum/go-ethereum/common/chequebook" - "github.com/ethereum/go-ethereum/rpc/shared" -) - -type BzzDepositArgs struct { - Amount *big.Int -} - -func (args *BzzDepositArgs) UnmarshalJSON(b []byte) (err error) { - var obj []interface{} - if err := json.Unmarshal(b, &obj); err != nil { - return shared.NewDecodeParamError(err.Error()) - } - - if len(obj) < 1 { - return shared.NewInsufficientParamsError(len(obj), 1) - } - - amount, ok := obj[0].(string) - if !ok { - return shared.NewInvalidTypeError("Amount", "not a string") - } - args.Amount, ok = new(big.Int).SetString(amount, 10) - if !ok { - return shared.NewInvalidTypeError("Amount", "not a number") - } - - return nil -} - -type BzzCashArgs struct { - Cheque *chequebook.Cheque -} - -func (args *BzzCashArgs) UnmarshalJSON(b []byte) (err error) { - var obj []interface{} - if err := json.Unmarshal(b, &obj); err != nil { - return shared.NewDecodeParamError(err.Error()) - } - - if len(obj) < 1 { - return shared.NewInsufficientParamsError(len(obj), 1) - } - - chequestr, ok := obj[0].(string) - if !ok { - return shared.NewInvalidTypeError("Cheque", "not a string") - } - var cheque chequebook.Cheque - err = json.Unmarshal([]byte(chequestr), &cheque) - if err != nil { - return shared.NewDecodeParamError(err.Error()) - } - args.Cheque = &cheque - - return nil -} - -type BzzIssueArgs struct { - Beneficiary string - Amount *big.Int -} - -func (args *BzzIssueArgs) UnmarshalJSON(b []byte) (err error) { - var obj []interface{} - if err := json.Unmarshal(b, &obj); err != nil { - return shared.NewDecodeParamError(err.Error()) - } - - if len(obj) < 2 { - return shared.NewInsufficientParamsError(len(obj), 2) - } - - beneficiary, ok := obj[0].(string) - if !ok { - return shared.NewInvalidTypeError("Amount", "not a string") - } - args.Beneficiary = beneficiary - - amount, ok := obj[1].(string) - if !ok { - return shared.NewInvalidTypeError("Amount", "not a string") - } - args.Amount, ok = new(big.Int).SetString(amount, 10) - if !ok { - return shared.NewInvalidTypeError("Amount", "not a number") - } - - return nil -} - -type BzzRegisterArgs struct { - Address, ContentHash, Domain string -} - -func (args *BzzRegisterArgs) UnmarshalJSON(b []byte) (err error) { - var obj []interface{} - if err := json.Unmarshal(b, &obj); err != nil { - return shared.NewDecodeParamError(err.Error()) - } - - if len(obj) < 3 { - return shared.NewInsufficientParamsError(len(obj), 1) - } - - addstr, ok := obj[0].(string) - if !ok { - return shared.NewInvalidTypeError("Address", "not a string") - } - args.Address = addstr - - addstr, ok = obj[1].(string) - if !ok { - return shared.NewInvalidTypeError("Domain", "not a string") - } - args.Domain = addstr - - addstr, ok = obj[2].(string) - if !ok { - return shared.NewInvalidTypeError("ContentHash", "not a string") - } - args.ContentHash = addstr - - return nil -} - -type BzzResolveArgs struct { - Domain string -} - -func (args *BzzResolveArgs) UnmarshalJSON(b []byte) (err error) { - var obj []interface{} - if err := json.Unmarshal(b, &obj); err != nil { - return shared.NewDecodeParamError(err.Error()) - } - - if len(obj) < 1 { - return shared.NewInsufficientParamsError(len(obj), 1) - } - - addstr, ok := obj[0].(string) - if !ok { - return shared.NewInvalidTypeError("Domain", "not a string") - } - args.Domain = addstr - - return nil -} - -type BzzDownloadArgs struct { - BzzPath, LocalPath string -} - -func (args *BzzDownloadArgs) UnmarshalJSON(b []byte) (err error) { - var obj []interface{} - if err := json.Unmarshal(b, &obj); err != nil { - return shared.NewDecodeParamError(err.Error()) - } - - if len(obj) < 2 { - return shared.NewInsufficientParamsError(len(obj), 1) - } - - addstr, ok := obj[0].(string) - if !ok { - return shared.NewInvalidTypeError("BzzPath", "not a string") - } - args.BzzPath = addstr - - addstr, ok = obj[1].(string) - if !ok { - return shared.NewInvalidTypeError("LocalPath", "not a string") - } - args.LocalPath = addstr - - return nil -} - -type BzzUploadArgs struct { - LocalPath, Index string -} - -func (args *BzzUploadArgs) UnmarshalJSON(b []byte) (err error) { - var obj []interface{} - if err := json.Unmarshal(b, &obj); err != nil { - return shared.NewDecodeParamError(err.Error()) - } - - if len(obj) < 1 { - return shared.NewInsufficientParamsError(len(obj), 1) - } - - addstr, ok := obj[0].(string) - if !ok { - return shared.NewInvalidTypeError("LocalPath", "not a string") - } - args.LocalPath = addstr - - if len(obj) > 1 { - addstr, ok := obj[1].(string) - if ok { - args.Index = addstr - } - } - - return nil -} - -type BzzGetArgs struct { - Path string -} - -func (args *BzzGetArgs) UnmarshalJSON(b []byte) (err error) { - var obj []interface{} - if err := json.Unmarshal(b, &obj); err != nil { - return shared.NewDecodeParamError(err.Error()) - } - - if len(obj) < 1 { - return shared.NewInsufficientParamsError(len(obj), 1) - } - - addstr, ok := obj[0].(string) - if !ok { - return shared.NewInvalidTypeError("Path", "not a string") - } - args.Path = addstr - - return nil -} - -type BzzPutArgs struct { - Content, ContenType string -} - -func (args *BzzPutArgs) UnmarshalJSON(b []byte) (err error) { - var obj []interface{} - if err := json.Unmarshal(b, &obj); err != nil { - return shared.NewDecodeParamError(err.Error()) - } - - if len(obj) < 1 { - return shared.NewInsufficientParamsError(len(obj), 1) - } - - addstr, ok := obj[0].(string) - if !ok { - return shared.NewInvalidTypeError("Content", "not a string") - } - args.Content = addstr - - addstr, ok = obj[1].(string) - if !ok { - return shared.NewInvalidTypeError("ContenType", "not a string") - } - args.ContenType = addstr - - return nil -} - -type BzzModifyArgs struct { - RootHash, Path, ContentHash, ContentType string -} - -func (args *BzzModifyArgs) UnmarshalJSON(b []byte) (err error) { - var obj []interface{} - if err := json.Unmarshal(b, &obj); err != nil { - return shared.NewDecodeParamError(err.Error()) - } - - if len(obj) < 2 { - return shared.NewInsufficientParamsError(len(obj), 1) - } - - addstr, ok := obj[0].(string) - if !ok { - return shared.NewInvalidTypeError("RootHash", "not a string") - } - args.RootHash = addstr - - addstr, ok = obj[1].(string) - if !ok { - return shared.NewInvalidTypeError("Path", "not a string") - } - args.Path = addstr - - if len(obj) >= 4 { - addstr, ok = obj[2].(string) - if ok { - args.ContentHash = addstr - } - - addstr, ok = obj[3].(string) - if ok { - args.ContentType = addstr - } - } - - return nil -} diff --git a/rpc/api/bzz_js.go b/rpc/api/bzz_js.go deleted file mode 100644 index 50bbd43d07..0000000000 --- a/rpc/api/bzz_js.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with go-ethereum. If not, see . - -package api - -const Bzz_JS = ` -web3._extend({ - property: 'bzz', - methods: - [ - new web3._extend.Method({ - name: 'deposit', - call: 'bzz_deposit', - params: 1, - inputFormatter: [null] - }), - new web3._extend.Method({ - name: 'info', - call: 'bzz_info', - params: 1, - inputFormatter: [null] - }), - new web3._extend.Method({ - name: 'cash', - call: 'bzz_cash', - params: 1, - inputFormatter: [null] - }), - new web3._extend.Method({ - name: 'issue', - call: 'bzz_issue', - params: 2, - inputFormatter: [null, null] - }), - new web3._extend.Method({ - name: 'register', - call: 'bzz_register', - params: 3, - inputFormatter: [null, null, null] - }), - new web3._extend.Method({ - name: 'resolve', - call: 'bzz_resolve', - params: 1, - inputFormatter: [null] - }), - new web3._extend.Method({ - name: 'download', - call: 'bzz_download', - params: 2, - inputFormatter: [null, null] - }), - new web3._extend.Method({ - name: 'upload', - call: 'bzz_upload', - params: 2, - inputFormatter: [null, null] - }), - new web3._extend.Method({ - name: 'get', - call: 'bzz_get', - params: 1, - inputFormatter: [null] - }), - new web3._extend.Method({ - name: 'put', - call: 'bzz_put', - params: 2, - inputFormatter: [null, null] - }), - new web3._extend.Method({ - name: 'modify', - call: 'bzz_modify', - params: 4, - inputFormatter: [null, null, null, null] - }) - ], - properties: - [ - ] -}); -` diff --git a/rpc/api/utils.go b/rpc/api/utils.go index d52f6b3dc3..e3218d5432 100644 --- a/rpc/api/utils.go +++ b/rpc/api/utils.go @@ -63,19 +63,6 @@ var ( "httpGet", "verbosity", }, - "bzz": []string{ - "info", - "issue", - "cash", - "deposit", - "register", - "resolve", - "download", - "upload", - "get", - "put", - "modify", - }, "db": []string{ "getString", "putString", @@ -194,8 +181,6 @@ func ParseApiString(apistr string, codec codec.Codec, xeth *xeth.XEth, stack *no switch strings.ToLower(strings.TrimSpace(name)) { case shared.AdminApiName: apis[i] = NewAdminApi(xeth, stack, codec) - case shared.BzzApiName: - apis[i] = NewBzzApi(stack, codec) case shared.DebugApiName: apis[i] = NewDebugApi(xeth, eth, codec) case shared.DbApiName: @@ -227,8 +212,6 @@ func Javascript(name string) string { switch strings.ToLower(strings.TrimSpace(name)) { case shared.AdminApiName: return Admin_JS - case shared.BzzApiName: - return Bzz_JS case shared.DebugApiName: return Debug_JS case shared.DbApiName: diff --git a/rpc/shared/utils.go b/rpc/shared/utils.go index 1590265a01..b13e9eb1b4 100644 --- a/rpc/shared/utils.go +++ b/rpc/shared/utils.go @@ -20,7 +20,6 @@ import "strings" const ( AdminApiName = "admin" - BzzApiName = "bzz" EthApiName = "eth" DbApiName = "db" DebugApiName = "debug" @@ -38,7 +37,7 @@ const ( var ( // All API's AllApis = strings.Join([]string{ - AdminApiName, BzzApiName, DbApiName, EthApiName, DebugApiName, MinerApiName, NetApiName, + AdminApiName, DbApiName, EthApiName, DebugApiName, MinerApiName, NetApiName, ShhApiName, TxPoolApiName, PersonalApiName, Web3ApiName, }, ",") ) diff --git a/swarm/api/api.go b/swarm/api/api.go index 11bf4d81ec..ecd18a12a5 100644 --- a/swarm/api/api.go +++ b/swarm/api/api.go @@ -1,24 +1,16 @@ package api import ( - "bufio" "fmt" "io" - "math/big" - "net/http" - "os" - "path/filepath" "regexp" "strings" "sync" - "github.com/ethereum/go-ethereum/swarm/storage" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/chequebook" - "github.com/ethereum/go-ethereum/common/registrar" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/logger" "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/swarm/storage" ) var ( @@ -27,68 +19,86 @@ var ( domainAndVersion = regexp.MustCompile("[@:;,]+") ) +type Resolver interface { + Resolve(string) (storage.Key, error) +} + /* Api implements webserver/file system related content storage and retrieval on top of the dpa it is the public interface of the dpa which is included in the ethereum stack */ type Api struct { - dpa *storage.DPA - registrar registrar.VersionedRegistrar - conf *Config + dpa *storage.DPA + dns Resolver } //the api constructor initialises -func NewApi(dpa *storage.DPA, registrar registrar.VersionedRegistrar, conf *Config) (self *Api) { - return &Api{dpa, registrar, conf} -} - -// this should move over to chequebook ipc api -func (self *Api) Issue(beneficiary common.Address, amount *big.Int) (cheque *chequebook.Cheque, err error) { - return self.conf.Swap.Chequebook().Issue(beneficiary, amount) -} - -func (self *Api) Cash(cheque *chequebook.Cheque) (txhash string, err error) { - return self.conf.Swap.Chequebook().Cash(cheque) -} - -func (self *Api) Deposit(amount *big.Int) (txhash string, err error) { - return self.conf.Swap.Chequebook().Deposit(amount) -} - -// serialisable info about swarm -type Info struct { - *Config - *chequebook.Params -} - -func (self *Api) Info() *Info { - - return &Info{ - Config: self.conf, - Params: chequebook.ContractParams, - } - -} - -// Get uses iterative manifest retrieval and prefix matching -// to resolve path to content using dpa retrieve -func (self *Api) Get(bzzpath string) (content []byte, mimeType string, status int, size int, err error) { - var reader storage.SectionReader - reader, mimeType, status, err = self.getPath("/" + bzzpath) - if err != nil { - return - } - content = make([]byte, reader.Size()) - size, err = reader.Read(content) - if err == io.EOF { - err = nil +func NewApi(dpa *storage.DPA, dns Resolver) (self *Api) { + self = &Api{ + dpa: dpa, + dns: dns, } return } -// Put provides singleton manifest creation and optional name registration -// on top of dpa store +// DPA reader API +func (self *Api) Retrieve(key storage.Key) storage.SectionReader { + return self.dpa.Retrieve(key) +} + +func (self *Api) Store(data storage.SectionReader, wg *sync.WaitGroup) (key storage.Key, err error) { + return self.dpa.Store(data, wg) +} + +// DNS Resolver +func (self *Api) Resolve(hostPort string) (contentHash storage.Key, err error) { + if hashMatcher.MatchString(hostPort) || self.dns == nil { + glog.V(logger.Debug).Infof("[BZZ] host is a contentHash: '%v'", contentHash) + return storage.Key(common.Hex2Bytes(hostPort)), nil + } + contentHash, err = self.dns.Resolve(hostPort) + if err != nil { + err = ErrResolve(err) + glog.V(logger.Debug).Infof("[BZZ] DNS error : %v", err) + } + glog.V(logger.Debug).Infof("[BZZ] host lookup: %v -> %v", err) + return +} + +func parse(uri string) (hostPort, path string) { + parts := slashes.Split(uri, 3) + var i int + if len(parts) == 0 { + return + } + // beginning with slash is now optional + if len(parts[0]) == 0 { + i++ + } + hostPort = parts[i] + if len(parts) > i+1 { + path = parts[i+1] + if len(parts) == 3 { + path += "/" + parts[2] + } + path += "/" + } + if len(path) > 0 { + path = "/" + path + } + glog.V(logger.Debug).Infof("[BZZ] Swarm: host: '%s', path '%s' requested.", hostPort, path) + return +} + +func (self *Api) parseAndResolve(uri string) (contentHash storage.Key, hostPort, path string, err error) { + hostPort, path = parse(uri) + //resolving host and port + contentHash, err = self.Resolve(hostPort) + return +} + +// Put provides singleton manifest creation on top of dpa store func (self *Api) Put(content, contentType string) (string, error) { sr := io.NewSectionReader(strings.NewReader(content), 0, int64(len(content))) wg := &sync.WaitGroup{} @@ -106,6 +116,34 @@ func (self *Api) Put(content, contentType string) (string, error) { return key.String(), nil } +// Get uses iterative manifest retrieval and prefix matching +// to resolve path to content using dpa retrieve +// it returns a section reader, mimeType, status and an error +func (self *Api) Get(uri string) (reader storage.SectionReader, mimeType string, status int, err error) { + + key, _, path, err := self.parseAndResolve(uri) + + trie, err := loadManifest(self.dpa, key) + if err != nil { + glog.V(logger.Debug).Infof("[BZZ] Swarm: loadManifestTrie error: %v", err) + return + } + + glog.V(logger.Debug).Infof("[BZZ] Swarm: getEntry(%s)", path) + entry, _ := trie.getEntry(path) + if entry != nil { + key = common.Hex2Bytes(entry.Hash) + status = entry.Status + mimeType = entry.ContentType + glog.V(logger.Debug).Infof("[BZZ] Swarm: content lookup key: '%v' (%v)", key, mimeType) + reader = self.dpa.Retrieve(key) + } else { + err = fmt.Errorf("manifest entry for '%s' not found", path) + glog.V(logger.Debug).Infof("[BZZ] Swarm: %v", err) + } + return +} + func (self *Api) Modify(rootHash, path, contentHash, contentType string) (newRootHash string, err error) { root := common.Hex2Bytes(rootHash) trie, err := loadManifest(self.dpa, root) @@ -130,327 +168,3 @@ func (self *Api) Modify(rootHash, path, contentHash, contentType string) (newRoo } return trie.hash.String(), nil } - -const maxParallelFiles = 5 - -// Download replicates the manifest path structure on the local filesystem -// under localpath -func (self *Api) Download(bzzpath, localpath string) (err error) { - lpath, err := filepath.Abs(filepath.Clean(localpath)) - if err != nil { - return - } - err = os.MkdirAll(lpath, os.ModePerm) - if err != nil { - return - } - - parts := slashes.Split(bzzpath, 3) - if len(parts) < 2 { - return fmt.Errorf("Invalid bzz path") - } - hostPort := parts[1] - var path string - if len(parts) > 2 { - path = regularSlashes(parts[2]) + "/" - } - glog.V(logger.Debug).Infof("[BZZ] Swarm: host: '%s', path '%s' requested.", hostPort, path) - - //resolving host and port - var key storage.Key - key, err = self.Resolve(hostPort) - if err != nil { - err = errResolve(err) - glog.V(logger.Debug).Infof("[BZZ] Swarm: error : %v", err) - return - } - - trie, err := loadManifest(self.dpa, key) - if err != nil { - glog.V(logger.Debug).Infof("[BZZ] Swarm: loadManifestTrie error: %v", err) - return - } - - type downloadListEntry struct { - key storage.Key - path string - } - - var list []*downloadListEntry - var mde, mderr error - - prevPath := lpath - err = trie.listWithPrefix(path, func(entry *manifestTrieEntry, suffix string) { // TODO: paralellize - key := common.Hex2Bytes(entry.Hash) - path := lpath + "/" + suffix - dir := filepath.Dir(path) - if dir != prevPath { - mde = os.MkdirAll(dir, os.ModePerm) - if mde != nil { - mderr = mde - } - prevPath = dir - } - if (mde == nil) && (path != dir+"/") { - list = append(list, &downloadListEntry{key: key, path: path}) - } - }) - if err == nil { - err = mderr - } - - cnt := len(list) - errors := make([]error, cnt) - done := make(chan bool, maxParallelFiles) - dcnt := 0 - - for i, entry := range list { - if i >= dcnt+maxParallelFiles { - <-done - dcnt++ - } - go func(i int, entry *downloadListEntry, done chan bool) { - f, err := os.Create(entry.path) // TODO: path separators - if err == nil { - reader := self.dpa.Retrieve(entry.key) - writer := bufio.NewWriter(f) - _, err = io.CopyN(writer, reader, reader.Size()) // TODO: handle errors - err2 := writer.Flush() - if err == nil { - err = err2 - } - err2 = f.Close() - if err == nil { - err = err2 - } - } - - errors[i] = err - done <- true - }(i, entry, done) - } - for dcnt < cnt { - <-done - dcnt++ - } - - if err != nil { - return - } - for i, _ := range list { - if errors[i] != nil { - return errors[i] - } - } - return -} - -// Upload replicates a local directory as a manifest file and uploads it -// using dpa store -// TODO: localpath should point to a manifest -func (self *Api) Upload(lpath, index string) (string, error) { - var list []*manifestTrieEntry - localpath, err := filepath.Abs(filepath.Clean(lpath)) - if err != nil { - return "", err - } - - f, err := os.Open(localpath) - if err != nil { - return "", err - } - stat, err := f.Stat() - if err != nil { - return "", err - } - - var start int - if stat.IsDir() { - start = len(localpath) - glog.V(logger.Debug).Infof("[BZZ] uploading '%s'", localpath) - err = filepath.Walk(localpath, func(path string, info os.FileInfo, err error) error { - if (err == nil) && !info.IsDir() { - //fmt.Printf("lp %s path %s\n", localpath, path) - if len(path) <= start { - return fmt.Errorf("Path is too short") - } - if path[:start] != localpath { - return fmt.Errorf("Path prefix of '%s' does not match localpath '%s'", path, localpath) - } - entry := &manifestTrieEntry{ - Path: path, - } - list = append(list, entry) - } - return err - }) - if err != nil { - return "", err - } - } else { - dir := filepath.Dir(localpath) - start = len(dir) - if len(localpath) <= start { - return "", fmt.Errorf("Path is too short") - } - if localpath[:start] != dir { - return "", fmt.Errorf("Path prefix of '%s' does not match dir '%s'", localpath, dir) - } - entry := &manifestTrieEntry{ - Path: localpath, - } - list = append(list, entry) - } - - cnt := len(list) - errors := make([]error, cnt) - done := make(chan bool, maxParallelFiles) - dcnt := 0 - - for i, entry := range list { - if i >= dcnt+maxParallelFiles { - <-done - dcnt++ - } - go func(i int, entry *manifestTrieEntry, done chan bool) { - f, err := os.Open(entry.Path) - if err == nil { - stat, _ := f.Stat() - sr := io.NewSectionReader(f, 0, stat.Size()) - wg := &sync.WaitGroup{} - var hash storage.Key - hash, err = self.dpa.Store(sr, wg) - if hash != nil { - list[i].Hash = hash.String() - } - wg.Wait() - if err == nil { - first512 := make([]byte, 512) - fread, _ := sr.ReadAt(first512, 0) - if fread > 0 { - mimeType := http.DetectContentType(first512[:fread]) - if filepath.Ext(entry.Path) == ".css" { - mimeType = "text/css" - } - list[i].ContentType = mimeType - //fmt.Printf("%v %v %v\n", entry.Path, mimeType, filepath.Ext(entry.Path)) - } - } - f.Close() - } - errors[i] = err - done <- true - }(i, entry, done) - } - for dcnt < cnt { - <-done - dcnt++ - } - - trie := &manifestTrie{ - dpa: self.dpa, - } - for i, entry := range list { - if errors[i] != nil { - return "", errors[i] - } - entry.Path = regularSlashes(entry.Path[start:]) - if entry.Path == index { - ientry := &manifestTrieEntry{ - Path: "", - Hash: entry.Hash, - ContentType: entry.ContentType, - } - trie.addEntry(ientry) - } - trie.addEntry(entry) - } - - err2 := trie.recalcAndStore() - var hs string - if err2 == nil { - hs = trie.hash.String() - } - return hs, err2 -} - -func (self *Api) Register(sender common.Address, domain string, hash common.Hash) (err error) { - domainhash := common.BytesToHash(crypto.Sha3([]byte(domain))) - - if self.registrar != nil { - glog.V(logger.Debug).Infof("[BZZ] Swarm: host '%s' (hash: '%v') to be registered as '%v'", domain, domainhash.Hex(), hash.Hex()) - _, err = self.registrar.Registry().SetHashToHash(sender, domainhash, hash) - } else { - err = fmt.Errorf("no registry: %v", err) - } - return -} - -type errResolve error - -func (self *Api) Resolve(hostPort string) (contentHash storage.Key, err error) { - host := hostPort - if hashMatcher.MatchString(host) { - contentHash = storage.Key(common.Hex2Bytes(host)) - glog.V(logger.Debug).Infof("[BZZ] Swarm: host is a contentHash: '%v'", contentHash) - } else { - if self.registrar != nil { - var hash common.Hash - var version *big.Int - parts := domainAndVersion.Split(host, 3) - if len(parts) > 1 && parts[1] != "" { - host = parts[0] - version = common.Big(parts[1]) - } - hostHash := crypto.Sha3Hash([]byte(host)) - hash, err = self.registrar.Resolver(version).HashToHash(hostHash) - if err != nil { - err = fmt.Errorf("unable to resolve '%s': %v", hostPort, err) - } - contentHash = storage.Key(hash.Bytes()) - glog.V(logger.Debug).Infof("[BZZ] Swarm: resolve host '%s' to contentHash: '%v'", hostPort, contentHash) - } else { - err = fmt.Errorf("no resolver '%s': %v", hostPort, err) - } - } - return -} - -func (self *Api) getPath(uri string) (reader storage.SectionReader, mimeType string, status int, err error) { - parts := slashes.Split(uri, 3) - hostPort := parts[1] - var path string - if len(parts) > 2 { - path = parts[2] - } - glog.V(logger.Debug).Infof("[BZZ] Swarm: host: '%s', path '%s' requested.", hostPort, path) - - //resolving host and port - var key storage.Key - key, err = self.Resolve(hostPort) - if err != nil { - err = errResolve(err) - glog.V(logger.Debug).Infof("[BZZ] Swarm: error : %v", err) - return - } - - trie, err := loadManifest(self.dpa, key) - if err != nil { - glog.V(logger.Debug).Infof("[BZZ] Swarm: loadManifestTrie error: %v", err) - return - } - - glog.V(logger.Debug).Infof("[BZZ] Swarm: getEntry(%s)", path) - entry, _ := trie.getEntry(path) - if entry != nil { - key = common.Hex2Bytes(entry.Hash) - status = entry.Status - mimeType = entry.ContentType - glog.V(logger.Debug).Infof("[BZZ] Swarm: content lookup key: '%v' (%v)", key, mimeType) - reader = self.dpa.Retrieve(key) - } else { - err = fmt.Errorf("manifest entry for '%s' not found", path) - glog.V(logger.Debug).Infof("[BZZ] Swarm: %v", err) - } - return -} diff --git a/swarm/api/api_test.go b/swarm/api/api_test.go index 81156afcd8..3941bcf78b 100644 --- a/swarm/api/api_test.go +++ b/swarm/api/api_test.go @@ -4,202 +4,77 @@ import ( "bytes" "io/ioutil" "os" - "path" - "runtime" "testing" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/swarm/storage" ) -//TODO: add tests for resolver/registrar -// will most likely be its own package under service? - -var ( - testDir string -) - -func init() { - _, filename, _, _ := runtime.Caller(1) - testDir = path.Join(path.Dir(filename), "../test") -} - -func testApi() (api *Api, err error) { +func testApi(t *testing.T, f func(*Api)) { datadir, err := ioutil.TempDir("", "bzz-test") if err != nil { - return nil, err + t.Fatalf("unable to create temp dir: %v", err) } os.RemoveAll(datadir) dpa, err := storage.NewLocalDPA(datadir) if err != nil { return } - prvkey, _ := crypto.GenerateKey() + api := NewApi(dpa, nil) + dpa.Start() + f(api) + dpa.Stop() + os.RemoveAll(datadir) +} - config, err := NewConfig(datadir, common.Address{}, prvkey) - if err != nil { - return +type testResponse struct { + reader storage.SectionReader + *Response +} + +func checkResponse(t *testing.T, resp *testResponse, exp *Response) { + + if resp.MimeType != exp.MimeType { + t.Errorf("incorrect mimeType. expected '%s', got '%s'", exp.MimeType, resp.MimeType) } - api = NewApi(dpa, nil, config) - api.dpa.Start() + if resp.Status != exp.Status { + t.Errorf("incorrect status. expected '%d', got '%d'", exp.Status, resp.Status) + } + if resp.Size != exp.Size { + t.Errorf("incorrect size. expected '%d', got '%d'", exp.Size, resp.Size) + } + if resp.reader != nil { + resp.Content = make([]byte, resp.Size) + read, _ := resp.reader.Read(resp.Content) + if int64(read) != exp.Size { + t.Errorf("incorrect content length. expected '%s...', got '%s...'", read, exp.Size) + } + } + if !bytes.Equal(resp.Content, exp.Content) { + t.Errorf("incorrect content. expected '%s...', got '%s...'", string(exp.Content), string(resp.Content)) + } +} - return +func expResponse(content []byte, mimeType string, status int) *Response { + return &Response{mimeType, status, int64(len(content)), content} +} + +func testGet(t *testing.T, api *Api, bzzhash string) *testResponse { + reader, mimeType, status, err := api.Get(bzzhash) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + return &testResponse{reader, &Response{mimeType, status, reader.Size(), nil}} } func TestApiPut(t *testing.T) { - api, err := testApi() - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - defer api.dpa.Stop() - expContent := "hello" - expMimeType := "text/plain" - expStatus := 0 - expSize := len(expContent) - bzzhash, err := api.Put(expContent, expMimeType) - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - testGet(t, api, bzzhash, []byte(expContent), expMimeType, expStatus, expSize) -} - -func testGet(t *testing.T, api *Api, bzzhash string, expContent []byte, expMimeType string, expStatus int, expSize int) { - content, mimeType, status, size, err := api.Get(bzzhash) - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - if !bytes.Equal(content, expContent) { - t.Errorf("incorrect content. expected '%s...', got '%s...'", string(expContent), string(content)) - } - if mimeType != expMimeType { - t.Errorf("incorrect mimeType. expected '%s', got '%s'", expMimeType, mimeType) - } - if status != expStatus { - t.Errorf("incorrect status. expected '%d', got '%d'", expStatus, status) - } - if size != expSize { - t.Errorf("incorrect size. expected '%d', got '%d'", expSize, size) - } -} - -func TestApiDirUpload(t *testing.T) { - t.Skip("FIXME") - api, err := testApi() - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - bzzhash, err := api.Upload(path.Join(testDir, "test0"), "") - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - content, err := ioutil.ReadFile(path.Join(testDir, "test0", "index.html")) - testGet(t, api, path.Join(bzzhash, "index.html"), content, "text/html; charset=utf-8", 0, 202) - - content, err = ioutil.ReadFile(path.Join(testDir, "test0", "index.css")) - testGet(t, api, path.Join(bzzhash, "index.css"), content, "text/css", 0, 132) - - content, err = ioutil.ReadFile(path.Join(testDir, "test0", "img", "logo.png")) - testGet(t, api, path.Join(bzzhash, "img", "logo.png"), content, "image/png", 0, 18136) - - _, _, _, _, err = api.Get(bzzhash) - if err == nil { - t.Errorf("expected error: %v", err) - } -} - -func TestApiDirUploadModify(t *testing.T) { - t.Skip("FIXME") - api, err := testApi() - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - bzzhash, err := api.Upload(path.Join(testDir, "test0"), "") - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - bzzhash, err = api.Modify(bzzhash, "index.html", "", "") - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - bzzhash, err = api.Modify(bzzhash, "index2.html", "9ea1f60ebd80786d6005f6b256376bdb494a82496cd86fe8c307cdfb23c99e71", "text/html; charset=utf-8") - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - bzzhash, err = api.Modify(bzzhash, "img/logo.png", "9ea1f60ebd80786d6005f6b256376bdb494a82496cd86fe8c307cdfb23c99e71", "text/html; charset=utf-8") - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - content, err := ioutil.ReadFile(path.Join(testDir, "test0", "index.html")) - testGet(t, api, path.Join(bzzhash, "index2.html"), content, "text/html; charset=utf-8", 0, 202) - testGet(t, api, path.Join(bzzhash, "img", "logo.png"), content, "text/html; charset=utf-8", 0, 202) - - content, err = ioutil.ReadFile(path.Join(testDir, "test0", "index.css")) - testGet(t, api, path.Join(bzzhash, "index.css"), content, "text/css", 0, 132) - - _, _, _, _, err = api.Get(bzzhash) - if err == nil { - t.Errorf("expected error: %v", err) - } -} - -func TestApiDirUploadWithRootFile(t *testing.T) { - api, err := testApi() - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - bzzhash, err := api.Upload(path.Join(testDir, "test0"), "index.html") - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - content, err := ioutil.ReadFile(path.Join(testDir, "test0", "index.html")) - testGet(t, api, bzzhash, content, "text/html; charset=utf-8", 0, 202) -} - -func TestApiFileUpload(t *testing.T) { - api, err := testApi() - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - bzzhash, err := api.Upload(path.Join(testDir, "test0", "index.html"), "") - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - content, err := ioutil.ReadFile(path.Join(testDir, "test0", "index.html")) - testGet(t, api, path.Join(bzzhash, "index.html"), content, "text/html; charset=utf-8", 0, 202) -} - -func TestApiFileUploadWithRootFile(t *testing.T) { - api, err := testApi() - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - bzzhash, err := api.Upload(path.Join(testDir, "test0", "index.html"), "index.html") - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - content, err := ioutil.ReadFile(path.Join(testDir, "test0", "index.html")) - testGet(t, api, bzzhash, content, "text/html; charset=utf-8", 0, 202) + testApi(t, func(api *Api) { + content := "hello" + exp := expResponse([]byte(content), "text/plain", 0) + bzzhash, err := api.Put(content, exp.MimeType) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resp := testGet(t, api, bzzhash) + checkResponse(t, resp, exp) + }) } diff --git a/swarm/api/dns.go b/swarm/api/dns.go new file mode 100644 index 0000000000..c26722bca1 --- /dev/null +++ b/swarm/api/dns.go @@ -0,0 +1,58 @@ +package api + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/registrar" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/swarm/storage" +) + +// swarm domain name registry and resolver +// the DNS instance can be directly wrapped in rpc.Api +type DNS struct { + registrar registrar.VersionedRegistrar +} + +func NewDNS(registrar registrar.VersionedRegistrar) *DNS { + return &DNS{registrar} +} + +// Register involves sending a transaction, sender is an account with funds +// the same account is used to register the authors of commits +func (self *DNS) Register(sender common.Address, domain string, hash common.Hash) (err error) { + domainhash := common.BytesToHash(crypto.Sha3([]byte(domain))) + + if self.registrar != nil { + glog.V(logger.Debug).Infof("[DNR]: host '%s' (hash: '%v') to be registered as '%v'", domain, domainhash.Hex(), hash.Hex()) + _, err = self.registrar.Registry().SetHashToHash(sender, domainhash, hash) + } else { + err = fmt.Errorf("no registry: %v", err) + } + return +} + +type ErrResolve error + +func (self *DNS) Resolve(hostPort string) (contentHash storage.Key, err error) { + host := hostPort + var hash common.Hash + var version *big.Int + parts := domainAndVersion.Split(host, 3) + if len(parts) > 1 && parts[1] != "" { + host = parts[0] + version = common.Big(parts[1]) + } + hostHash := crypto.Sha3Hash([]byte(host)) + hash, err = self.registrar.Resolver(version).HashToHash(hostHash) + if err != nil { + err = fmt.Errorf("unable to resolve '%s': %v", hostPort, err) + } + contentHash = storage.Key(hash.Bytes()) + glog.V(logger.Debug).Infof("[DNR] resolve host '%s' to contentHash: '%v'", hostPort, contentHash) + return +} diff --git a/swarm/api/filesystem.go b/swarm/api/filesystem.go new file mode 100644 index 0000000000..eddd72dd0b --- /dev/null +++ b/swarm/api/filesystem.go @@ -0,0 +1,258 @@ +package api + +import ( + "bufio" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/swarm/storage" +) + +const maxParallelFiles = 5 + +type FileSystem struct { + api *Api +} + +func NewFileSystem(api *Api) *FileSystem { + return &FileSystem{api} +} + +// Upload replicates a local directory as a manifest file and uploads it +// using dpa store +// TODO: localpath should point to a manifest +func (self *FileSystem) Upload(lpath, index string) (string, error) { + var list []*manifestTrieEntry + localpath, err := filepath.Abs(filepath.Clean(lpath)) + if err != nil { + return "", err + } + + f, err := os.Open(localpath) + if err != nil { + return "", err + } + stat, err := f.Stat() + if err != nil { + return "", err + } + + var start int + if stat.IsDir() { + start = len(localpath) + glog.V(logger.Debug).Infof("[BZZ] uploading '%s'", localpath) + err = filepath.Walk(localpath, func(path string, info os.FileInfo, err error) error { + if (err == nil) && !info.IsDir() { + //fmt.Printf("lp %s path %s\n", localpath, path) + if len(path) <= start { + return fmt.Errorf("Path is too short") + } + if path[:start] != localpath { + return fmt.Errorf("Path prefix of '%s' does not match localpath '%s'", path, localpath) + } + entry := &manifestTrieEntry{ + Path: path, + } + list = append(list, entry) + } + return err + }) + if err != nil { + return "", err + } + } else { + dir := filepath.Dir(localpath) + start = len(dir) + if len(localpath) <= start { + return "", fmt.Errorf("Path is too short") + } + if localpath[:start] != dir { + return "", fmt.Errorf("Path prefix of '%s' does not match dir '%s'", localpath, dir) + } + entry := &manifestTrieEntry{ + Path: localpath, + } + list = append(list, entry) + } + + cnt := len(list) + errors := make([]error, cnt) + done := make(chan bool, maxParallelFiles) + dcnt := 0 + + for i, entry := range list { + if i >= dcnt+maxParallelFiles { + <-done + dcnt++ + } + go func(i int, entry *manifestTrieEntry, done chan bool) { + f, err := os.Open(entry.Path) + if err == nil { + stat, _ := f.Stat() + sr := io.NewSectionReader(f, 0, stat.Size()) + wg := &sync.WaitGroup{} + var hash storage.Key + hash, err = self.api.dpa.Store(sr, wg) + if hash != nil { + list[i].Hash = hash.String() + } + wg.Wait() + if err == nil { + first512 := make([]byte, 512) + fread, _ := sr.ReadAt(first512, 0) + if fread > 0 { + mimeType := http.DetectContentType(first512[:fread]) + if filepath.Ext(entry.Path) == ".css" { + mimeType = "text/css" + } + list[i].ContentType = mimeType + //fmt.Printf("%v %v %v\n", entry.Path, mimeType, filepath.Ext(entry.Path)) + } + } + f.Close() + } + errors[i] = err + done <- true + }(i, entry, done) + } + for dcnt < cnt { + <-done + dcnt++ + } + + trie := &manifestTrie{ + dpa: self.api.dpa, + } + for i, entry := range list { + if errors[i] != nil { + return "", errors[i] + } + entry.Path = RegularSlashes(entry.Path[start:]) + if entry.Path == index { + ientry := &manifestTrieEntry{ + Path: "", + Hash: entry.Hash, + ContentType: entry.ContentType, + } + trie.addEntry(ientry) + } + trie.addEntry(entry) + } + + err2 := trie.recalcAndStore() + var hs string + if err2 == nil { + hs = trie.hash.String() + } + return hs, err2 +} + +// Download replicates the manifest path structure on the local filesystem +// under localpath +func (self *FileSystem) Download(bzzpath, localpath string) error { + lpath, err := filepath.Abs(filepath.Clean(localpath)) + if err != nil { + return err + } + err = os.MkdirAll(lpath, os.ModePerm) + if err != nil { + return err + } + + //resolving host and port + key, _, path, err := self.api.parseAndResolve(bzzpath) + if err != nil { + return err + } + // if len(path) > 0 { + // path += "/" + // } + + trie, err := loadManifest(self.api.dpa, key) + if err != nil { + glog.V(logger.Debug).Infof("[BZZ] fs.Download: loadManifestTrie error: %v", err) + return err + } + + type downloadListEntry struct { + key storage.Key + path string + } + + var list []*downloadListEntry + var mde, mderr error + + prevPath := lpath + err = trie.listWithPrefix(path, func(entry *manifestTrieEntry, suffix string) { // TODO: paralellize + glog.V(logger.Detail).Infof("[BZZ] fs.Download: %#v", entry) + + key := common.Hex2Bytes(entry.Hash) + path := lpath + "/" + suffix + dir := filepath.Dir(path) + if dir != prevPath { + mde = os.MkdirAll(dir, os.ModePerm) + if mde != nil { + mderr = mde + } + prevPath = dir + } + if (mde == nil) && (path != dir+"/") { + list = append(list, &downloadListEntry{key: key, path: path}) + } + }) + if err == nil { + err = mderr + } + + cnt := len(list) + errors := make([]error, cnt) + done := make(chan bool, maxParallelFiles) + dcnt := 0 + + for i, entry := range list { + if i >= dcnt+maxParallelFiles { + <-done + dcnt++ + } + go func(i int, entry *downloadListEntry, done chan bool) { + f, err := os.Create(entry.path) // TODO: path separators + if err == nil { + reader := self.api.dpa.Retrieve(entry.key) + writer := bufio.NewWriter(f) + _, err = io.CopyN(writer, reader, reader.Size()) // TODO: handle errors + err2 := writer.Flush() + if err == nil { + err = err2 + } + err2 = f.Close() + if err == nil { + err = err2 + } + } + + errors[i] = err + done <- true + }(i, entry, done) + } + for dcnt < cnt { + <-done + dcnt++ + } + + if err != nil { + return err + } + for i, _ := range list { + if errors[i] != nil { + return errors[i] + } + } + return err +} diff --git a/swarm/api/filesystem_test.go b/swarm/api/filesystem_test.go new file mode 100644 index 0000000000..2d4eceea96 --- /dev/null +++ b/swarm/api/filesystem_test.go @@ -0,0 +1,180 @@ +package api + +import ( + "io/ioutil" + "os" + "path" + "runtime" + "testing" + + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" +) + +var ( + testDir string + testDownloadDir string +) + +func init() { + glog.SetV(logger.Detail) + glog.SetToStderr(true) + _, filename, _, _ := runtime.Caller(1) + testDir = path.Join(path.Dir(filename), "../test") + testDownloadDir, _ = ioutil.TempDir(os.TempDir(), "bzz-test") +} + +func testFileSystem(t *testing.T, f func(*FileSystem)) { + testApi(t, func(api *Api) { + f(NewFileSystem(api)) + }) +} + +func readPath(t *testing.T, parts ...string) []byte { + file := path.Join(parts...) + content, err := ioutil.ReadFile(file) + if err != nil { + t.Fatalf("unexpected error reading '%v': %v", file, err) + } + return content +} + +func TestApiDirUpload0(t *testing.T) { + // t.Skip("FIXME") + testFileSystem(t, func(fs *FileSystem) { + api := fs.api + bzzhash, err := fs.Upload(path.Join(testDir, "test0"), "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + content := readPath(t, testDir, "test0", "index.html") + resp := testGet(t, api, bzzhash+"/index.html") + exp := expResponse(content, "text/html; charset=utf-8", 0) + checkResponse(t, resp, exp) + + content = readPath(t, testDir, "test0", "index.css") + resp = testGet(t, api, bzzhash+"/index.css") + exp = expResponse(content, "text/css", 0) + checkResponse(t, resp, exp) + + content = readPath(t, testDir, "test0", "img", "logo.png") + resp = testGet(t, api, bzzhash+"/img/logo.png") + exp = expResponse(content, "image/png", 0) + + _, _, _, err = api.Get(bzzhash) + if err == nil { + t.Fatalf("expected error: %v", err) + } + + downloadDir := path.Join(testDownloadDir, "test0") + os.RemoveAll(downloadDir) + defer os.RemoveAll(downloadDir) + err = fs.Download(bzzhash, downloadDir) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + newbzzhash, err := fs.Upload(downloadDir, "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if bzzhash != newbzzhash { + t.Fatalf("download %v reuploaded has incorrect hash, expected %v, got %v", downloadDir, bzzhash, newbzzhash) + } + + }) +} + +func TestApiDirUploadModify(t *testing.T) { + // t.Skip("FIXME") + testFileSystem(t, func(fs *FileSystem) { + api := fs.api + bzzhash, err := fs.Upload(path.Join(testDir, "test0"), "") + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + bzzhash, err = api.Modify(bzzhash, "index.html", "", "") + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + bzzhash, err = api.Modify(bzzhash, "index2.html", "9ea1f60ebd80786d6005f6b256376bdb494a82496cd86fe8c307cdfb23c99e71", "text/html; charset=utf-8") + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + bzzhash, err = api.Modify(bzzhash, "img/logo.png", "9ea1f60ebd80786d6005f6b256376bdb494a82496cd86fe8c307cdfb23c99e71", "text/html; charset=utf-8") + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + content := readPath(t, testDir, "test0", "index.html") + resp := testGet(t, api, bzzhash+"/index2.html") + exp := expResponse(content, "text/html; charset=utf-8", 0) + checkResponse(t, resp, exp) + + resp = testGet(t, api, bzzhash+"/img/logo.png") + exp = expResponse(content, "text/html; charset=utf-8", 0) + checkResponse(t, resp, exp) + + content, err = ioutil.ReadFile(path.Join(testDir, "test0", "index.css")) + resp = testGet(t, api, bzzhash+"/index.css") + exp = expResponse(content, "text/css", 0) + + _, _, _, err = api.Get(bzzhash) + if err == nil { + t.Errorf("expected error: %v", err) + } + }) +} + +func TestApiDirUploadWithRootFile(t *testing.T) { + testFileSystem(t, func(fs *FileSystem) { + api := fs.api + bzzhash, err := fs.Upload(path.Join(testDir, "test0"), "index.html") + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + content := readPath(t, testDir, "test0", "index.html") + resp := testGet(t, api, bzzhash) + exp := expResponse(content, "text/html; charset=utf-8", 0) + checkResponse(t, resp, exp) + }) +} + +func TestApiFileUpload(t *testing.T) { + testFileSystem(t, func(fs *FileSystem) { + api := fs.api + bzzhash, err := fs.Upload(path.Join(testDir, "test0", "index.html"), "") + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + content := readPath(t, testDir, "test0", "index.html") + resp := testGet(t, api, bzzhash+"/index.html") + exp := expResponse(content, "text/html; charset=utf-8", 0) + checkResponse(t, resp, exp) + }) +} + +func TestApiFileUploadWithRootFile(t *testing.T) { + testFileSystem(t, func(fs *FileSystem) { + api := fs.api + bzzhash, err := fs.Upload(path.Join(testDir, "test0", "index.html"), "index.html") + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + content := readPath(t, testDir, "test0", "index.html") + resp := testGet(t, api, bzzhash) + exp := expResponse(content, "text/html; charset=utf-8", 0) + checkResponse(t, resp, exp) + }) +} diff --git a/swarm/api/http/roundtripper.go b/swarm/api/http/roundtripper.go new file mode 100644 index 0000000000..da6a63a255 --- /dev/null +++ b/swarm/api/http/roundtripper.go @@ -0,0 +1,49 @@ +package http + +import ( + "fmt" + "net/http" + + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" +) + +/* +http roundtripper to register for bzz url scheme +see https://github.com/ethereum/go-ethereum/issues/2040 +Usage: + +import ( + "github.com/ethereum/go-ethereum/common/httpclient" + "github.com/ethereum/go-ethereum/swarm/api/http" +) +client := httpclient.New() +// for (private) swarm proxy running locally +client.RegisterScheme("bzz", &http.RoundTripper{Port: port}) +// for public swarm gateway +client.RegisterScheme(scheme, &http.RoundTripper{Host: host, Port: port}) + +The port you give the Roundtripper is the port the swarm proxy is listening on. +If Host is left empty, localhost is assumed. + +Using a public gateway, the above few lines gives you the leanest +bzz-scheme aware read-only http client. You really only ever need this +if you need go-native swarm access to bzz addresses, e.g., +github.com/ethereum/go-ethereum/common/natspec + +*/ + +type RoundTripper struct { + Host string + Port string +} + +func (self *RoundTripper) RoundTrip(req *http.Request) (resp *http.Response, err error) { + host := self.Host + if len(host) == 0 { + host = "localhost" + } + url := fmt.Sprintf("http://%s:%s/%s/%s", host, self.Port, req.URL.Host, req.URL.Path) + glog.V(logger.Info).Infof("[BZZ] roundtripper: proxying request '%s' to '%s'", req.RequestURI, url) + return http.Get(url) +} diff --git a/swarm/api/roundtripper_test.go b/swarm/api/http/roundtripper_test.go similarity index 95% rename from swarm/api/roundtripper_test.go rename to swarm/api/http/roundtripper_test.go index e0b15658dc..51bcd2e41c 100644 --- a/swarm/api/roundtripper_test.go +++ b/swarm/api/http/roundtripper_test.go @@ -1,4 +1,4 @@ -package api +package http import ( "io/ioutil" @@ -22,7 +22,7 @@ func TestRoundTripper(t *testing.T) { }) go http.ListenAndServe(":8600", serveMux) - rt := &RoundTripper{"8600"} + rt := &RoundTripper{Port: "8600"} client := httpclient.New("/") client.RegisterProtocol("bzz", rt) diff --git a/swarm/api/http.go b/swarm/api/http/server.go similarity index 91% rename from swarm/api/http.go rename to swarm/api/http/server.go index b1cd039cce..efa3651652 100644 --- a/swarm/api/http.go +++ b/swarm/api/http/server.go @@ -1,7 +1,7 @@ /* A simple http server interface to Swarm */ -package api +package http import ( "bytes" @@ -14,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/logger" "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/swarm/api" ) const ( @@ -41,7 +42,7 @@ type sequentialReader struct { // https://github.com/atom/electron/blob/master/docs/api/protocol.md // starts up http server -func StartHttpServer(api *Api, port string) { +func StartHttpServer(api *api.Api, port string) { serveMux := http.NewServeMux() serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { handler(w, r, api) @@ -50,7 +51,7 @@ func StartHttpServer(api *Api, port string) { glog.V(logger.Info).Infof("[BZZ] Swarm HTTP proxy started on localhost:%s", port) } -func handler(w http.ResponseWriter, r *http.Request, api *Api) { +func handler(w http.ResponseWriter, r *http.Request, a *api.Api) { requestURL := r.URL // This is wrong // if requestURL.Host == "" { @@ -78,7 +79,7 @@ func handler(w http.ResponseWriter, r *http.Request, api *Api) { switch { case r.Method == "POST" || r.Method == "PUT": - key, err := api.dpa.Store(io.NewSectionReader(&sequentialReader{ + key, err := a.Store(io.NewSectionReader(&sequentialReader{ reader: r.Body, ahead: make(map[int64]chan bool), }, 0, r.ContentLength), nil) @@ -102,11 +103,11 @@ func handler(w http.ResponseWriter, r *http.Request, api *Api) { http.Error(w, "No PUT to /raw allowed.", http.StatusBadRequest) return } else { - path = regularSlashes(path) + path = api.RegularSlashes(path) mime := r.Header.Get("Content-Type") // TODO proper root hash separation glog.V(logger.Debug).Infof("[BZZ] Modify '%s' to store %v as '%s'.", path, key.Log(), mime) - newKey, err := api.Modify(path[:64], path[65:], common.Bytes2Hex(key), mime) + newKey, err := a.Modify(path[:64], path[65:], common.Bytes2Hex(key), mime) if err == nil { glog.V(logger.Debug).Infof("[BZZ] Swarm replaced manifest by '%s'", newKey) w.Header().Set("Content-Type", "text/plain") @@ -122,9 +123,9 @@ func handler(w http.ResponseWriter, r *http.Request, api *Api) { http.Error(w, "No DELETE to /raw allowed.", http.StatusBadRequest) return } else { - path = regularSlashes(path) + path = api.RegularSlashes(path) glog.V(logger.Debug).Infof("[BZZ] Delete '%s'.", path) - newKey, err := api.Modify(path[:64], path[65:], "", "") + newKey, err := a.Modify(path[:64], path[65:], "", "") if err == nil { glog.V(logger.Debug).Infof("[BZZ] Swarm replaced manifest by '%s'", newKey) w.Header().Set("Content-Type", "text/plain") @@ -138,7 +139,7 @@ func handler(w http.ResponseWriter, r *http.Request, api *Api) { path = trailingSlashes.ReplaceAllString(path, "") if raw { // resolving host - key, err := api.Resolve(path) + key, err := a.Resolve(path) if err != nil { glog.V(logger.Error).Infof("[BZZ] Swarm: %v", err) http.Error(w, err.Error(), http.StatusBadRequest) @@ -146,7 +147,7 @@ func handler(w http.ResponseWriter, r *http.Request, api *Api) { } // retrieving content - reader := api.dpa.Retrieve(key) + reader := a.Retrieve(key) glog.V(logger.Debug).Infof("[BZZ] Swarm: Reading %d bytes.", reader.Size()) // setting mime type @@ -165,10 +166,9 @@ func handler(w http.ResponseWriter, r *http.Request, api *Api) { glog.V(logger.Debug).Infof("[BZZ] Swarm: Structured GET request '%s' received.", uri) - // call to api.getPath on uri - reader, mimeType, status, err := api.getPath(path) + reader, mimeType, status, err := a.Get(path) if err != nil { - if _, ok := err.(errResolve); ok { + if _, ok := err.(api.ErrResolve); ok { glog.V(logger.Debug).Infof("[BZZ] Swarm: %v", err) status = http.StatusBadRequest } else { diff --git a/swarm/api/manifest.go b/swarm/api/manifest.go index c1781ab7d3..42942a0213 100644 --- a/swarm/api/manifest.go +++ b/swarm/api/manifest.go @@ -290,7 +290,7 @@ func (self *manifestTrie) findPrefixOf(path string) (entry *manifestTrieEntry, p // file system manifest always contains regularized paths // no leading or trailing slashes, only single slashes inside -func regularSlashes(path string) (res string) { +func RegularSlashes(path string) (res string) { for i := 0; i < len(path); i++ { if (path[i] != '/') || ((i > 0) && (path[i-1] != '/')) { res = res + path[i:i+1] @@ -303,7 +303,7 @@ func regularSlashes(path string) (res string) { } func (self *manifestTrie) getEntry(spath string) (entry *manifestTrieEntry, fullpath string) { - path := regularSlashes(spath) + path := RegularSlashes(spath) var pos int entry, pos = self.findPrefixOf(path) return entry, path[:pos] diff --git a/swarm/api/roundtripper.go b/swarm/api/roundtripper.go deleted file mode 100644 index 3dad7d04ef..0000000000 --- a/swarm/api/roundtripper.go +++ /dev/null @@ -1,22 +0,0 @@ -package api - -import ( - "fmt" - "net/http" - - "github.com/ethereum/go-ethereum/logger" - "github.com/ethereum/go-ethereum/logger/glog" - - // "github.com/ethereum/go-ethereum/common/httpclient" - // "github.com/ethereum/go-ethereum/jsre" -) - -type RoundTripper struct { - Port string -} - -func (self *RoundTripper) RoundTrip(req *http.Request) (resp *http.Response, err error) { - url := fmt.Sprintf("http://localhost:%s/%s/%s", self.Port, req.URL.Host, req.URL.Path) - glog.V(logger.Info).Infof("[BZZ] roundtripper: proxying request '%s' to '%s'", req.RequestURI, url) - return http.Get(url) -} diff --git a/swarm/api/storage.go b/swarm/api/storage.go new file mode 100644 index 0000000000..903cbf61a0 --- /dev/null +++ b/swarm/api/storage.go @@ -0,0 +1,54 @@ +package api + +import ( +// "github.com/ethereum/go-ethereum/common" +// "github.com/ethereum/go-ethereum/logger" +// "github.com/ethereum/go-ethereum/logger/glog" +// "github.com/ethereum/go-ethereum/swarm/storage" +) + +type Response struct { + MimeType string + Status int + Size int64 + Content []byte +} + +// implements a service +type Storage struct { + api *Api +} + +func NewStorage(api *Api) *Storage { + return &Storage{api} +} + +// Put uploads the content to the swarm with a simple manifest speficying +// its content type +func (self *Storage) Put(content, contentType string) (string, error) { + return self.api.Put(content, contentType) +} + +// Get retrieves the content from bzzpath and reads the response in full +// It returns the SwarmResponse object, which serialises containing the +// response body as the value of the Content field +// NOTE: if error is non-nil, SwarmResponse may still have partial content +// the actual size of which is given in len(resp.Content), while the expected +// size is resp.Size +func (self *Storage) Get(bzzpath string) (*Response, error) { + reader, mimeType, status, err := self.api.Get(bzzpath) + if err != nil { + return nil, err + } + expsize := reader.Size() + body := make([]byte, expsize) + size, err := reader.Read(body) + if int64(size) == expsize { + err = nil + } + return &Response{mimeType, status, expsize, body[:size]}, err +} + +func (self *Storage) Modify(rootHash, path, contentHash, contentType string) (newRootHash string, err error) { + return self.api.Modify(rootHash, path, contentHash, contentType) +} diff --git a/swarm/api/storage_test.go b/swarm/api/storage_test.go new file mode 100644 index 0000000000..ef901002e5 --- /dev/null +++ b/swarm/api/storage_test.go @@ -0,0 +1,32 @@ +package api + +import ( + "testing" +) + +func testStorage(t *testing.T, f func(*Storage)) { + testApi(t, func(api *Api) { + f(NewStorage(api)) + }) +} + +func TestStoragePutGet(t *testing.T) { + testStorage(t, func(api *Storage) { + content := "hello" + exp := expResponse([]byte(content), "text/plain", 0) + bzzhash, err := api.Put(content, exp.MimeType) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // to check put against the Api#Get + resp0 := testGet(t, api.api, bzzhash) + checkResponse(t, resp0, exp) + + // check storage#Get + resp, err := api.Get(bzzhash) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + checkResponse(t, &testResponse{nil, resp}, exp) + }) +} diff --git a/swarm/storage/chunkreader.go b/swarm/storage/chunkreader.go index b147c85bf8..cfa79df7c6 100644 --- a/swarm/storage/chunkreader.go +++ b/swarm/storage/chunkreader.go @@ -8,17 +8,18 @@ import ( type Bounded interface { Size() int64 + io.Reader } type Sliced interface { Slice(int64, int64) (b []byte, err error) + io.Reader } // Size, Seek, Read, ReadAt type SectionReader interface { Bounded io.Seeker - io.Reader io.ReaderAt } diff --git a/swarm/swarm.go b/swarm/swarm.go index d7edbfbcd8..86e5b2c1b9 100644 --- a/swarm/swarm.go +++ b/swarm/swarm.go @@ -15,15 +15,23 @@ import ( "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p/discover" + rpc "github.com/ethereum/go-ethereum/rpc/v2" "github.com/ethereum/go-ethereum/swarm/api" + httpapi "github.com/ethereum/go-ethereum/swarm/api/http" "github.com/ethereum/go-ethereum/swarm/network" "github.com/ethereum/go-ethereum/swarm/storage" ) +const ( + Namespace = "bzz" + Version = "0.1" // versioning reflect POC and release versions +) + // the swarm stack type Swarm struct { config *api.Config // swarm configuration api *api.Api // high level api layer (fs/manifest) + dns api.Resolver // DNS registrar dbAccess *network.DbAccess // access to local chunk db iterator and storage counter storage storage.ChunkStore // internal access to storage, common interface to cloud storage backends dpa *storage.DPA // distributed preimage archive, the local API to the storage with document level storage/retrieval support @@ -78,9 +86,9 @@ func NewSwarm(stack *node.ServiceContext, config *api.Config, swapEnabled bool) // setup cloud storage internal access layer self.storage = storage.NewNetStore(hash, lstore, cloud, config.StoreParams) - glog.V(logger.Debug).Infof("[BZZ] -> Level 0: swarm net store shared access layer to Swarm Chunk Store") + glog.V(logger.Debug).Infof("[BZZ] -> swarm net store shared access layer to Swarm Chunk Store") - // set up Depo (storage handler = remote cloud storage access layer) + // set up Depo (storage handler = cloud storage access layer for incoming remote requests) self.depo = network.NewDepo(hash, lstore, self.storage) glog.V(logger.Debug).Infof("[BZZ] -> REmote Access to CHunks") @@ -89,14 +97,17 @@ func NewSwarm(stack *node.ServiceContext, config *api.Config, swapEnabled bool) glog.V(logger.Debug).Infof("[BZZ] -> Local Access to Swarm") // Swarm Hash Merklised Chunking for Arbitrary-length Document/File storage self.dpa = storage.NewDPA(dpaChunkStore, self.config.ChunkerParams) - glog.V(logger.Debug).Infof("[BZZ] -> Level 1: Document/File API") + glog.V(logger.Debug).Infof("[BZZ] -> Content Store API") // set up high level api backend := api.NewEthApi(ethereum) backend.UpdateState() - self.api = api.NewApi(self.dpa, ethreg.New(backend), self.config) + self.dns = api.NewDNS(ethreg.New(backend)) + glog.V(logger.Debug).Infof("[BZZ] -> Swarm Domain Registrar") + + self.api = api.NewApi(self.dpa, self.dns) // Manifests for Smart Hosting - glog.V(logger.Debug).Infof("[BZZ] -> Level 2: Collection/Directory API") + glog.V(logger.Debug).Infof("[BZZ] -> Web3 virtual server API") // set chequebook if swapEnabled { @@ -143,7 +154,7 @@ func (self *Swarm) Start(net *p2p.Server) error { // start swarm http proxy server if self.config.Port != "" { - go api.StartHttpServer(self.api, self.config.Port) + go httpapi.StartHttpServer(self.api, self.config.Port) } glog.V(logger.Debug).Infof("[BZZ] Swarm http proxy started on port: %v", self.config.Port) @@ -154,7 +165,7 @@ func (self *Swarm) Start(net *p2p.Server) error { "bzz": self.config.Port, } for scheme, port := range schemes { - self.client.RegisterScheme(scheme, &api.RoundTripper{Port: port}) + self.client.RegisterScheme(scheme, &httpapi.RoundTripper{Port: port}) } glog.V(logger.Debug).Infof("[BZZ] Swarm protocol handlers registered for url schemes: %v", schemes) @@ -182,8 +193,21 @@ func (self *Swarm) Protocols() []p2p.Protocol { return []p2p.Protocol{proto} } -func (self *Swarm) Api() *api.Api { - return self.api +// implements node.Service +// Apis returns the RPC Api descriptors the Swarm implementation offers +func (self *Swarm) Apis() []rpc.API { + return []rpc.API{ + // public APIs. + rpc.API{Namespace, Version, api.NewStorage(self.api), true}, + rpc.API{Namespace, Version, self.dns, true}, + rpc.API{Namespace, Version, &Info{self.config, chequebook.ContractParams}, true}, + // admin APIs + rpc.API{Namespace, Version, api.NewFileSystem(self.api), false}, + // rpc.API{Namespace, Version, test.New(self), false}, + // rpc.API{Namespace, Version, api.NewAdmin(self), false}, + // TODO: external apis exposed + rpc.API{"chequebook", chequebook.Version, chequebook.NewApi(self.config.Swap.Chequebook()), true}, + } } // Backend interface implemented by eth or JSON-IPC client @@ -223,9 +247,19 @@ func NewLocalSwarm(datadir, port string) (self *Swarm, err error) { } self = &Swarm{ - api: api.NewApi(dpa, nil, config), + api: api.NewApi(dpa, nil), config: config, } return } + +// serialisable info about swarm +type Info struct { + *api.Config + *chequebook.Params +} + +func (self *Info) Info() *Info { + return self +}