diff options
Diffstat (limited to 'swarm/api')
-rw-r--r-- | swarm/api/api.go | 191 | ||||
-rw-r--r-- | swarm/api/api_test.go | 117 | ||||
-rw-r--r-- | swarm/api/config.go | 132 | ||||
-rw-r--r-- | swarm/api/config_test.go | 124 | ||||
-rw-r--r-- | swarm/api/filesystem.go | 283 | ||||
-rw-r--r-- | swarm/api/filesystem_test.go | 187 | ||||
-rw-r--r-- | swarm/api/http/roundtripper.go | 69 | ||||
-rw-r--r-- | swarm/api/http/roundtripper_test.go | 68 | ||||
-rw-r--r-- | swarm/api/http/server.go | 286 | ||||
-rw-r--r-- | swarm/api/manifest.go | 336 | ||||
-rw-r--r-- | swarm/api/manifest_test.go | 80 | ||||
-rw-r--r-- | swarm/api/storage.go | 70 | ||||
-rw-r--r-- | swarm/api/storage_test.go | 49 | ||||
-rw-r--r-- | swarm/api/testapi.go | 46 | ||||
-rw-r--r-- | swarm/api/testdata/test0/img/logo.png | bin | 0 -> 18136 bytes | |||
-rw-r--r-- | swarm/api/testdata/test0/index.css | 9 | ||||
-rw-r--r-- | swarm/api/testdata/test0/index.html | 10 |
17 files changed, 2057 insertions, 0 deletions
diff --git a/swarm/api/api.go b/swarm/api/api.go new file mode 100644 index 000000000..673cff350 --- /dev/null +++ b/swarm/api/api.go @@ -0,0 +1,191 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library 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. +// +// The go-ethereum library 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 the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +package api + +import ( + "fmt" + "io" + "regexp" + "strings" + "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" +) + +var ( + hashMatcher = regexp.MustCompile("^[0-9A-Fa-f]{64}") + slashes = regexp.MustCompile("/+") + domainAndVersion = regexp.MustCompile("[@:;,]+") +) + +type Resolver interface { + Resolve(string) (common.Hash, 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 + dns Resolver +} + +//the api constructor initialises +func NewApi(dpa *storage.DPA, dns Resolver) (self *Api) { + self = &Api{ + dpa: dpa, + dns: dns, + } + return +} + +// DPA reader API +func (self *Api) Retrieve(key storage.Key) storage.LazySectionReader { + return self.dpa.Retrieve(key) +} + +func (self *Api) Store(data io.Reader, size int64, wg *sync.WaitGroup) (key storage.Key, err error) { + return self.dpa.Store(data, size, wg, nil) +} + +type ErrResolve error + +// DNS Resolver +func (self *Api) Resolve(hostPort string, nameresolver bool) (storage.Key, error) { + if hashMatcher.MatchString(hostPort) || self.dns == nil { + glog.V(logger.Detail).Infof("host is a contentHash: '%v'", hostPort) + return storage.Key(common.Hex2Bytes(hostPort)), nil + } + if !nameresolver { + return nil, fmt.Errorf("'%s' is not a content hash value.", hostPort) + } + contentHash, err := self.dns.Resolve(hostPort) + if err != nil { + err = ErrResolve(err) + glog.V(logger.Warn).Infof("DNS error : %v", err) + } + glog.V(logger.Detail).Infof("host lookup: %v -> %v", err) + return contentHash[:], err +} + +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 + for len(parts[i]) == 0 { + i++ + } + hostPort = parts[i] + for i < len(parts)-1 { + i++ + if len(path) > 0 { + path = path + "/" + parts[i] + } else { + path = parts[i] + } + } + glog.V(logger.Debug).Infof("host: '%s', path '%s' requested.", hostPort, path) + return +} + +func (self *Api) parseAndResolve(uri string, nameresolver bool) (key storage.Key, hostPort, path string, err error) { + hostPort, path = parse(uri) + //resolving host and port + contentHash, err := self.Resolve(hostPort, nameresolver) + glog.V(logger.Debug).Infof("Resolved '%s' to contentHash: '%s', path: '%s'", uri, contentHash, path) + return contentHash[:], hostPort, path, err +} + +// Put provides singleton manifest creation on top of dpa store +func (self *Api) Put(content, contentType string) (string, error) { + r := strings.NewReader(content) + wg := &sync.WaitGroup{} + key, err := self.dpa.Store(r, int64(len(content)), wg, nil) + if err != nil { + return "", err + } + manifest := fmt.Sprintf(`{"entries":[{"hash":"%v","contentType":"%s"}]}`, key, contentType) + r = strings.NewReader(manifest) + key, err = self.dpa.Store(r, int64(len(manifest)), wg, nil) + if err != nil { + return "", err + } + wg.Wait() + 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, nameresolver bool) (reader storage.LazySectionReader, mimeType string, status int, err error) { + + key, _, path, err := self.parseAndResolve(uri, nameresolver) + quitC := make(chan bool) + trie, err := loadManifest(self.dpa, key, quitC) + if err != nil { + glog.V(logger.Warn).Infof("loadManifestTrie error: %v", err) + return + } + + glog.V(logger.Detail).Infof("getEntry(%s)", path) + entry, _ := trie.getEntry(path) + if entry != nil { + key = common.Hex2Bytes(entry.Hash) + status = entry.Status + mimeType = entry.ContentType + glog.V(logger.Detail).Infof("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.Warn).Infof("%v", err) + } + return +} + +func (self *Api) Modify(uri, contentHash, contentType string, nameresolver bool) (newRootHash string, err error) { + root, _, path, err := self.parseAndResolve(uri, nameresolver) + quitC := make(chan bool) + trie, err := loadManifest(self.dpa, root, quitC) + if err != nil { + return + } + + if contentHash != "" { + entry := &manifestTrieEntry{ + Path: path, + Hash: contentHash, + ContentType: contentType, + } + trie.addEntry(entry, quitC) + } else { + trie.deleteEntry(path, quitC) + } + + err = trie.recalcAndStore() + if err != nil { + return + } + return trie.hash.String(), nil +} diff --git a/swarm/api/api_test.go b/swarm/api/api_test.go new file mode 100644 index 000000000..b09811959 --- /dev/null +++ b/swarm/api/api_test.go @@ -0,0 +1,117 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library 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. +// +// The go-ethereum library 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 the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +package api + +import ( + "io" + "io/ioutil" + "os" + "testing" + + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/swarm/storage" +) + +func testApi(t *testing.T, f func(*Api)) { + datadir, err := ioutil.TempDir("", "bzz-test") + if err != nil { + 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 + } + api := NewApi(dpa, nil) + dpa.Start() + f(api) + dpa.Stop() +} + +type testResponse struct { + reader storage.LazySectionReader + *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) + } + 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 '%d...', got '%d...'", 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)) + } +} + +// func expResponse(content []byte, mimeType string, status int) *Response { +func expResponse(content string, mimeType string, status int) *Response { + glog.V(logger.Detail).Infof("expected content (%v): %v ", len(content), content) + 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, true) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + quitC := make(chan bool) + size, err := reader.Size(quitC) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + glog.V(logger.Detail).Infof("reader size: %v ", size) + s := make([]byte, size) + _, err = reader.Read(s) + if err != io.EOF { + t.Fatalf("unexpected error: %v", err) + } + reader.Seek(0, 0) + return &testResponse{reader, &Response{mimeType, status, size, string(s)}} + // return &testResponse{reader, &Response{mimeType, status, reader.Size(), nil}} +} + +func TestApiPut(t *testing.T) { + 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.go b/swarm/api/config.go new file mode 100644 index 000000000..730755c43 --- /dev/null +++ b/swarm/api/config.go @@ -0,0 +1,132 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library 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. +// +// The go-ethereum library 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 the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +package api + +import ( + "crypto/ecdsa" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/swarm/network" + "github.com/ethereum/go-ethereum/swarm/services/swap" + "github.com/ethereum/go-ethereum/swarm/storage" +) + +const ( + port = "8500" +) + +// by default ens root is north internal +var ( + toyNetEnsRoot = common.HexToAddress("0xd344889e0be3e9ef6c26b0f60ef66a32e83c1b69") +) + +// separate bzz directories +// allow several bzz nodes running in parallel +type Config struct { + // serialised/persisted fields + *storage.StoreParams + *storage.ChunkerParams + *network.HiveParams + Swap *swap.SwapParams + *network.SyncParams + Path string + Port string + PublicKey string + BzzKey string + EnsRoot common.Address +} + +// config is agnostic to where private key is coming from +// so managing accounts is outside swarm and left to wrappers +func NewConfig(path string, contract common.Address, prvKey *ecdsa.PrivateKey) (self *Config, err error) { + + address := crypto.PubkeyToAddress(prvKey.PublicKey) // default beneficiary address + dirpath := filepath.Join(path, common.Bytes2Hex(address.Bytes())) + err = os.MkdirAll(dirpath, os.ModePerm) + if err != nil { + return + } + confpath := filepath.Join(dirpath, "config.json") + var data []byte + pubkey := crypto.FromECDSAPub(&prvKey.PublicKey) + pubkeyhex := common.ToHex(pubkey) + keyhex := crypto.Sha3Hash(pubkey).Hex() + + self = &Config{ + SyncParams: network.NewSyncParams(dirpath), + HiveParams: network.NewHiveParams(dirpath), + ChunkerParams: storage.NewChunkerParams(), + StoreParams: storage.NewStoreParams(dirpath), + Port: port, + Path: dirpath, + Swap: swap.DefaultSwapParams(contract, prvKey), + PublicKey: pubkeyhex, + BzzKey: keyhex, + EnsRoot: toyNetEnsRoot, + } + data, err = ioutil.ReadFile(confpath) + if err != nil { + if !os.IsNotExist(err) { + return + } + // file does not exist + // write out config file + err = self.Save() + if err != nil { + err = fmt.Errorf("error writing config: %v", err) + } + return + } + // file exists, deserialise + err = json.Unmarshal(data, self) + if err != nil { + return nil, fmt.Errorf("unable to parse config: %v", err) + } + // check public key + if pubkeyhex != self.PublicKey { + return nil, fmt.Errorf("public key does not match the one in the config file %v != %v", pubkeyhex, self.PublicKey) + } + if keyhex != self.BzzKey { + return nil, fmt.Errorf("bzz key does not match the one in the config file %v != %v", keyhex, self.BzzKey) + } + self.Swap.SetKey(prvKey) + + if (self.EnsRoot == common.Address{}) { + self.EnsRoot = toyNetEnsRoot + } + + return +} + +func (self *Config) Save() error { + data, err := json.MarshalIndent(self, "", " ") + if err != nil { + return err + } + err = os.MkdirAll(self.Path, os.ModePerm) + if err != nil { + return err + } + confpath := filepath.Join(self.Path, "config.json") + return ioutil.WriteFile(confpath, data, os.ModePerm) +} diff --git a/swarm/api/config_test.go b/swarm/api/config_test.go new file mode 100644 index 000000000..874701119 --- /dev/null +++ b/swarm/api/config_test.go @@ -0,0 +1,124 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library 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. +// +// The go-ethereum library 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 the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +package api + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +var ( + hexprvkey = "65138b2aa745041b372153550584587da326ab440576b2a1191dd95cee30039c" + defaultConfig = `{ + "ChunkDbPath": "` + filepath.Join("TMPDIR", "0d2f62485607cf38d9d795d93682a517661e513e", "chunks") + `", + "DbCapacity": 5000000, + "CacheCapacity": 5000, + "Radius": 0, + "Branches": 128, + "Hash": "SHA3", + "CallInterval": 3000000000, + "KadDbPath": "` + filepath.Join("TMPDIR", "0d2f62485607cf38d9d795d93682a517661e513e", "bzz-peers.json") + `", + "MaxProx": 8, + "ProxBinSize": 2, + "BucketSize": 4, + "PurgeInterval": 151200000000000, + "InitialRetryInterval": 42000000, + "MaxIdleInterval": 42000000000, + "ConnRetryExp": 2, + "Swap": { + "BuyAt": 20000000000, + "SellAt": 20000000000, + "PayAt": 100, + "DropAt": 10000, + "AutoCashInterval": 300000000000, + "AutoCashThreshold": 50000000000000, + "AutoDepositInterval": 300000000000, + "AutoDepositThreshold": 50000000000000, + "AutoDepositBuffer": 100000000000000, + "PublicKey": "0x045f5cfd26692e48d0017d380349bcf50982488bc11b5145f3ddf88b24924299048450542d43527fbe29a5cb32f38d62755393ac002e6bfdd71b8d7ba725ecd7a3", + "Contract": "0x0000000000000000000000000000000000000000", + "Beneficiary": "0x0d2f62485607cf38d9d795d93682a517661e513e" + }, + "RequestDbPath": "` + filepath.Join("TMPDIR", "0d2f62485607cf38d9d795d93682a517661e513e", "requests") + `", + "RequestDbBatchSize": 512, + "KeyBufferSize": 1024, + "SyncBatchSize": 128, + "SyncBufferSize": 128, + "SyncCacheSize": 1024, + "SyncPriorities": [ + 2, + 1, + 1, + 0, + 0 + ], + "SyncModes": [ + true, + true, + true, + true, + false + ], + "Path": "` + filepath.Join("TMPDIR", "0d2f62485607cf38d9d795d93682a517661e513e") + `", + "Port": "8500", + "PublicKey": "0x045f5cfd26692e48d0017d380349bcf50982488bc11b5145f3ddf88b24924299048450542d43527fbe29a5cb32f38d62755393ac002e6bfdd71b8d7ba725ecd7a3", + "BzzKey": "0xe861964402c0b78e2d44098329b8545726f215afa737d803714a4338552fcb81", + "EnsRoot": "0xd344889e0be3e9ef6c26b0f60ef66a32e83c1b69" +}` +) + +func TestConfigWriteRead(t *testing.T) { + tmp, err := ioutil.TempDir(os.TempDir(), "bzz-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + + prvkey := crypto.ToECDSA(common.Hex2Bytes(hexprvkey)) + orig, err := NewConfig(tmp, common.Address{}, prvkey) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + account := crypto.PubkeyToAddress(prvkey.PublicKey) + dirpath := filepath.Join(tmp, common.Bytes2Hex(account.Bytes())) + confpath := filepath.Join(dirpath, "config.json") + data, err := ioutil.ReadFile(confpath) + if err != nil { + 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: %v\ngot: %v", exp, string(data)) + } + + conf, err := NewConfig(tmp, common.Address{}, prvkey) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if conf.Swap.Beneficiary.Hex() != orig.Swap.Beneficiary.Hex() { + t.Fatalf("expected beneficiary from loaded config %v to match original %v", conf.Swap.Beneficiary.Hex(), orig.Swap.Beneficiary.Hex()) + } + +} diff --git a/swarm/api/filesystem.go b/swarm/api/filesystem.go new file mode 100644 index 000000000..428f3e3ac --- /dev/null +++ b/swarm/api/filesystem.go @@ -0,0 +1,283 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library 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. +// +// The go-ethereum library 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 the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +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("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: filepath.ToSlash(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: filepath.ToSlash(localpath), + } + list = append(list, entry) + } + + cnt := len(list) + errors := make([]error, cnt) + done := make(chan bool, maxParallelFiles) + dcnt := 0 + awg := &sync.WaitGroup{} + + for i, entry := range list { + if i >= dcnt+maxParallelFiles { + <-done + dcnt++ + } + awg.Add(1) + go func(i int, entry *manifestTrieEntry, done chan bool) { + f, err := os.Open(entry.Path) + if err == nil { + stat, _ := f.Stat() + var hash storage.Key + wg := &sync.WaitGroup{} + hash, err = self.api.dpa.Store(f, stat.Size(), wg, nil) + if hash != nil { + list[i].Hash = hash.String() + } + wg.Wait() + awg.Done() + if err == nil { + first512 := make([]byte, 512) + fread, _ := f.ReadAt(first512, 0) + if fread > 0 { + mimeType := http.DetectContentType(first512[:fread]) + if filepath.Ext(entry.Path) == ".css" { + mimeType = "text/css" + } + list[i].ContentType = mimeType + } + } + f.Close() + } + errors[i] = err + done <- true + }(i, entry, done) + } + for dcnt < cnt { + <-done + dcnt++ + } + + trie := &manifestTrie{ + dpa: self.api.dpa, + } + quitC := make(chan bool) + 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, quitC) + } + trie.addEntry(entry, quitC) + } + + err2 := trie.recalcAndStore() + var hs string + if err2 == nil { + hs = trie.hash.String() + } + awg.Wait() + 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, true) + if err != nil { + return err + } + + if len(path) > 0 { + path += "/" + } + + quitC := make(chan bool) + trie, err := loadManifest(self.api.dpa, key, quitC) + if err != nil { + glog.V(logger.Warn).Infof("fs.Download: loadManifestTrie error: %v", err) + return err + } + + type downloadListEntry struct { + key storage.Key + path string + } + + var list []*downloadListEntry + var mde error + + prevPath := lpath + err = trie.listWithPrefix(path, quitC, func(entry *manifestTrieEntry, suffix string) { + glog.V(logger.Detail).Infof("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) + prevPath = dir + } + if (mde == nil) && (path != dir+"/") { + list = append(list, &downloadListEntry{key: key, path: path}) + } + }) + if err != nil { + return err + } + + wg := sync.WaitGroup{} + errC := make(chan error) + done := make(chan bool, maxParallelFiles) + for i, entry := range list { + select { + case done <- true: + wg.Add(1) + case <-quitC: + return fmt.Errorf("aborted") + } + go func(i int, entry *downloadListEntry) { + defer wg.Done() + f, err := os.Create(entry.path) // TODO: path separators + if err == nil { + + reader := self.api.dpa.Retrieve(entry.key) + writer := bufio.NewWriter(f) + size, err := reader.Size(quitC) + if err == nil { + _, err = io.CopyN(writer, reader, size) // TODO: handle errors + err2 := writer.Flush() + if err == nil { + err = err2 + } + err2 = f.Close() + if err == nil { + err = err2 + } + } + } + if err != nil { + select { + case errC <- err: + case <-quitC: + } + return + } + <-done + }(i, entry) + } + go func() { + wg.Wait() + close(errC) + }() + select { + case err = <-errC: + return err + case <-quitC: + return fmt.Errorf("aborted") + } + +} diff --git a/swarm/api/filesystem_test.go b/swarm/api/filesystem_test.go new file mode 100644 index 000000000..f6657aede --- /dev/null +++ b/swarm/api/filesystem_test.go @@ -0,0 +1,187 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library 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. +// +// The go-ethereum library 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 the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +package api + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "sync" + "testing" +) + +var 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 { + file := filepath.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) { + testFileSystem(t, func(fs *FileSystem) { + api := fs.api + bzzhash, err := fs.Upload(filepath.Join("testdata", "test0"), "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + content := readPath(t, "testdata", "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, "testdata", "test0", "index.css") + resp = testGet(t, api, bzzhash+"/index.css") + exp = expResponse(content, "text/css", 0) + checkResponse(t, resp, exp) + + _, _, _, err = api.Get(bzzhash, true) + if err == nil { + t.Fatalf("expected error: %v", err) + } + + downloadDir := filepath.Join(testDownloadDir, "test0") + 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) { + testFileSystem(t, func(fs *FileSystem) { + api := fs.api + bzzhash, err := fs.Upload(filepath.Join("testdata", "test0"), "") + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + bzzhash, err = api.Modify(bzzhash+"/index.html", "", "", true) + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + index, err := ioutil.ReadFile(filepath.Join("testdata", "test0", "index.html")) + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + wg := &sync.WaitGroup{} + hash, err := api.Store(bytes.NewReader(index), int64(len(index)), wg) + wg.Wait() + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + bzzhash, err = api.Modify(bzzhash+"/index2.html", hash.Hex(), "text/html; charset=utf-8", true) + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + bzzhash, err = api.Modify(bzzhash+"/img/logo.png", hash.Hex(), "text/html; charset=utf-8", true) + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + content := readPath(t, "testdata", "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, "testdata", "test0", "index.css") + resp = testGet(t, api, bzzhash+"/index.css") + exp = expResponse(content, "text/css", 0) + + _, _, _, err = api.Get(bzzhash, true) + 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(filepath.Join("testdata", "test0"), "index.html") + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + content := readPath(t, "testdata", "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(filepath.Join("testdata", "test0", "index.html"), "") + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + content := readPath(t, "testdata", "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(filepath.Join("testdata", "test0", "index.html"), "index.html") + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + content := readPath(t, "testdata", "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 000000000..a3a644b73 --- /dev/null +++ b/swarm/api/http/roundtripper.go @@ -0,0 +1,69 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library 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. +// +// The go-ethereum library 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 the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +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}) +client.RegisterScheme("bzzi", &http.RoundTripper{Port: port}) +client.RegisterScheme("bzzr", &http.RoundTripper{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/%s", host, self.Port, req.Proto, req.URL.Host, req.URL.Path) + glog.V(logger.Info).Infof("roundtripper: proxying request '%s' to '%s'", req.RequestURI, url) + reqProxy, err := http.NewRequest(req.Method, url, req.Body) + if err != nil { + return nil, err + } + return http.DefaultClient.Do(reqProxy) +} diff --git a/swarm/api/http/roundtripper_test.go b/swarm/api/http/roundtripper_test.go new file mode 100644 index 000000000..9afad20ae --- /dev/null +++ b/swarm/api/http/roundtripper_test.go @@ -0,0 +1,68 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library 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. +// +// The go-ethereum library 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 the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +package http + +import ( + "io/ioutil" + "net/http" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common/httpclient" +) + +const port = "3222" + +func TestRoundTripper(t *testing.T) { + serveMux := http.NewServeMux() + serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + w.Header().Set("Content-Type", "text/plain") + http.ServeContent(w, r, "", time.Unix(0, 0), strings.NewReader(r.RequestURI)) + } else { + http.Error(w, "Method "+r.Method+" is not supported.", http.StatusMethodNotAllowed) + } + }) + go http.ListenAndServe(":"+port, serveMux) + + rt := &RoundTripper{Port: port} + client := httpclient.New("/") + client.RegisterProtocol("bzz", rt) + + resp, err := client.Client().Get("bzz://test.com/path") + if err != nil { + t.Errorf("expected no error, got %v", err) + return + } + + defer func() { + if resp != nil { + resp.Body.Close() + } + }() + + content, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("expected no error, got %v", err) + return + } + if string(content) != "/HTTP/1.1:/test.com/path" { + t.Errorf("incorrect response from http server: expected '%v', got '%v'", "/HTTP/1.1:/test.com/path", string(content)) + } + +} diff --git a/swarm/api/http/server.go b/swarm/api/http/server.go new file mode 100644 index 000000000..a35672687 --- /dev/null +++ b/swarm/api/http/server.go @@ -0,0 +1,286 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library 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. +// +// The go-ethereum library 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 the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +/* +A simple http server interface to Swarm +*/ +package http + +import ( + "bytes" + "io" + "net/http" + "regexp" + "sync" + "time" + + "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 ( + rawType = "application/octet-stream" +) + +var ( + // accepted protocols: bzz (traditional), bzzi (immutable) and bzzr (raw) + bzzPrefix = regexp.MustCompile("^/+bzz[ir]?:/+") + trailingSlashes = regexp.MustCompile("/+$") + rootDocumentUri = regexp.MustCompile("^/+bzz[i]?:/+[^/]+$") + // forever = func() time.Time { return time.Unix(0, 0) } + forever = time.Now +) + +type sequentialReader struct { + reader io.Reader + pos int64 + ahead map[int64](chan bool) + lock sync.Mutex +} + +// browser API for registering bzz url scheme handlers: +// https://developer.mozilla.org/en/docs/Web-based_protocol_handlers +// electron (chromium) api for registering bzz url scheme handlers: +// https://github.com/atom/electron/blob/master/docs/api/protocol.md + +// starts up http server +func StartHttpServer(api *api.Api, port string) { + serveMux := http.NewServeMux() + serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + handler(w, r, api) + }) + go http.ListenAndServe(":"+port, serveMux) + glog.V(logger.Info).Infof("Swarm HTTP proxy started on localhost:%s", port) +} + +func handler(w http.ResponseWriter, r *http.Request, a *api.Api) { + requestURL := r.URL + // This is wrong + // if requestURL.Host == "" { + // var err error + // requestURL, err = url.Parse(r.Referer() + requestURL.String()) + // if err != nil { + // http.Error(w, err.Error(), http.StatusBadRequest) + // return + // } + // } + glog.V(logger.Debug).Infof("HTTP %s request URL: '%s', Host: '%s', Path: '%s', Referer: '%s', Accept: '%s'", r.Method, r.RequestURI, requestURL.Host, requestURL.Path, r.Referer(), r.Header.Get("Accept")) + uri := requestURL.Path + var raw, nameresolver bool + var proto string + + // HTTP-based URL protocol handler + glog.V(logger.Debug).Infof("BZZ request URI: '%s'", uri) + + path := bzzPrefix.ReplaceAllStringFunc(uri, func(p string) string { + proto = p + return "" + }) + + // protocol identification (ugly) + if proto == "" { + if glog.V(logger.Error) { + glog.Errorf( + "[BZZ] Swarm: Protocol error in request `%s`.", + uri, + ) + http.Error(w, "BZZ protocol error", http.StatusBadRequest) + return + } + } + if len(proto) > 4 { + raw = proto[1:5] == "bzzr" + nameresolver = proto[1:5] != "bzzi" + } + + glog.V(logger.Debug).Infof( + "[BZZ] Swarm: %s request over protocol %s '%s' received.", + r.Method, proto, path, + ) + + switch { + case r.Method == "POST" || r.Method == "PUT": + key, err := a.Store(r.Body, r.ContentLength, nil) + if err == nil { + glog.V(logger.Debug).Infof("Content for %v stored", key.Log()) + } else { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if r.Method == "POST" { + if raw { + w.Header().Set("Content-Type", "text/plain") + http.ServeContent(w, r, "", time.Now(), bytes.NewReader([]byte(common.Bytes2Hex(key)))) + } else { + http.Error(w, "No POST to "+uri+" allowed.", http.StatusBadRequest) + return + } + } else { + // PUT + if raw { + http.Error(w, "No PUT to /raw allowed.", http.StatusBadRequest) + return + } else { + path = api.RegularSlashes(path) + mime := r.Header.Get("Content-Type") + // TODO proper root hash separation + glog.V(logger.Debug).Infof("Modify '%s' to store %v as '%s'.", path, key.Log(), mime) + newKey, err := a.Modify(path, common.Bytes2Hex(key), mime, nameresolver) + if err == nil { + glog.V(logger.Debug).Infof("Swarm replaced manifest by '%s'", newKey) + w.Header().Set("Content-Type", "text/plain") + http.ServeContent(w, r, "", time.Now(), bytes.NewReader([]byte(newKey))) + } else { + http.Error(w, "PUT to "+path+"failed.", http.StatusBadRequest) + return + } + } + } + case r.Method == "DELETE": + if raw { + http.Error(w, "No DELETE to /raw allowed.", http.StatusBadRequest) + return + } else { + path = api.RegularSlashes(path) + glog.V(logger.Debug).Infof("Delete '%s'.", path) + newKey, err := a.Modify(path, "", "", nameresolver) + if err == nil { + glog.V(logger.Debug).Infof("Swarm replaced manifest by '%s'", newKey) + w.Header().Set("Content-Type", "text/plain") + http.ServeContent(w, r, "", time.Now(), bytes.NewReader([]byte(newKey))) + } else { + http.Error(w, "DELETE to "+path+"failed.", http.StatusBadRequest) + return + } + } + case r.Method == "GET" || r.Method == "HEAD": + path = trailingSlashes.ReplaceAllString(path, "") + if raw { + // resolving host + key, err := a.Resolve(path, nameresolver) + if err != nil { + glog.V(logger.Error).Infof("%v", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // retrieving content + reader := a.Retrieve(key) + quitC := make(chan bool) + size, err := reader.Size(quitC) + glog.V(logger.Debug).Infof("Reading %d bytes.", size) + + // setting mime type + qv := requestURL.Query() + mimeType := qv.Get("content_type") + if mimeType == "" { + mimeType = rawType + } + + w.Header().Set("Content-Type", mimeType) + http.ServeContent(w, r, uri, forever(), reader) + glog.V(logger.Debug).Infof("Serve raw content '%s' (%d bytes) as '%s'", uri, size, mimeType) + + // retrieve path via manifest + } else { + glog.V(logger.Debug).Infof("Structured GET request '%s' received.", uri) + // add trailing slash, if missing + if rootDocumentUri.MatchString(uri) { + http.Redirect(w, r, path+"/", http.StatusFound) + return + } + reader, mimeType, status, err := a.Get(path, nameresolver) + if err != nil { + if _, ok := err.(api.ErrResolve); ok { + glog.V(logger.Debug).Infof("%v", err) + status = http.StatusBadRequest + } else { + glog.V(logger.Debug).Infof("error retrieving '%s': %v", uri, err) + status = http.StatusNotFound + } + http.Error(w, err.Error(), status) + return + } + // set mime type and status headers + w.Header().Set("Content-Type", mimeType) + if status > 0 { + w.WriteHeader(status) + } else { + status = 200 + } + quitC := make(chan bool) + size, err := reader.Size(quitC) + glog.V(logger.Debug).Infof("Served '%s' (%d bytes) as '%s' (status code: %v)", uri, size, mimeType, status) + + http.ServeContent(w, r, path, forever(), reader) + + } + default: + http.Error(w, "Method "+r.Method+" is not supported.", http.StatusMethodNotAllowed) + } +} + +func (self *sequentialReader) ReadAt(target []byte, off int64) (n int, err error) { + self.lock.Lock() + // assert self.pos <= off + if self.pos > off { + glog.V(logger.Error).Infof("non-sequential read attempted from sequentialReader; %d > %d", + self.pos, off) + panic("Non-sequential read attempt") + } + if self.pos != off { + glog.V(logger.Debug).Infof("deferred read in POST at position %d, offset %d.", + self.pos, off) + wait := make(chan bool) + self.ahead[off] = wait + self.lock.Unlock() + if <-wait { + // failed read behind + n = 0 + err = io.ErrUnexpectedEOF + return + } + self.lock.Lock() + } + localPos := 0 + for localPos < len(target) { + n, err = self.reader.Read(target[localPos:]) + localPos += n + glog.V(logger.Debug).Infof("Read %d bytes into buffer size %d from POST, error %v.", + n, len(target), err) + if err != nil { + glog.V(logger.Debug).Infof("POST stream's reading terminated with %v.", err) + for i := range self.ahead { + self.ahead[i] <- true + delete(self.ahead, i) + } + self.lock.Unlock() + return localPos, err + } + self.pos += int64(n) + } + wait := self.ahead[self.pos] + if wait != nil { + glog.V(logger.Debug).Infof("deferred read in POST at position %d triggered.", + self.pos) + delete(self.ahead, self.pos) + close(wait) + } + self.lock.Unlock() + return localPos, err +} diff --git a/swarm/api/manifest.go b/swarm/api/manifest.go new file mode 100644 index 000000000..a289c01f9 --- /dev/null +++ b/swarm/api/manifest.go @@ -0,0 +1,336 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library 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. +// +// The go-ethereum library 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 the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +package api + +import ( + "bytes" + "encoding/json" + "fmt" + "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 ( + manifestType = "application/bzz-manifest+json" +) + +type manifestTrie struct { + dpa *storage.DPA + entries [257]*manifestTrieEntry // indexed by first character of path, entries[256] is the empty path entry + hash storage.Key // if hash != nil, it is stored +} + +type manifestJSON struct { + Entries []*manifestTrieEntry `json:"entries"` +} + +type manifestTrieEntry struct { + Path string `json:"path"` + Hash string `json:"hash"` // for manifest content type, empty until subtrie is evaluated + ContentType string `json:"contentType"` + Status int `json:"status"` + subtrie *manifestTrie +} + +func loadManifest(dpa *storage.DPA, hash storage.Key, quitC chan bool) (trie *manifestTrie, err error) { // non-recursive, subtrees are downloaded on-demand + + glog.V(logger.Detail).Infof("manifest lookup key: '%v'.", hash.Log()) + // retrieve manifest via DPA + manifestReader := dpa.Retrieve(hash) + return readManifest(manifestReader, hash, dpa, quitC) +} + +func readManifest(manifestReader storage.LazySectionReader, hash storage.Key, dpa *storage.DPA, quitC chan bool) (trie *manifestTrie, err error) { // non-recursive, subtrees are downloaded on-demand + + // TODO check size for oversized manifests + size, err := manifestReader.Size(quitC) + manifestData := make([]byte, size) + read, err := manifestReader.Read(manifestData) + if int64(read) < size { + glog.V(logger.Detail).Infof("Manifest %v not found.", hash.Log()) + if err == nil { + err = fmt.Errorf("Manifest retrieval cut short: read %v, expect %v", read, size) + } + return + } + + glog.V(logger.Detail).Infof("Manifest %v retrieved", hash.Log()) + man := manifestJSON{} + err = json.Unmarshal(manifestData, &man) + if err != nil { + err = fmt.Errorf("Manifest %v is malformed: %v", hash.Log(), err) + glog.V(logger.Detail).Infof("%v", err) + return + } + + glog.V(logger.Detail).Infof("Manifest %v has %d entries.", hash.Log(), len(man.Entries)) + + trie = &manifestTrie{ + dpa: dpa, + } + for _, entry := range man.Entries { + trie.addEntry(entry, quitC) + } + return +} + +func (self *manifestTrie) addEntry(entry *manifestTrieEntry, quitC chan bool) { + self.hash = nil // trie modified, hash needs to be re-calculated on demand + + if len(entry.Path) == 0 { + self.entries[256] = entry + return + } + + b := byte(entry.Path[0]) + if (self.entries[b] == nil) || (self.entries[b].Path == entry.Path) { + self.entries[b] = entry + return + } + + oldentry := self.entries[b] + cpl := 0 + for (len(entry.Path) > cpl) && (len(oldentry.Path) > cpl) && (entry.Path[cpl] == oldentry.Path[cpl]) { + cpl++ + } + + if (oldentry.ContentType == manifestType) && (cpl == len(oldentry.Path)) { + if self.loadSubTrie(oldentry, quitC) != nil { + return + } + entry.Path = entry.Path[cpl:] + oldentry.subtrie.addEntry(entry, quitC) + oldentry.Hash = "" + return + } + + commonPrefix := entry.Path[:cpl] + + subtrie := &manifestTrie{ + dpa: self.dpa, + } + entry.Path = entry.Path[cpl:] + oldentry.Path = oldentry.Path[cpl:] + subtrie.addEntry(entry, quitC) + subtrie.addEntry(oldentry, quitC) + + self.entries[b] = &manifestTrieEntry{ + Path: commonPrefix, + Hash: "", + ContentType: manifestType, + subtrie: subtrie, + } +} + +func (self *manifestTrie) getCountLast() (cnt int, entry *manifestTrieEntry) { + for _, e := range self.entries { + if e != nil { + cnt++ + entry = e + } + } + return +} + +func (self *manifestTrie) deleteEntry(path string, quitC chan bool) { + self.hash = nil // trie modified, hash needs to be re-calculated on demand + + if len(path) == 0 { + self.entries[256] = nil + return + } + + b := byte(path[0]) + entry := self.entries[b] + if entry == nil { + return + } + if entry.Path == path { + self.entries[b] = nil + return + } + + epl := len(entry.Path) + if (entry.ContentType == manifestType) && (len(path) >= epl) && (path[:epl] == entry.Path) { + if self.loadSubTrie(entry, quitC) != nil { + return + } + entry.subtrie.deleteEntry(path[epl:], quitC) + entry.Hash = "" + // remove subtree if it has less than 2 elements + cnt, lastentry := entry.subtrie.getCountLast() + if cnt < 2 { + if lastentry != nil { + lastentry.Path = entry.Path + lastentry.Path + } + self.entries[b] = lastentry + } + } +} + +func (self *manifestTrie) recalcAndStore() error { + if self.hash != nil { + return nil + } + + var buffer bytes.Buffer + buffer.WriteString(`{"entries":[`) + + list := &manifestJSON{} + for _, entry := range self.entries { + if entry != nil { + if entry.Hash == "" { // TODO: paralellize + err := entry.subtrie.recalcAndStore() + if err != nil { + return err + } + entry.Hash = entry.subtrie.hash.String() + } + list.Entries = append(list.Entries, entry) + } + } + + manifest, err := json.Marshal(list) + if err != nil { + return err + } + + sr := bytes.NewReader(manifest) + wg := &sync.WaitGroup{} + key, err2 := self.dpa.Store(sr, int64(len(manifest)), wg, nil) + wg.Wait() + self.hash = key + return err2 +} + +func (self *manifestTrie) loadSubTrie(entry *manifestTrieEntry, quitC chan bool) (err error) { + if entry.subtrie == nil { + hash := common.Hex2Bytes(entry.Hash) + entry.subtrie, err = loadManifest(self.dpa, hash, quitC) + entry.Hash = "" // might not match, should be recalculated + } + return +} + +func (self *manifestTrie) listWithPrefixInt(prefix, rp string, quitC chan bool, cb func(entry *manifestTrieEntry, suffix string)) error { + plen := len(prefix) + var start, stop int + if plen == 0 { + start = 0 + stop = 256 + } else { + start = int(prefix[0]) + stop = start + } + + for i := start; i <= stop; i++ { + select { + case <-quitC: + return fmt.Errorf("aborted") + default: + } + entry := self.entries[i] + if entry != nil { + epl := len(entry.Path) + if entry.ContentType == manifestType { + l := plen + if epl < l { + l = epl + } + if prefix[:l] == entry.Path[:l] { + err := self.loadSubTrie(entry, quitC) + if err != nil { + return err + } + err = entry.subtrie.listWithPrefixInt(prefix[l:], rp+entry.Path[l:], quitC, cb) + if err != nil { + return err + } + } + } else { + if (epl >= plen) && (prefix == entry.Path[:plen]) { + cb(entry, rp+entry.Path[plen:]) + } + } + } + } + return nil +} + +func (self *manifestTrie) listWithPrefix(prefix string, quitC chan bool, cb func(entry *manifestTrieEntry, suffix string)) (err error) { + return self.listWithPrefixInt(prefix, "", quitC, cb) +} + +func (self *manifestTrie) findPrefixOf(path string, quitC chan bool) (entry *manifestTrieEntry, pos int) { + + glog.V(logger.Detail).Infof("findPrefixOf(%s)", path) + + if len(path) == 0 { + return self.entries[256], 0 + } + + b := byte(path[0]) + entry = self.entries[b] + if entry == nil { + return self.entries[256], 0 + } + epl := len(entry.Path) + glog.V(logger.Detail).Infof("path = %v entry.Path = %v epl = %v", path, entry.Path, epl) + if (len(path) >= epl) && (path[:epl] == entry.Path) { + glog.V(logger.Detail).Infof("entry.ContentType = %v", entry.ContentType) + if entry.ContentType == manifestType { + if self.loadSubTrie(entry, quitC) != nil { + return nil, 0 + } + entry, pos = entry.subtrie.findPrefixOf(path[epl:], quitC) + if entry != nil { + pos += epl + } + } else { + pos = epl + } + } else { + entry = nil + } + return +} + +// file system manifest always contains regularized paths +// no leading or trailing slashes, only single slashes inside +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] + } + } + if (len(res) > 0) && (res[len(res)-1] == '/') { + res = res[:len(res)-1] + } + return +} + +func (self *manifestTrie) getEntry(spath string) (entry *manifestTrieEntry, fullpath string) { + path := RegularSlashes(spath) + var pos int + quitC := make(chan bool) + entry, pos = self.findPrefixOf(path, quitC) + return entry, path[:pos] +} diff --git a/swarm/api/manifest_test.go b/swarm/api/manifest_test.go new file mode 100644 index 000000000..20b8117c6 --- /dev/null +++ b/swarm/api/manifest_test.go @@ -0,0 +1,80 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library 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. +// +// The go-ethereum library 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 the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +package api + +import ( + // "encoding/json" + "fmt" + "io" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/swarm/storage" +) + +func manifest(paths ...string) (manifestReader storage.LazySectionReader) { + var entries []string + for _, path := range paths { + entry := fmt.Sprintf(`{"path":"%s"}`, path) + entries = append(entries, entry) + } + manifest := fmt.Sprintf(`{"entries":[%s]}`, strings.Join(entries, ",")) + return &storage.LazyTestSectionReader{ + SectionReader: io.NewSectionReader(strings.NewReader(manifest), 0, int64(len(manifest))), + } +} + +func testGetEntry(t *testing.T, path, match string, paths ...string) *manifestTrie { + quitC := make(chan bool) + trie, err := readManifest(manifest(paths...), nil, nil, quitC) + if err != nil { + t.Errorf("unexpected error making manifest: %v", err) + } + checkEntry(t, path, match, trie) + return trie +} + +func checkEntry(t *testing.T, path, match string, trie *manifestTrie) { + entry, fullpath := trie.getEntry(path) + if match == "-" && entry != nil { + t.Errorf("expected no match for '%s', got '%s'", path, fullpath) + } else if entry == nil { + if match != "-" { + t.Errorf("expected entry '%s' to match '%s', got no match", match, path) + } + } else if fullpath != match { + t.Errorf("incorrect entry retrieved for '%s'. expected path '%v', got '%s'", path, match, fullpath) + } +} + +func TestGetEntry(t *testing.T) { + // file system manifest always contains regularized paths + testGetEntry(t, "a", "a", "a") + testGetEntry(t, "b", "-", "a") + testGetEntry(t, "/a//", "a", "a") + // fallback + testGetEntry(t, "/a", "", "") + testGetEntry(t, "/a/b", "a/b", "a/b") + // longest/deepest math + testGetEntry(t, "a/b", "-", "a", "a/ba", "a/b/c") + testGetEntry(t, "a/b", "a/b", "a", "a/b", "a/bb", "a/b/c") + testGetEntry(t, "//a//b//", "a/b", "a", "a/b", "a/bb", "a/b/c") +} + +func TestDeleteEntry(t *testing.T) { + +} diff --git a/swarm/api/storage.go b/swarm/api/storage.go new file mode 100644 index 000000000..31b484675 --- /dev/null +++ b/swarm/api/storage.go @@ -0,0 +1,70 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library 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. +// +// The go-ethereum library 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 the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +package api + +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, true) + if err != nil { + return nil, err + } + quitC := make(chan bool) + expsize, err := reader.Size(quitC) + if err != nil { + return nil, err + } + body := make([]byte, expsize) + size, err := reader.Read(body) + if int64(size) == expsize { + err = nil + } + return &Response{mimeType, status, expsize, string(body[:size])}, err +} + +// Modify(rootHash, path, contentHash, contentType) takes th e manifest trie rooted in rootHash, +// and merge on to it. creating an entry w conentType (mime) +func (self *Storage) Modify(rootHash, path, contentHash, contentType string) (newRootHash string, err error) { + return self.api.Modify(rootHash+"/"+path, contentHash, contentType, true) +} diff --git a/swarm/api/storage_test.go b/swarm/api/storage_test.go new file mode 100644 index 000000000..72caf52df --- /dev/null +++ b/swarm/api/storage_test.go @@ -0,0 +1,49 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library 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. +// +// The go-ethereum library 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 the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +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/api/testapi.go b/swarm/api/testapi.go new file mode 100644 index 000000000..6631196c1 --- /dev/null +++ b/swarm/api/testapi.go @@ -0,0 +1,46 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library 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. +// +// The go-ethereum library 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 the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +package api + +import ( + "github.com/ethereum/go-ethereum/swarm/network" +) + +type Control struct { + api *Api + hive *network.Hive +} + +func NewControl(api *Api, hive *network.Hive) *Control { + return &Control{api, hive} +} + +func (self *Control) BlockNetworkRead(on bool) { + self.hive.BlockNetworkRead(on) +} + +func (self *Control) SyncEnabled(on bool) { + self.hive.SyncEnabled(on) +} + +func (self *Control) SwapEnabled(on bool) { + self.hive.SwapEnabled(on) +} + +func (self *Control) Hive() string { + return self.hive.String() +} diff --git a/swarm/api/testdata/test0/img/logo.png b/swarm/api/testdata/test0/img/logo.png Binary files differnew file mode 100644 index 000000000..e0fb15ab3 --- /dev/null +++ b/swarm/api/testdata/test0/img/logo.png diff --git a/swarm/api/testdata/test0/index.css b/swarm/api/testdata/test0/index.css new file mode 100644 index 000000000..67cb8d0ff --- /dev/null +++ b/swarm/api/testdata/test0/index.css @@ -0,0 +1,9 @@ +h1 { + color: black; + font-size: 12px; + background-color: orange; + border: 4px solid black; +} +body { + background-color: orange +}
\ No newline at end of file diff --git a/swarm/api/testdata/test0/index.html b/swarm/api/testdata/test0/index.html new file mode 100644 index 000000000..321e910d7 --- /dev/null +++ b/swarm/api/testdata/test0/index.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> + <head> + <link rel="stylesheet" href="index.css"> + </head> + <body> + <h1>Swarm Test</h1> + <img src="img/logo.gif" align="center", alt="Ethereum logo"> + </body> +</html>
\ No newline at end of file |