diff --git a/cmd/geth/js_bzz_test.go b/cmd/geth/js_bzz_test.go deleted file mode 100644 index 2d8b70efb9..0000000000 --- a/cmd/geth/js_bzz_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package main - -import ( - "io/ioutil" - "os" - "testing" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/swarm" - "github.com/ethereum/go-ethereum/swarm/api" -) - -var port = 8500 - -func bzzREPL(t *testing.T, configf func(*api.Config)) (string, string, *testjethre, *node.Node) { - prvKey, err := crypto.GenerateKey() - if err != nil { - t.Fatal("unable to generate key") - } - bzztmp, err := ioutil.TempDir("", "bzz-js-test") - config, err := api.NewConfig(bzztmp, common.Address{}, prvKey) - if err != nil { - t.Fatal("unable to configure swarm") - } - if configf != nil { - configf(config) - } - tmp, repl, stack := testREPL(t, func(n *node.Node) { - if err := n.Register(func(ctx *node.ServiceContext) (node.Service, error) { - return swarm.NewSwarm(ctx, config, false) - }); err != nil { - t.Fatalf("Failed to register the Swarm service: %v", err) - } - }) - return bzztmp, tmp, repl, stack -} - -func withREPL(t *testing.T, cf func(*api.Config), f func(repl *testjethre)) { - bzztmp, tmp, repl, stack := bzzREPL(t, cf) - defer stack.Stop() - defer os.RemoveAll(tmp) - defer os.RemoveAll(bzztmp) - f(repl) -} - -func TestBzzPutGet(t *testing.T) { - withREPL(t, - func(c *api.Config) { - c.Port = "" - }, func(repl *testjethre) { - if checkEvalJSON(t, repl, `hash = bzz.put("console.log(\"hello from console\")", "application/javascript")`, `"97f1b7c7ea12468fd37c262383b9aa862d0cfbc4fc7218652374679fc5cf40cd"`) != nil { - return - } - want := `{"content":"console.log(\"hello from console\")","contentType":"application/javascript","size":"33","status":"0"}` - if checkEvalJSON(t, repl, `bzz.get(hash)`, want) != nil { - return - } - }) -} - -// the server can be initialized only once per test session ! -// until we implement a stoppable http server -// further http tests will need to make sure the correct server is running -func TestHTTP(t *testing.T) { - withREPL(t, nil, func(repl *testjethre) { - if checkEvalJSON(t, repl, `hash = bzz.put("f42 = function() { return 42 }", "application/javascript")`, `"e6847876f00102441f850b2d438a06d10e3bf24e6a0a76d47b073a86c3c2f9ac"`) != nil { - return - } - if checkEvalJSON(t, repl, `admin.httpGet("bzz://"+hash)`, `"f42 = function() { return 42 }"`) != nil { - return - } - - // if checkEvalJSON(t, repl, `http.loadScript("bzz://"+hash)`, `true`) != nil { - // return - // } - - // if checkEvalJSON(t, repl, `f42()`, `42`) != nil { - // return - // } - }) -} 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/eth/downloader/modes.go b/eth/downloader/modes.go index 743d827c76..e8cc451ebb 100644 --- a/eth/downloader/modes.go +++ b/eth/downloader/modes.go @@ -35,6 +35,7 @@ const ( TxPoolApiName = "txpool" PersonalApiName = "personal" Web3ApiName = "web3" + ChequebookApiName = "chequebook" JsonRpcVersion = "2.0" ) @@ -42,7 +43,7 @@ const ( var ( // All API's AllApis = strings.Join([]string{ - AdminApiName, BzzApiName, DbApiName, EthApiName, DebugApiName, MinerApiName, NetApiName, + AdminApiName, BzzApiName, ChequebookApiName, DbApiName, EthApiName, DebugApiName, MinerApiName, NetApiName, ShhApiName, TxPoolApiName, PersonalApiName, Web3ApiName, }, ",") ) 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/chequebook_js.go b/rpc/api/chequebook_js.go new file mode 100644 index 0000000000..4254997284 --- /dev/null +++ b/rpc/api/chequebook_js.go @@ -0,0 +1,50 @@ +// 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 Chequebook_JS = ` +web3._extend({ + property: 'chequebook', + methods: + [ + new web3._extend.Method({ + name: 'deposit', + call: 'chequebook_deposit', + params: 1, + inputFormatter: [null] + }), + new web3._extend.Method({ + name: 'info', + call: 'chequebook_info', + params: 1, + inputFormatter: [null] + }), + new web3._extend.Method({ + name: 'cash', + call: 'chequebook_cash', + params: 1, + inputFormatter: [null] + }), + new web3._extend.Method({ + name: 'issue', + call: 'chequebook_issue', + params: 2, + inputFormatter: [null, null] + }), + ] +}); +` diff --git a/rpc/api/utils.go b/rpc/api/utils.go index d52f6b3dc3..336d197f5a 100644 --- a/rpc/api/utils.go +++ b/rpc/api/utils.go @@ -64,10 +64,8 @@ var ( "verbosity", }, "bzz": []string{ + "version", "info", - "issue", - "cash", - "deposit", "register", "resolve", "download", @@ -76,6 +74,13 @@ var ( "put", "modify", }, + "chequebook": []string{ + "version", + "info", + "issue", + "cash", + "deposit", + }, "db": []string{ "getString", "putString", @@ -129,7 +134,7 @@ var ( "sign", "syncing", }, - "miner": []string{ + "miner ": []string{ "hashrate", "makeDAG", "setEtherbase", @@ -195,7 +200,7 @@ func ParseApiString(apistr string, codec codec.Codec, xeth *xeth.XEth, stack *no case shared.AdminApiName: apis[i] = NewAdminApi(xeth, stack, codec) case shared.BzzApiName: - apis[i] = NewBzzApi(stack, codec) + case shared.ChequebookApiName: case shared.DebugApiName: apis[i] = NewDebugApi(xeth, eth, codec) case shared.DbApiName: @@ -229,6 +234,8 @@ func Javascript(name string) string { return Admin_JS case shared.BzzApiName: return Bzz_JS + case shared.ChequebookApiName: + return Chequebook_JS case shared.DebugApiName: return Debug_JS case shared.DbApiName: 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..7db209c9d4 100644 --- a/swarm/api/api_test.go +++ b/swarm/api/api_test.go @@ -1,205 +1,86 @@ package api import ( - "bytes" + // "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) + defer 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() +} - 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 { + content := make([]byte, resp.Size) + read, _ := resp.reader.Read(content) + if int64(read) != exp.Size { + t.Errorf("incorrect content length. expected '%s...', got '%s...'", read, exp.Size) + } + resp.Content = string(content) + } + if resp.Content != exp.Content { + // 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 { +func expResponse(content string, mimeType string, status int) *Response { + return &Response{mimeType, status, int64(len(content)), content} +} + +// func testGet(t *testing.T, api *Api, bzzhash string) *testResponse { +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(), ""}} + // 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(content, "text/plain", 0) + // 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/config_test.go b/swarm/api/config_test.go index 44bb484667..9bed72d23d 100644 --- a/swarm/api/config_test.go +++ b/swarm/api/config_test.go @@ -91,6 +91,7 @@ func TestConfigWriteRead(t *testing.T) { t.Fatalf("default config file cannot be read: %v", err) } exp := strings.Replace(defaultConfig, "TMPDIR", tmp, -1) + exp = strings.Replace(exp, "\\", "\\\\", -1) if string(data) != exp { t.Fatalf("default config mismatch:\nexpected:\n'%v'\ngot:\n'%v'", exp, string(data)) 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..174d8833a1 --- /dev/null +++ b/swarm/api/filesystem_test.go @@ -0,0 +1,176 @@ +package api + +import ( + "io/ioutil" + "os" + "path" + "runtime" + "testing" +) + +var ( + testDir string + testDownloadDir string +) + +func init() { + _, 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) string { + // 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 string(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 = readPath(t, 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..6563c75ae0 --- /dev/null +++ b/swarm/api/storage.go @@ -0,0 +1,56 @@ +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 + Content string +} + +// 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 Response object, which serialises containing the +// response body as the value of the Content field +// NOTE: if error is non-nil, sResponse 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 + } + glog.V(logger.Detail).Infof("body: %s", body[:size]) + return &Response{mimeType, status, expsize, string(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..44fa7636dc --- /dev/null +++ b/swarm/api/storage_test.go @@ -0,0 +1,33 @@ +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(content, "text/plain", 0) + // 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/swarm.go b/swarm/swarm.go index d7edbfbcd8..f323ffd368 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 +}