aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLewis Marshall <lewis@lmars.net>2017-04-07 06:22:22 +0800
committerFelix Lange <fjl@users.noreply.github.com>2017-04-07 06:22:22 +0800
commit71fdaa42386173da7bfa13f1728c394aeeb4eb01 (patch)
tree364a169f650982d3b2880c95e40e2c91cb27c86e
parent9aca9e6deb243b87cc75325be593a3b0c2f0a113 (diff)
downloaddexon-71fdaa42386173da7bfa13f1728c394aeeb4eb01.tar.gz
dexon-71fdaa42386173da7bfa13f1728c394aeeb4eb01.tar.zst
dexon-71fdaa42386173da7bfa13f1728c394aeeb4eb01.zip
swarm/api: refactor and improve HTTP API (#3773)
This PR deprecates the file related RPC calls in favour of an improved HTTP API. The main aim is to expose a simple to use API which can be consumed by thin clients (e.g. curl and HTML forms) without the need for complex logic (e.g. manipulating prefix trie manifests).
-rw-r--r--cmd/swarm/list.go7
-rw-r--r--cmd/swarm/manifest.go63
-rw-r--r--cmd/swarm/upload.go78
-rw-r--r--swarm/api/api.go104
-rw-r--r--swarm/api/api_test.go10
-rw-r--r--swarm/api/client/client.go551
-rw-r--r--swarm/api/client/client_test.go260
-rw-r--r--swarm/api/filesystem.go27
-rw-r--r--swarm/api/filesystem_test.go32
-rw-r--r--swarm/api/http/roundtripper_test.go11
-rw-r--r--swarm/api/http/server.go757
-rw-r--r--swarm/api/http/server_test.go4
-rw-r--r--swarm/api/http/templates.go71
-rw-r--r--swarm/api/manifest.go170
-rw-r--r--swarm/api/storage.go40
-rw-r--r--swarm/api/storage_test.go2
-rw-r--r--swarm/api/swarmfs_unix.go9
-rw-r--r--swarm/api/uri.go96
-rw-r--r--swarm/api/uri_test.go120
-rw-r--r--swarm/swarm.go30
20 files changed, 1779 insertions, 663 deletions
diff --git a/cmd/swarm/list.go b/cmd/swarm/list.go
index 3a68fef03..06d3883cf 100644
--- a/cmd/swarm/list.go
+++ b/cmd/swarm/list.go
@@ -44,7 +44,7 @@ func list(ctx *cli.Context) {
bzzapi := strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
client := swarm.NewClient(bzzapi)
- entries, err := client.ManifestFileList(manifest, prefix)
+ list, err := client.List(manifest, prefix)
if err != nil {
utils.Fatalf("Failed to generate file and directory list: %s", err)
}
@@ -52,7 +52,10 @@ func list(ctx *cli.Context) {
w := tabwriter.NewWriter(os.Stdout, 1, 2, 2, ' ', 0)
defer w.Flush()
fmt.Fprintln(w, "HASH\tCONTENT TYPE\tPATH")
- for _, entry := range entries {
+ for _, prefix := range list.CommonPrefixes {
+ fmt.Fprintf(w, "%s\t%s\t%s\n", "", "DIR", prefix)
+ }
+ for _, entry := range list.Entries {
fmt.Fprintf(w, "%s\t%s\t%s\n", entry.Hash, entry.ContentType, entry.Path)
}
}
diff --git a/cmd/swarm/manifest.go b/cmd/swarm/manifest.go
index 698b8ddb8..9729022c0 100644
--- a/cmd/swarm/manifest.go
+++ b/cmd/swarm/manifest.go
@@ -25,6 +25,7 @@ import (
"strings"
"github.com/ethereum/go-ethereum/cmd/utils"
+ "github.com/ethereum/go-ethereum/swarm/api"
swarm "github.com/ethereum/go-ethereum/swarm/api/client"
"gopkg.in/urfave/cli.v1"
)
@@ -42,7 +43,7 @@ func add(ctx *cli.Context) {
ctype string
wantManifest = ctx.GlobalBoolT(SwarmWantManifestFlag.Name)
- mroot swarm.Manifest
+ mroot api.Manifest
)
if len(args) > 3 {
@@ -76,7 +77,7 @@ func update(ctx *cli.Context) {
ctype string
wantManifest = ctx.GlobalBoolT(SwarmWantManifestFlag.Name)
- mroot swarm.Manifest
+ mroot api.Manifest
)
if len(args) > 3 {
ctype = args[3]
@@ -106,7 +107,7 @@ func remove(ctx *cli.Context) {
path = args[1]
wantManifest = ctx.GlobalBoolT(SwarmWantManifestFlag.Name)
- mroot swarm.Manifest
+ mroot api.Manifest
)
newManifest := removeEntryFromManifest(ctx, mhash, path)
@@ -125,11 +126,7 @@ func addEntryToManifest(ctx *cli.Context, mhash, path, hash, ctype string) strin
var (
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
client = swarm.NewClient(bzzapi)
- longestPathEntry = swarm.ManifestEntry{
- Path: "",
- Hash: "",
- ContentType: "",
- }
+ longestPathEntry = api.ManifestEntry{}
)
mroot, err := client.DownloadManifest(mhash)
@@ -163,7 +160,7 @@ func addEntryToManifest(ctx *cli.Context, mhash, path, hash, ctype string) strin
newHash := addEntryToManifest(ctx, longestPathEntry.Hash, newPath, hash, ctype)
// Replace the hash for parent Manifests
- newMRoot := swarm.Manifest{}
+ newMRoot := &api.Manifest{}
for _, entry := range mroot.Entries {
if longestPathEntry.Path == entry.Path {
entry.Hash = newHash
@@ -173,9 +170,9 @@ func addEntryToManifest(ctx *cli.Context, mhash, path, hash, ctype string) strin
mroot = newMRoot
} else {
// Add the entry in the leaf Manifest
- newEntry := swarm.ManifestEntry{
- Path: path,
+ newEntry := api.ManifestEntry{
Hash: hash,
+ Path: path,
ContentType: ctype,
}
mroot.Entries = append(mroot.Entries, newEntry)
@@ -192,18 +189,10 @@ func addEntryToManifest(ctx *cli.Context, mhash, path, hash, ctype string) strin
func updateEntryInManifest(ctx *cli.Context, mhash, path, hash, ctype string) string {
var (
- bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
- client = swarm.NewClient(bzzapi)
- newEntry = swarm.ManifestEntry{
- Path: "",
- Hash: "",
- ContentType: "",
- }
- longestPathEntry = swarm.ManifestEntry{
- Path: "",
- Hash: "",
- ContentType: "",
- }
+ bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
+ client = swarm.NewClient(bzzapi)
+ newEntry = api.ManifestEntry{}
+ longestPathEntry = api.ManifestEntry{}
)
mroot, err := client.DownloadManifest(mhash)
@@ -237,7 +226,7 @@ func updateEntryInManifest(ctx *cli.Context, mhash, path, hash, ctype string) st
newHash := updateEntryInManifest(ctx, longestPathEntry.Hash, newPath, hash, ctype)
// Replace the hash for parent Manifests
- newMRoot := swarm.Manifest{}
+ newMRoot := &api.Manifest{}
for _, entry := range mroot.Entries {
if longestPathEntry.Path == entry.Path {
entry.Hash = newHash
@@ -250,12 +239,12 @@ func updateEntryInManifest(ctx *cli.Context, mhash, path, hash, ctype string) st
if newEntry.Path != "" {
// Replace the hash for leaf Manifest
- newMRoot := swarm.Manifest{}
+ newMRoot := &api.Manifest{}
for _, entry := range mroot.Entries {
if newEntry.Path == entry.Path {
- myEntry := swarm.ManifestEntry{
- Path: entry.Path,
+ myEntry := api.ManifestEntry{
Hash: hash,
+ Path: entry.Path,
ContentType: ctype,
}
newMRoot.Entries = append(newMRoot.Entries, myEntry)
@@ -276,18 +265,10 @@ func updateEntryInManifest(ctx *cli.Context, mhash, path, hash, ctype string) st
func removeEntryFromManifest(ctx *cli.Context, mhash, path string) string {
var (
- bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
- client = swarm.NewClient(bzzapi)
- entryToRemove = swarm.ManifestEntry{
- Path: "",
- Hash: "",
- ContentType: "",
- }
- longestPathEntry = swarm.ManifestEntry{
- Path: "",
- Hash: "",
- ContentType: "",
- }
+ bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
+ client = swarm.NewClient(bzzapi)
+ entryToRemove = api.ManifestEntry{}
+ longestPathEntry = api.ManifestEntry{}
)
mroot, err := client.DownloadManifest(mhash)
@@ -319,7 +300,7 @@ func removeEntryFromManifest(ctx *cli.Context, mhash, path string) string {
newHash := removeEntryFromManifest(ctx, longestPathEntry.Hash, newPath)
// Replace the hash for parent Manifests
- newMRoot := swarm.Manifest{}
+ newMRoot := &api.Manifest{}
for _, entry := range mroot.Entries {
if longestPathEntry.Path == entry.Path {
entry.Hash = newHash
@@ -331,7 +312,7 @@ func removeEntryFromManifest(ctx *cli.Context, mhash, path string) string {
if entryToRemove.Path != "" {
// remove the entry in this Manifest
- newMRoot := swarm.Manifest{}
+ newMRoot := &api.Manifest{}
for _, entry := range mroot.Entries {
if entryToRemove.Path != entry.Path {
newMRoot.Entries = append(newMRoot.Entries, entry)
diff --git a/cmd/swarm/upload.go b/cmd/swarm/upload.go
index 46f10c4be..42673ae21 100644
--- a/cmd/swarm/upload.go
+++ b/cmd/swarm/upload.go
@@ -18,13 +18,15 @@
package main
import (
- "encoding/json"
"fmt"
"io"
"io/ioutil"
+ "mime"
+ "net/http"
"os"
"os/user"
"path"
+ "path/filepath"
"strings"
"github.com/ethereum/go-ethereum/cmd/utils"
@@ -42,12 +44,10 @@ func upload(ctx *cli.Context) {
defaultPath = ctx.GlobalString(SwarmUploadDefaultPath.Name)
fromStdin = ctx.GlobalBool(SwarmUpFromStdinFlag.Name)
mimeType = ctx.GlobalString(SwarmUploadMimeType.Name)
+ client = swarm.NewClient(bzzapi)
+ file string
)
- var client = swarm.NewClient(bzzapi)
- var entry swarm.ManifestEntry
- var file string
-
if len(args) != 1 {
if fromStdin {
tmp, err := ioutil.TempFile("", "swarm-stdin")
@@ -66,41 +66,47 @@ func upload(ctx *cli.Context) {
utils.Fatalf("Need filename as the first and only argument")
}
} else {
- file = args[0]
+ file = expandPath(args[0])
+ }
+
+ if !wantManifest {
+ f, err := swarm.Open(file)
+ if err != nil {
+ utils.Fatalf("Error opening file: %s", err)
+ }
+ defer f.Close()
+ hash, err := client.UploadRaw(f, f.Size)
+ if err != nil {
+ utils.Fatalf("Upload failed: %s", err)
+ }
+ fmt.Println(hash)
+ return
}
- fi, err := os.Stat(expandPath(file))
+ stat, err := os.Stat(file)
if err != nil {
- utils.Fatalf("Failed to stat file: %v", err)
+ utils.Fatalf("Error opening file: %s", err)
}
- if fi.IsDir() {
+ var hash string
+ if stat.IsDir() {
if !recursive {
utils.Fatalf("Argument is a directory and recursive upload is disabled")
}
- if !wantManifest {
- utils.Fatalf("Manifest is required for directory uploads")
+ hash, err = client.UploadDirectory(file, defaultPath, "")
+ } else {
+ if mimeType == "" {
+ mimeType = detectMimeType(file)
}
- mhash, err := client.UploadDirectory(file, defaultPath)
+ f, err := swarm.Open(file)
if err != nil {
- utils.Fatalf("Failed to upload directory: %v", err)
+ utils.Fatalf("Error opening file: %s", err)
}
- fmt.Println(mhash)
- return
+ defer f.Close()
+ f.ContentType = mimeType
+ hash, err = client.Upload(f, "")
}
- entry, err = client.UploadFile(file, fi, mimeType)
if err != nil {
- utils.Fatalf("Upload failed: %v", err)
- }
- mroot := swarm.Manifest{Entries: []swarm.ManifestEntry{entry}}
- if !wantManifest {
- // Print the manifest. This is the only output to stdout.
- mrootJSON, _ := json.MarshalIndent(mroot, "", " ")
- fmt.Println(string(mrootJSON))
- return
- }
- hash, err := client.UploadManifest(mroot)
- if err != nil {
- utils.Fatalf("Manifest upload failed: %v", err)
+ utils.Fatalf("Upload failed: %s", err)
}
fmt.Println(hash)
}
@@ -128,3 +134,19 @@ func homeDir() string {
}
return ""
}
+
+func detectMimeType(file string) string {
+ if ext := filepath.Ext(file); ext != "" {
+ return mime.TypeByExtension(ext)
+ }
+ f, err := os.Open(file)
+ if err != nil {
+ return ""
+ }
+ defer f.Close()
+ buf := make([]byte, 512)
+ if n, _ := f.Read(buf); n > 0 {
+ return http.DetectContentType(buf)
+ }
+ return ""
+}
diff --git a/swarm/api/api.go b/swarm/api/api.go
index 7af27208d..ba1156f7e 100644
--- a/swarm/api/api.go
+++ b/swarm/api/api.go
@@ -17,6 +17,7 @@
package api
import (
+ "errors"
"fmt"
"io"
"net/http"
@@ -70,86 +71,50 @@ func (self *Api) Store(data io.Reader, size int64, wg *sync.WaitGroup) (key stor
type ErrResolve error
// DNS Resolver
-func (self *Api) Resolve(hostPort string, nameresolver bool) (storage.Key, error) {
- log.Trace(fmt.Sprintf("Resolving : %v", hostPort))
- if hashMatcher.MatchString(hostPort) || self.dns == nil {
- log.Trace(fmt.Sprintf("host is a contentHash: '%v'", hostPort))
- return storage.Key(common.Hex2Bytes(hostPort)), nil
+func (self *Api) Resolve(uri *URI) (storage.Key, error) {
+ log.Trace(fmt.Sprintf("Resolving : %v", uri.Addr))
+ if hashMatcher.MatchString(uri.Addr) {
+ log.Trace(fmt.Sprintf("addr is a hash: %q", uri.Addr))
+ return storage.Key(common.Hex2Bytes(uri.Addr)), nil
}
- if !nameresolver {
- return nil, fmt.Errorf("'%s' is not a content hash value.", hostPort)
+ if uri.Immutable() {
+ return nil, errors.New("refusing to resolve immutable address")
}
- contentHash, err := self.dns.Resolve(hostPort)
- if err != nil {
- err = ErrResolve(err)
- log.Warn(fmt.Sprintf("DNS error : %v", err))
- }
- log.Trace(fmt.Sprintf("host lookup: %v -> %v", hostPort, contentHash))
- return contentHash[:], err
-}
-func Parse(uri string) (hostPort, path string) {
- if uri == "" {
- return
- }
- parts := slashes.Split(uri, 3)
- var i int
- if len(parts) == 0 {
- return
+ if self.dns == nil {
+ return nil, fmt.Errorf("unable to resolve addr %q, resolver not configured", uri.Addr)
}
- // 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]
- }
+ hash, err := self.dns.Resolve(uri.Addr)
+ if err != nil {
+ log.Warn(fmt.Sprintf("DNS error resolving addr %q: %s", uri.Addr, err))
+ return nil, ErrResolve(err)
}
- log.Debug(fmt.Sprintf("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)
- log.Debug(fmt.Sprintf("Resolved '%s' to contentHash: '%s', path: '%s'", uri, contentHash, path))
- return contentHash[:], hostPort, path, err
+ log.Trace(fmt.Sprintf("addr lookup: %v -> %v", uri.Addr, hash))
+ return hash[:], nil
}
// Put provides singleton manifest creation on top of dpa store
-func (self *Api) Put(content, contentType string) (string, error) {
+func (self *Api) Put(content, contentType string) (storage.Key, error) {
r := strings.NewReader(content)
wg := &sync.WaitGroup{}
key, err := self.dpa.Store(r, int64(len(content)), wg, nil)
if err != nil {
- return "", err
+ return nil, 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
+ return nil, err
}
wg.Wait()
- return key.String(), nil
+ return key, 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)
- if err != nil {
- return nil, "", 500, fmt.Errorf("can't resolve: %v", err)
- }
-
- quitC := make(chan bool)
- trie, err := loadManifest(self.dpa, key, quitC)
+func (self *Api) Get(key storage.Key, path string) (reader storage.LazySectionReader, mimeType string, status int, err error) {
+ trie, err := loadManifest(self.dpa, key, nil)
if err != nil {
log.Warn(fmt.Sprintf("loadManifestTrie error: %v", err))
return
@@ -173,32 +138,25 @@ func (self *Api) Get(uri string, nameresolver bool) (reader storage.LazySectionR
return
}
-func (self *Api) Modify(uri, contentHash, contentType string, nameresolver bool) (newRootHash string, err error) {
- root, _, path, err := self.parseAndResolve(uri, nameresolver)
- if err != nil {
- return "", fmt.Errorf("can't resolve: %v", err)
- }
-
+func (self *Api) Modify(key storage.Key, path, contentHash, contentType string) (storage.Key, error) {
quitC := make(chan bool)
- trie, err := loadManifest(self.dpa, root, quitC)
+ trie, err := loadManifest(self.dpa, key, quitC)
if err != nil {
- return
+ return nil, err
}
-
if contentHash != "" {
- entry := &manifestTrieEntry{
+ entry := newManifestTrieEntry(&ManifestEntry{
Path: path,
- Hash: contentHash,
ContentType: contentType,
- }
+ }, nil)
+ entry.Hash = contentHash
trie.addEntry(entry, quitC)
} else {
trie.deleteEntry(path, quitC)
}
- err = trie.recalcAndStore()
- if err != nil {
- return
+ if err := trie.recalcAndStore(); err != nil {
+ return nil, err
}
- return trie.hash.String(), nil
+ return trie.hash, nil
}
diff --git a/swarm/api/api_test.go b/swarm/api/api_test.go
index 16e90dd32..c2d78c2dc 100644
--- a/swarm/api/api_test.go
+++ b/swarm/api/api_test.go
@@ -23,6 +23,7 @@ import (
"os"
"testing"
+ "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/swarm/storage"
)
@@ -81,8 +82,9 @@ func expResponse(content string, mimeType string, status int) *Response {
}
// 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)
+func testGet(t *testing.T, api *Api, bzzhash, path string) *testResponse {
+ key := storage.Key(common.Hex2Bytes(bzzhash))
+ reader, mimeType, status, err := api.Get(key, path)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -107,11 +109,11 @@ func TestApiPut(t *testing.T) {
content := "hello"
exp := expResponse(content, "text/plain", 0)
// exp := expResponse([]byte(content), "text/plain", 0)
- bzzhash, err := api.Put(content, exp.MimeType)
+ key, err := api.Put(content, exp.MimeType)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
- resp := testGet(t, api, bzzhash)
+ resp := testGet(t, api, key.String(), "")
checkResponse(t, resp, exp)
})
}
diff --git a/swarm/api/client/client.go b/swarm/api/client/client.go
index ef5335be3..f9c3e51e8 100644
--- a/swarm/api/client/client.go
+++ b/swarm/api/client/client.go
@@ -17,18 +17,23 @@
package client
import (
+ "archive/tar"
"bytes"
"encoding/json"
+ "errors"
"fmt"
"io"
"io/ioutil"
"mime"
+ "mime/multipart"
"net/http"
+ "net/textproto"
"os"
"path/filepath"
+ "strconv"
"strings"
- "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/swarm/api"
)
var (
@@ -36,18 +41,6 @@ var (
DefaultClient = NewClient(DefaultGateway)
)
-// Manifest represents a swarm manifest.
-type Manifest struct {
- Entries []ManifestEntry `json:"entries,omitempty"`
-}
-
-// ManifestEntry represents an entry in a swarm manifest.
-type ManifestEntry struct {
- Hash string `json:"hash,omitempty"`
- ContentType string `json:"contentType,omitempty"`
- Path string `json:"path,omitempty"`
-}
-
func NewClient(gateway string) *Client {
return &Client{
Gateway: gateway,
@@ -59,160 +52,207 @@ type Client struct {
Gateway string
}
-func (c *Client) UploadDirectory(dir string, defaultPath string) (string, error) {
- mhash, err := c.postRaw("application/json", 2, ioutil.NopCloser(bytes.NewReader([]byte("{}"))))
- if err != nil {
- return "", fmt.Errorf("failed to upload empty manifest")
+// UploadRaw uploads raw data to swarm and returns the resulting hash
+func (c *Client) UploadRaw(r io.Reader, size int64) (string, error) {
+ if size <= 0 {
+ return "", errors.New("data size must be greater than zero")
}
- if len(defaultPath) > 0 {
- fi, err := os.Stat(defaultPath)
- if err != nil {
- return "", err
- }
- mhash, err = c.uploadToManifest(mhash, "", defaultPath, fi)
- if err != nil {
- return "", err
- }
+ req, err := http.NewRequest("POST", c.Gateway+"/bzzr:/", r)
+ if err != nil {
+ return "", err
}
- prefix := filepath.ToSlash(filepath.Clean(dir)) + "/"
- err = filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
- if err != nil || fi.IsDir() {
- return err
- }
- if !strings.HasPrefix(path, dir) {
- return fmt.Errorf("path %s outside directory %s", path, dir)
- }
- uripath := strings.TrimPrefix(filepath.ToSlash(filepath.Clean(path)), prefix)
- mhash, err = c.uploadToManifest(mhash, uripath, path, fi)
- return err
- })
- return mhash, err
-}
-
-func (c *Client) UploadFile(file string, fi os.FileInfo, mimetype_hint string) (ManifestEntry, error) {
- var mimetype string
- hash, err := c.uploadFileContent(file, fi)
- if mimetype_hint != "" {
- mimetype = mimetype_hint
- log.Info("Mime type set by override", "mime", mimetype)
- } else {
- ext := filepath.Ext(file)
- log.Info("Ext", "ext", ext, "file", file)
- if ext != "" {
- mimetype = mime.TypeByExtension(filepath.Ext(fi.Name()))
- log.Info("Mime type set by fileextension", "mime", mimetype, "ext", filepath.Ext(file))
- } else {
- f, err := os.Open(file)
- if err == nil {
- first512 := make([]byte, 512)
- fread, _ := f.ReadAt(first512, 0)
- if fread > 0 {
- mimetype = http.DetectContentType(first512[:fread])
- log.Info("Mime type set by autodetection", "mime", mimetype)
- }
- }
- f.Close()
- }
-
+ req.ContentLength = size
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return "", err
}
- m := ManifestEntry{
- Hash: hash,
- ContentType: mime.TypeByExtension(filepath.Ext(fi.Name())),
+ defer res.Body.Close()
+ if res.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("unexpected HTTP status: %s", res.Status)
}
- return m, err
-}
-
-func (c *Client) uploadFileContent(file string, fi os.FileInfo) (string, error) {
- fd, err := os.Open(file)
+ data, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
- defer fd.Close()
- log.Info("Uploading swarm content", "file", file, "bytes", fi.Size())
- return c.postRaw("application/octet-stream", fi.Size(), fd)
+ return string(data), nil
}
-func (c *Client) UploadManifest(m Manifest) (string, error) {
- jsm, err := json.Marshal(m)
+// DownloadRaw downloads raw data from swarm
+func (c *Client) DownloadRaw(hash string) (io.ReadCloser, error) {
+ uri := c.Gateway + "/bzzr:/" + hash
+ res, err := http.DefaultClient.Get(uri)
if err != nil {
- panic(err)
+ return nil, err
+ }
+ if res.StatusCode != http.StatusOK {
+ res.Body.Close()
+ return nil, fmt.Errorf("unexpected HTTP status: %s", res.Status)
}
- log.Info("Uploading swarm manifest")
- return c.postRaw("application/json", int64(len(jsm)), ioutil.NopCloser(bytes.NewReader(jsm)))
+ return res.Body, nil
}
-func (c *Client) uploadToManifest(mhash string, path string, fpath string, fi os.FileInfo) (string, error) {
- fd, err := os.Open(fpath)
- if err != nil {
- return "", err
- }
- defer fd.Close()
- log.Info("Uploading swarm content and path", "file", fpath, "bytes", fi.Size(), "path", path)
- req, err := http.NewRequest("PUT", c.Gateway+"/bzz:/"+mhash+"/"+path, fd)
+// File represents a file in a swarm manifest and is used for uploading and
+// downloading content to and from swarm
+type File struct {
+ io.ReadCloser
+ api.ManifestEntry
+}
+
+// Open opens a local file which can then be passed to client.Upload to upload
+// it to swarm
+func Open(path string) (*File, error) {
+ f, err := os.Open(path)
if err != nil {
- return "", err
+ return nil, err
}
- req.Header.Set("content-type", mime.TypeByExtension(filepath.Ext(fi.Name())))
- req.ContentLength = fi.Size()
- resp, err := http.DefaultClient.Do(req)
+ stat, err := f.Stat()
if err != nil {
- return "", err
+ f.Close()
+ return nil, err
}
- defer resp.Body.Close()
- if resp.StatusCode >= 400 {
- return "", fmt.Errorf("bad status: %s", resp.Status)
+ return &File{
+ ReadCloser: f,
+ ManifestEntry: api.ManifestEntry{
+ ContentType: mime.TypeByExtension(filepath.Ext(path)),
+ Mode: int64(stat.Mode()),
+ Size: stat.Size(),
+ ModTime: stat.ModTime(),
+ },
+ }, nil
+}
+
+// Upload uploads a file to swarm and either adds it to an existing manifest
+// (if the manifest argument is non-empty) or creates a new manifest containing
+// the file, returning the resulting manifest hash (the file will then be
+// available at bzz:/<hash>/<path>)
+func (c *Client) Upload(file *File, manifest string) (string, error) {
+ if file.Size <= 0 {
+ return "", errors.New("file size must be greater than zero")
}
- content, err := ioutil.ReadAll(resp.Body)
- return string(content), err
+ return c.TarUpload(manifest, &FileUploader{file})
}
-func (c *Client) postRaw(mimetype string, size int64, body io.ReadCloser) (string, error) {
- req, err := http.NewRequest("POST", c.Gateway+"/bzzr:/", body)
+// Download downloads a file with the given path from the swarm manifest with
+// the given hash (i.e. it gets bzz:/<hash>/<path>)
+func (c *Client) Download(hash, path string) (*File, error) {
+ uri := c.Gateway + "/bzz:/" + hash + "/" + path
+ res, err := http.DefaultClient.Get(uri)
if err != nil {
- return "", err
+ return nil, err
}
- req.Header.Set("content-type", mimetype)
- req.ContentLength = size
- resp, err := http.DefaultClient.Do(req)
+ if res.StatusCode != http.StatusOK {
+ res.Body.Close()
+ return nil, fmt.Errorf("unexpected HTTP status: %s", res.Status)
+ }
+ return &File{
+ ReadCloser: res.Body,
+ ManifestEntry: api.ManifestEntry{
+ ContentType: res.Header.Get("Content-Type"),
+ Size: res.ContentLength,
+ },
+ }, nil
+}
+
+// UploadDirectory uploads a directory tree to swarm and either adds the files
+// to an existing manifest (if the manifest argument is non-empty) or creates a
+// new manifest, returning the resulting manifest hash (files from the
+// directory will then be available at bzz:/<hash>/path/to/file), with
+// the file specified in defaultPath being uploaded to the root of the manifest
+// (i.e. bzz:/<hash>/)
+func (c *Client) UploadDirectory(dir, defaultPath, manifest string) (string, error) {
+ stat, err := os.Stat(dir)
if err != nil {
return "", err
+ } else if !stat.IsDir() {
+ return "", fmt.Errorf("not a directory: %s", dir)
}
- defer resp.Body.Close()
- if resp.StatusCode >= 400 {
- return "", fmt.Errorf("bad status: %s", resp.Status)
- }
- content, err := ioutil.ReadAll(resp.Body)
- return string(content), err
+ return c.TarUpload(manifest, &DirectoryUploader{dir, defaultPath})
}
-func (c *Client) DownloadManifest(mhash string) (Manifest, error) {
+// DownloadDirectory downloads the files contained in a swarm manifest under
+// the given path into a local directory (existing files will be overwritten)
+func (c *Client) DownloadDirectory(hash, path, destDir string) error {
+ stat, err := os.Stat(destDir)
+ if err != nil {
+ return err
+ } else if !stat.IsDir() {
+ return fmt.Errorf("not a directory: %s", destDir)
+ }
- mroot := Manifest{}
- req, err := http.NewRequest("GET", c.Gateway+"/bzzr:/"+mhash, nil)
+ uri := c.Gateway + "/bzz:/" + hash + "/" + path
+ req, err := http.NewRequest("GET", uri, nil)
if err != nil {
- return mroot, err
+ return err
}
- resp, err := http.DefaultClient.Do(req)
+ req.Header.Set("Accept", "application/x-tar")
+ res, err := http.DefaultClient.Do(req)
if err != nil {
- return mroot, err
+ return err
}
- defer resp.Body.Close()
+ defer res.Body.Close()
+ if res.StatusCode != http.StatusOK {
+ return fmt.Errorf("unexpected HTTP status: %s", res.Status)
+ }
+ tr := tar.NewReader(res.Body)
+ for {
+ hdr, err := tr.Next()
+ if err == io.EOF {
+ return nil
+ } else if err != nil {
+ return err
+ }
+ // ignore the default path file
+ if hdr.Name == "" {
+ continue
+ }
- if resp.StatusCode >= 400 {
- return mroot, fmt.Errorf("bad status: %s", resp.Status)
+ dstPath := filepath.Join(destDir, filepath.Clean(strings.TrimPrefix(hdr.Name, path)))
+ if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
+ return err
+ }
+ var mode os.FileMode = 0644
+ if hdr.Mode > 0 {
+ mode = os.FileMode(hdr.Mode)
+ }
+ dst, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
+ if err != nil {
+ return err
+ }
+ n, err := io.Copy(dst, tr)
+ dst.Close()
+ if err != nil {
+ return err
+ } else if n != hdr.Size {
+ return fmt.Errorf("expected %s to be %d bytes but got %d", hdr.Name, hdr.Size, n)
+ }
+ }
+}
+// UploadManifest uploads the given manifest to swarm
+func (c *Client) UploadManifest(m *api.Manifest) (string, error) {
+ data, err := json.Marshal(m)
+ if err != nil {
+ return "", err
}
- content, err := ioutil.ReadAll(resp.Body)
+ return c.UploadRaw(bytes.NewReader(data), int64(len(data)))
+}
- err = json.Unmarshal(content, &mroot)
+// DownloadManifest downloads a swarm manifest
+func (c *Client) DownloadManifest(hash string) (*api.Manifest, error) {
+ res, err := c.DownloadRaw(hash)
if err != nil {
- return mroot, fmt.Errorf("Manifest %v is malformed: %v", mhash, err)
+ return nil, err
+ }
+ defer res.Close()
+ var manifest api.Manifest
+ if err := json.NewDecoder(res).Decode(&manifest); err != nil {
+ return nil, err
}
- return mroot, err
+ return &manifest, nil
}
-// ManifestFileList downloads the manifest with the given hash and generates a
-// list of files and directory prefixes which have the specified prefix.
+// List list files in a swarm manifest which have the given prefix, grouping
+// common prefixes using "/" as a delimiter.
//
// For example, if the manifest represents the following directory structure:
//
@@ -226,97 +266,200 @@ func (c *Client) DownloadManifest(mhash string) (Manifest, error) {
// - a prefix of "" would return [dir1/, file1.txt, file2.txt]
// - a prefix of "file" would return [file1.txt, file2.txt]
// - a prefix of "dir1/" would return [dir1/dir2/, dir1/file3.txt]
-func (c *Client) ManifestFileList(hash, prefix string) (entries []ManifestEntry, err error) {
- manifest, err := c.DownloadManifest(hash)
+//
+// where entries ending with "/" are common prefixes.
+func (c *Client) List(hash, prefix string) (*api.ManifestList, error) {
+ res, err := http.DefaultClient.Get(c.Gateway + "/bzz:/" + hash + "/" + prefix + "?list=true")
if err != nil {
return nil, err
}
+ defer res.Body.Close()
+ if res.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("unexpected HTTP status: %s", res.Status)
+ }
+ var list api.ManifestList
+ if err := json.NewDecoder(res.Body).Decode(&list); err != nil {
+ return nil, err
+ }
+ return &list, nil
+}
+
+// Uploader uploads files to swarm using a provided UploadFn
+type Uploader interface {
+ Upload(UploadFn) error
+}
+
+type UploaderFunc func(UploadFn) error
+
+func (u UploaderFunc) Upload(upload UploadFn) error {
+ return u(upload)
+}
+
+// DirectoryUploader uploads all files in a directory, optionally uploading
+// a file to the default path
+type DirectoryUploader struct {
+ Dir string
+ DefaultPath string
+}
- // handleFile handles a manifest entry which is a direct reference to a
- // file (i.e. it is not a swarm manifest)
- handleFile := func(entry ManifestEntry) {
- // ignore the file if it doesn't have the specified prefix
- if !strings.HasPrefix(entry.Path, prefix) {
- return
+// Upload performs the upload of the directory and default path
+func (d *DirectoryUploader) Upload(upload UploadFn) error {
+ if d.DefaultPath != "" {
+ file, err := Open(d.DefaultPath)
+ if err != nil {
+ return err
}
- // if the path after the prefix contains a directory separator,
- // add a directory prefix to the entries, otherwise add the
- // file
- suffix := strings.TrimPrefix(entry.Path, prefix)
- if sepIndex := strings.Index(suffix, "/"); sepIndex > -1 {
- entries = append(entries, ManifestEntry{
- Path: prefix + suffix[:sepIndex+1],
- ContentType: "DIR",
- })
- } else {
- if entry.Path == "" {
- entry.Path = "/"
- }
- entries = append(entries, entry)
+ if err := upload(file); err != nil {
+ return err
}
}
-
- // handleManifest handles a manifest entry which is a reference to
- // another swarm manifest.
- handleManifest := func(entry ManifestEntry) error {
- // if the manifest's path is a prefix of the specified prefix
- // then just recurse into the manifest by stripping its path
- // from the prefix
- if strings.HasPrefix(prefix, entry.Path) {
- subPrefix := strings.TrimPrefix(prefix, entry.Path)
- subEntries, err := c.ManifestFileList(entry.Hash, subPrefix)
- if err != nil {
- return err
- }
- // prefix the manifest's path to the sub entries and
- // add them to the returned entries
- for i, subEntry := range subEntries {
- subEntry.Path = entry.Path + subEntry.Path
- subEntries[i] = subEntry
- }
- entries = append(entries, subEntries...)
+ return filepath.Walk(d.Dir, func(path string, f os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if f.IsDir() {
return nil
}
+ file, err := Open(path)
+ if err != nil {
+ return err
+ }
+ relPath, err := filepath.Rel(d.Dir, path)
+ if err != nil {
+ return err
+ }
+ file.Path = filepath.ToSlash(relPath)
+ return upload(file)
+ })
+}
- // if the manifest's path has the specified prefix, then if the
- // path after the prefix contains a directory separator, add a
- // directory prefix to the entries, otherwise recurse into the
- // manifest
- if strings.HasPrefix(entry.Path, prefix) {
- suffix := strings.TrimPrefix(entry.Path, prefix)
- sepIndex := strings.Index(suffix, "/")
- if sepIndex > -1 {
- entries = append(entries, ManifestEntry{
- Path: prefix + suffix[:sepIndex+1],
- ContentType: "DIR",
- })
- return nil
- }
- subEntries, err := c.ManifestFileList(entry.Hash, "")
- if err != nil {
- return err
- }
- // prefix the manifest's path to the sub entries and
- // add them to the returned entries
- for i, subEntry := range subEntries {
- subEntry.Path = entry.Path + subEntry.Path
- subEntries[i] = subEntry
- }
- entries = append(entries, subEntries...)
- return nil
+// FileUploader uploads a single file
+type FileUploader struct {
+ File *File
+}
+
+// Upload performs the upload of the file
+func (f *FileUploader) Upload(upload UploadFn) error {
+ return upload(f.File)
+}
+
+// UploadFn is the type of function passed to an Uploader to perform the upload
+// of a single file (for example, a directory uploader would call a provided
+// UploadFn for each file in the directory tree)
+type UploadFn func(file *File) error
+
+// TarUpload uses the given Uploader to upload files to swarm as a tar stream,
+// returning the resulting manifest hash
+func (c *Client) TarUpload(hash string, uploader Uploader) (string, error) {
+ reqR, reqW := io.Pipe()
+ defer reqR.Close()
+ req, err := http.NewRequest("POST", c.Gateway+"/bzz:/"+hash, reqR)
+ if err != nil {
+ return "", err
+ }
+ req.Header.Set("Content-Type", "application/x-tar")
+
+ // use 'Expect: 100-continue' so we don't send the request body if
+ // the server refuses the request
+ req.Header.Set("Expect", "100-continue")
+
+ tw := tar.NewWriter(reqW)
+
+ // define an UploadFn which adds files to the tar stream
+ uploadFn := func(file *File) error {
+ hdr := &tar.Header{
+ Name: file.Path,
+ Mode: file.Mode,
+ Size: file.Size,
+ ModTime: file.ModTime,
+ Xattrs: map[string]string{
+ "user.swarm.content-type": file.ContentType,
+ },
+ }
+ if err := tw.WriteHeader(hdr); err != nil {
+ return err
}
- return nil
+ _, err = io.Copy(tw, file)
+ return err
}
- for _, entry := range manifest.Entries {
- if entry.ContentType == "application/bzz-manifest+json" {
- if err := handleManifest(entry); err != nil {
- return nil, err
- }
- } else {
- handleFile(entry)
+ // run the upload in a goroutine so we can send the request headers and
+ // wait for a '100 Continue' response before sending the tar stream
+ go func() {
+ err := uploader.Upload(uploadFn)
+ if err == nil {
+ err = tw.Close()
}
+ reqW.CloseWithError(err)
+ }()
+
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return "", err
+ }
+ defer res.Body.Close()
+ if res.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("unexpected HTTP status: %s", res.Status)
+ }
+ data, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ return "", err
+ }
+ return string(data), nil
+}
+
+// MultipartUpload uses the given Uploader to upload files to swarm as a
+// multipart form, returning the resulting manifest hash
+func (c *Client) MultipartUpload(hash string, uploader Uploader) (string, error) {
+ reqR, reqW := io.Pipe()
+ defer reqR.Close()
+ req, err := http.NewRequest("POST", c.Gateway+"/bzz:/"+hash, reqR)
+ if err != nil {
+ return "", err
}
- return
+ // use 'Expect: 100-continue' so we don't send the request body if
+ // the server refuses the request
+ req.Header.Set("Expect", "100-continue")
+
+ mw := multipart.NewWriter(reqW)
+ req.Header.Set("Content-Type", fmt.Sprintf("multipart/form-data; boundary=%q", mw.Boundary()))
+
+ // define an UploadFn which adds files to the multipart form
+ uploadFn := func(file *File) error {
+ hdr := make(textproto.MIMEHeader)
+ hdr.Set("Content-Disposition", fmt.Sprintf("form-data; name=%q", file.Path))
+ hdr.Set("Content-Type", file.ContentType)
+ hdr.Set("Content-Length", strconv.FormatInt(file.Size, 10))
+ w, err := mw.CreatePart(hdr)
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(w, file)
+ return err
+ }
+
+ // run the upload in a goroutine so we can send the request headers and
+ // wait for a '100 Continue' response before sending the multipart form
+ go func() {
+ err := uploader.Upload(uploadFn)
+ if err == nil {
+ err = mw.Close()
+ }
+ reqW.CloseWithError(err)
+ }()
+
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return "", err
+ }
+ defer res.Body.Close()
+ if res.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("unexpected HTTP status: %s", res.Status)
+ }
+ data, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ return "", err
+ }
+ return string(data), nil
}
diff --git a/swarm/api/client/client_test.go b/swarm/api/client/client_test.go
index 135474475..4d02ceaf4 100644
--- a/swarm/api/client/client_test.go
+++ b/swarm/api/client/client_test.go
@@ -17,6 +17,7 @@
package client
import (
+ "bytes"
"io/ioutil"
"os"
"path/filepath"
@@ -24,52 +25,221 @@ import (
"sort"
"testing"
+ "github.com/ethereum/go-ethereum/swarm/api"
"github.com/ethereum/go-ethereum/swarm/testutil"
)
-func TestClientManifestFileList(t *testing.T) {
+// TestClientUploadDownloadRaw test uploading and downloading raw data to swarm
+func TestClientUploadDownloadRaw(t *testing.T) {
srv := testutil.NewTestSwarmServer(t)
defer srv.Close()
+ client := NewClient(srv.URL)
+
+ // upload some raw data
+ data := []byte("foo123")
+ hash, err := client.UploadRaw(bytes.NewReader(data), int64(len(data)))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // check we can download the same data
+ res, err := client.DownloadRaw(hash)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer res.Close()
+ gotData, err := ioutil.ReadAll(res)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(gotData, data) {
+ t.Fatalf("expected downloaded data to be %q, got %q", data, gotData)
+ }
+}
+
+// TestClientUploadDownloadFiles test uploading and downloading files to swarm
+// manifests
+func TestClientUploadDownloadFiles(t *testing.T) {
+ srv := testutil.NewTestSwarmServer(t)
+ defer srv.Close()
+
+ client := NewClient(srv.URL)
+ upload := func(manifest, path string, data []byte) string {
+ file := &File{
+ ReadCloser: ioutil.NopCloser(bytes.NewReader(data)),
+ ManifestEntry: api.ManifestEntry{
+ Path: path,
+ ContentType: "text/plain",
+ Size: int64(len(data)),
+ },
+ }
+ hash, err := client.Upload(file, manifest)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return hash
+ }
+ checkDownload := func(manifest, path string, expected []byte) {
+ file, err := client.Download(manifest, path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer file.Close()
+ if file.Size != int64(len(expected)) {
+ t.Fatalf("expected downloaded file to be %d bytes, got %d", len(expected), file.Size)
+ }
+ if file.ContentType != file.ContentType {
+ t.Fatalf("expected downloaded file to have type %q, got %q", file.ContentType, file.ContentType)
+ }
+ data, err := ioutil.ReadAll(file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(data, expected) {
+ t.Fatalf("expected downloaded data to be %q, got %q", expected, data)
+ }
+ }
+
+ // upload a file to the root of a manifest
+ rootData := []byte("some-data")
+ rootHash := upload("", "", rootData)
+
+ // check we can download the root file
+ checkDownload(rootHash, "", rootData)
+
+ // upload another file to the same manifest
+ otherData := []byte("some-other-data")
+ newHash := upload(rootHash, "some/other/path", otherData)
+
+ // check we can download both files from the new manifest
+ checkDownload(newHash, "", rootData)
+ checkDownload(newHash, "some/other/path", otherData)
+
+ // replace the root file with different data
+ newHash = upload(newHash, "", otherData)
+
+ // check both files have the other data
+ checkDownload(newHash, "", otherData)
+ checkDownload(newHash, "some/other/path", otherData)
+}
+
+var testDirFiles = []string{
+ "file1.txt",
+ "file2.txt",
+ "dir1/file3.txt",
+ "dir1/file4.txt",
+ "dir2/file5.txt",
+ "dir2/dir3/file6.txt",
+ "dir2/dir4/file7.txt",
+ "dir2/dir4/file8.txt",
+}
+
+func newTestDirectory(t *testing.T) string {
dir, err := ioutil.TempDir("", "swarm-client-test")
if err != nil {
t.Fatal(err)
}
- files := []string{
- "file1.txt",
- "file2.txt",
- "dir1/file3.txt",
- "dir1/file4.txt",
- "dir2/file5.txt",
- "dir2/dir3/file6.txt",
- "dir2/dir4/file7.txt",
- "dir2/dir4/file8.txt",
- }
- for _, file := range files {
+
+ for _, file := range testDirFiles {
path := filepath.Join(dir, file)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+ os.RemoveAll(dir)
t.Fatalf("error creating dir for %s: %s", path, err)
}
- if err := ioutil.WriteFile(path, []byte("data"), 0644); err != nil {
+ if err := ioutil.WriteFile(path, []byte(file), 0644); err != nil {
+ os.RemoveAll(dir)
t.Fatalf("error writing file %s: %s", path, err)
}
}
+ return dir
+}
+
+// TestClientUploadDownloadDirectory tests uploading and downloading a
+// directory of files to a swarm manifest
+func TestClientUploadDownloadDirectory(t *testing.T) {
+ srv := testutil.NewTestSwarmServer(t)
+ defer srv.Close()
+
+ dir := newTestDirectory(t)
+ defer os.RemoveAll(dir)
+
+ // upload the directory
client := NewClient(srv.URL)
+ defaultPath := filepath.Join(dir, testDirFiles[0])
+ hash, err := client.UploadDirectory(dir, defaultPath, "")
+ if err != nil {
+ t.Fatalf("error uploading directory: %s", err)
+ }
+
+ // check we can download the individual files
+ checkDownloadFile := func(path string, expected []byte) {
+ file, err := client.Download(hash, path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer file.Close()
+ data, err := ioutil.ReadAll(file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(data, expected) {
+ t.Fatalf("expected data to be %q, got %q", expected, data)
+ }
+ }
+ for _, file := range testDirFiles {
+ checkDownloadFile(file, []byte(file))
+ }
+
+ // check we can download the default path
+ checkDownloadFile("", []byte(testDirFiles[0]))
+
+ // check we can download the directory
+ tmp, err := ioutil.TempDir("", "swarm-client-test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(tmp)
+ if err := client.DownloadDirectory(hash, "", tmp); err != nil {
+ t.Fatal(err)
+ }
+ for _, file := range testDirFiles {
+ data, err := ioutil.ReadFile(filepath.Join(tmp, file))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(data, []byte(file)) {
+ t.Fatalf("expected data to be %q, got %q", file, data)
+ }
+ }
+}
+
+// TestClientFileList tests listing files in a swarm manifest
+func TestClientFileList(t *testing.T) {
+ srv := testutil.NewTestSwarmServer(t)
+ defer srv.Close()
- hash, err := client.UploadDirectory(dir, "")
+ dir := newTestDirectory(t)
+ defer os.RemoveAll(dir)
+
+ client := NewClient(srv.URL)
+ hash, err := client.UploadDirectory(dir, "", "")
if err != nil {
t.Fatalf("error uploading directory: %s", err)
}
ls := func(prefix string) []string {
- entries, err := client.ManifestFileList(hash, prefix)
+ list, err := client.List(hash, prefix)
if err != nil {
t.Fatal(err)
}
- paths := make([]string, len(entries))
- for i, entry := range entries {
- paths[i] = entry.Path
+ paths := make([]string, 0, len(list.CommonPrefixes)+len(list.Entries))
+ for _, prefix := range list.CommonPrefixes {
+ paths = append(paths, prefix)
+ }
+ for _, entry := range list.Entries {
+ paths = append(paths, entry.Path)
}
sort.Strings(paths)
return paths
@@ -99,7 +269,59 @@ func TestClientManifestFileList(t *testing.T) {
for prefix, expected := range tests {
actual := ls(prefix)
if !reflect.DeepEqual(actual, expected) {
- t.Fatalf("expected prefix %q to return paths %v, got %v", prefix, expected, actual)
+ t.Fatalf("expected prefix %q to return %v, got %v", prefix, expected, actual)
+ }
+ }
+}
+
+// TestClientMultipartUpload tests uploading files to swarm using a multipart
+// upload
+func TestClientMultipartUpload(t *testing.T) {
+ srv := testutil.NewTestSwarmServer(t)
+ defer srv.Close()
+
+ // define an uploader which uploads testDirFiles with some data
+ data := []byte("some-data")
+ uploader := UploaderFunc(func(upload UploadFn) error {
+ for _, name := range testDirFiles {
+ file := &File{
+ ReadCloser: ioutil.NopCloser(bytes.NewReader(data)),
+ ManifestEntry: api.ManifestEntry{
+ Path: name,
+ ContentType: "text/plain",
+ Size: int64(len(data)),
+ },
+ }
+ if err := upload(file); err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+
+ // upload the files as a multipart upload
+ client := NewClient(srv.URL)
+ hash, err := client.MultipartUpload("", uploader)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // check we can download the individual files
+ checkDownloadFile := func(path string) {
+ file, err := client.Download(hash, path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer file.Close()
+ gotData, err := ioutil.ReadAll(file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(gotData, data) {
+ t.Fatalf("expected data to be %q, got %q", data, gotData)
}
}
+ for _, file := range testDirFiles {
+ checkDownloadFile(file)
+ }
}
diff --git a/swarm/api/filesystem.go b/swarm/api/filesystem.go
index c2583e265..e7deaa32f 100644
--- a/swarm/api/filesystem.go
+++ b/swarm/api/filesystem.go
@@ -22,6 +22,7 @@ import (
"io"
"net/http"
"os"
+ "path"
"path/filepath"
"sync"
@@ -43,6 +44,8 @@ func NewFileSystem(api *Api) *FileSystem {
// Upload replicates a local directory as a manifest file and uploads it
// using dpa store
// TODO: localpath should point to a manifest
+//
+// DEPRECATED: Use the HTTP API instead
func (self *FileSystem) Upload(lpath, index string) (string, error) {
var list []*manifestTrieEntry
localpath, err := filepath.Abs(filepath.Clean(lpath))
@@ -72,9 +75,7 @@ func (self *FileSystem) Upload(lpath, index string) (string, error) {
if path[:start] != localpath {
return fmt.Errorf("Path prefix of '%s' does not match localpath '%s'", path, localpath)
}
- entry := &manifestTrieEntry{
- Path: filepath.ToSlash(path),
- }
+ entry := newManifestTrieEntry(&ManifestEntry{Path: filepath.ToSlash(path)}, nil)
list = append(list, entry)
}
return err
@@ -91,9 +92,7 @@ func (self *FileSystem) Upload(lpath, index string) (string, error) {
if localpath[:start] != dir {
return "", fmt.Errorf("Path prefix of '%s' does not match dir '%s'", localpath, dir)
}
- entry := &manifestTrieEntry{
- Path: filepath.ToSlash(localpath),
- }
+ entry := newManifestTrieEntry(&ManifestEntry{Path: filepath.ToSlash(localpath)}, nil)
list = append(list, entry)
}
@@ -153,11 +152,10 @@ func (self *FileSystem) Upload(lpath, index string) (string, error) {
}
entry.Path = RegularSlashes(entry.Path[start:])
if entry.Path == index {
- ientry := &manifestTrieEntry{
- Path: "",
- Hash: entry.Hash,
+ ientry := newManifestTrieEntry(&ManifestEntry{
ContentType: entry.ContentType,
- }
+ }, nil)
+ ientry.Hash = entry.Hash
trie.addEntry(ientry, quitC)
}
trie.addEntry(entry, quitC)
@@ -174,6 +172,8 @@ func (self *FileSystem) Upload(lpath, index string) (string, error) {
// Download replicates the manifest path structure on the local filesystem
// under localpath
+//
+// DEPRECATED: Use the HTTP API instead
func (self *FileSystem) Download(bzzpath, localpath string) error {
lpath, err := filepath.Abs(filepath.Clean(localpath))
if err != nil {
@@ -185,10 +185,15 @@ func (self *FileSystem) Download(bzzpath, localpath string) error {
}
//resolving host and port
- key, _, path, err := self.api.parseAndResolve(bzzpath, true)
+ uri, err := Parse(path.Join("bzz:/", bzzpath))
+ if err != nil {
+ return err
+ }
+ key, err := self.api.Resolve(uri)
if err != nil {
return err
}
+ path := uri.Path
if len(path) > 0 {
path += "/"
diff --git a/swarm/api/filesystem_test.go b/swarm/api/filesystem_test.go
index 4a27cb1da..8a15e735d 100644
--- a/swarm/api/filesystem_test.go
+++ b/swarm/api/filesystem_test.go
@@ -23,6 +23,9 @@ import (
"path/filepath"
"sync"
"testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/swarm/storage"
)
var testDownloadDir, _ = ioutil.TempDir(os.TempDir(), "bzz-test")
@@ -51,16 +54,17 @@ func TestApiDirUpload0(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}
content := readPath(t, "testdata", "test0", "index.html")
- resp := testGet(t, api, bzzhash+"/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")
+ resp = testGet(t, api, bzzhash, "index.css")
exp = expResponse(content, "text/css", 0)
checkResponse(t, resp, exp)
- _, _, _, err = api.Get(bzzhash, true)
+ key := storage.Key(common.Hex2Bytes(bzzhash))
+ _, _, _, err = api.Get(key, "")
if err == nil {
t.Fatalf("expected error: %v", err)
}
@@ -90,7 +94,8 @@ func TestApiDirUploadModify(t *testing.T) {
return
}
- bzzhash, err = api.Modify(bzzhash+"/index.html", "", "", true)
+ key := storage.Key(common.Hex2Bytes(bzzhash))
+ key, err = api.Modify(key, "index.html", "", "")
if err != nil {
t.Errorf("unexpected error: %v", err)
return
@@ -107,32 +112,33 @@ func TestApiDirUploadModify(t *testing.T) {
t.Errorf("unexpected error: %v", err)
return
}
- bzzhash, err = api.Modify(bzzhash+"/index2.html", hash.Hex(), "text/html; charset=utf-8", true)
+ key, err = api.Modify(key, "index2.html", hash.Hex(), "text/html; charset=utf-8")
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)
+ key, err = api.Modify(key, "img/logo.png", hash.Hex(), "text/html; charset=utf-8")
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
+ bzzhash = key.String()
content := readPath(t, "testdata", "test0", "index.html")
- resp := testGet(t, api, bzzhash+"/index2.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")
+ 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")
+ resp = testGet(t, api, bzzhash, "index.css")
exp = expResponse(content, "text/css", 0)
checkResponse(t, resp, exp)
- _, _, _, err = api.Get(bzzhash, true)
+ _, _, _, err = api.Get(key, "")
if err == nil {
t.Errorf("expected error: %v", err)
}
@@ -149,7 +155,7 @@ func TestApiDirUploadWithRootFile(t *testing.T) {
}
content := readPath(t, "testdata", "test0", "index.html")
- resp := testGet(t, api, bzzhash)
+ resp := testGet(t, api, bzzhash, "")
exp := expResponse(content, "text/html; charset=utf-8", 0)
checkResponse(t, resp, exp)
})
@@ -165,7 +171,7 @@ func TestApiFileUpload(t *testing.T) {
}
content := readPath(t, "testdata", "test0", "index.html")
- resp := testGet(t, api, bzzhash+"/index.html")
+ resp := testGet(t, api, bzzhash, "index.html")
exp := expResponse(content, "text/html; charset=utf-8", 0)
checkResponse(t, resp, exp)
})
@@ -181,7 +187,7 @@ func TestApiFileUploadWithRootFile(t *testing.T) {
}
content := readPath(t, "testdata", "test0", "index.html")
- resp := testGet(t, api, bzzhash)
+ 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_test.go b/swarm/api/http/roundtripper_test.go
index fc74f5d3a..f99c4f35e 100644
--- a/swarm/api/http/roundtripper_test.go
+++ b/swarm/api/http/roundtripper_test.go
@@ -18,14 +18,14 @@ package http
import (
"io/ioutil"
+ "net"
"net/http"
+ "net/http/httptest"
"strings"
"testing"
"time"
)
-const port = "3222"
-
func TestRoundTripper(t *testing.T) {
serveMux := http.NewServeMux()
serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
@@ -36,9 +36,12 @@ func TestRoundTripper(t *testing.T) {
http.Error(w, "Method "+r.Method+" is not supported.", http.StatusMethodNotAllowed)
}
})
- go http.ListenAndServe(":"+port, serveMux)
- rt := &RoundTripper{Port: port}
+ srv := httptest.NewServer(serveMux)
+ defer srv.Close()
+
+ host, port, _ := net.SplitHostPort(srv.Listener.Addr().String())
+ rt := &RoundTripper{Host: host, Port: port}
trans := &http.Transport{}
trans.RegisterProtocol("bzz", rt)
client := &http.Client{Transport: trans}
diff --git a/swarm/api/http/server.go b/swarm/api/http/server.go
index 44e2c203a..849b9e10f 100644
--- a/swarm/api/http/server.go
+++ b/swarm/api/http/server.go
@@ -20,13 +20,19 @@ A simple http server interface to Swarm
package http
import (
- "bytes"
+ "archive/tar"
+ "encoding/json"
+ "errors"
"fmt"
"io"
+ "io/ioutil"
+ "mime"
+ "mime/multipart"
"net/http"
- "regexp"
+ "os"
+ "path"
+ "strconv"
"strings"
- "sync"
"time"
"github.com/ethereum/go-ethereum/common"
@@ -36,26 +42,6 @@ import (
"github.com/rs/cors"
)
-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
-}
-
// ServerConfig is the basic configuration needed for the HTTP server and also
// includes CORS settings.
type ServerConfig struct {
@@ -94,242 +80,569 @@ type Server struct {
api *api.Api
}
-func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- 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
- // }
- // }
- log.Debug(fmt.Sprintf("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
- log.Debug(fmt.Sprintf("BZZ request URI: '%s'", uri))
-
- path := bzzPrefix.ReplaceAllStringFunc(uri, func(p string) string {
- proto = p
- return ""
- })
+// Request wraps http.Request and also includes the parsed bzz URI
+type Request struct {
+ http.Request
+
+ uri *api.URI
+}
- // protocol identification (ugly)
- if proto == "" {
- log.Error(fmt.Sprintf("[BZZ] Swarm: Protocol error in request `%s`.", uri))
- http.Error(w, "Invalid request URL: need access protocol (bzz:/, bzzr:/, bzzi:/) as first element in path.", http.StatusBadRequest)
+// HandlePostRaw handles a POST request to a raw bzzr:/ URI, stores the request
+// body in swarm and returns the resulting storage key as a text/plain response
+func (s *Server) HandlePostRaw(w http.ResponseWriter, r *Request) {
+ if r.uri.Path != "" {
+ s.BadRequest(w, r, "raw POST request cannot contain a path")
return
}
- if len(proto) > 4 {
- raw = proto[1:5] == "bzzr"
- nameresolver = proto[1:5] != "bzzi"
+
+ if r.Header.Get("Content-Length") == "" {
+ s.BadRequest(w, r, "missing Content-Length header in request")
+ return
}
- log.Debug("", "msg", log.Lazy{Fn: func() string {
- return fmt.Sprintf("[BZZ] Swarm: %s request over protocol %s '%s' received.", r.Method, proto, path)
- }})
+ key, err := s.api.Store(r.Body, r.ContentLength, nil)
+ if err != nil {
+ s.Error(w, r, err)
+ return
+ }
+ s.logDebug("content for %s stored", key.Log())
+
+ w.Header().Set("Content-Type", "text/plain")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, key)
+}
+
+// HandlePostFiles handles a POST request (or deprecated PUT request) to
+// bzz:/<hash>/<path> which contains either a single file or multiple files
+// (either a tar archive or multipart form), adds those files either to an
+// existing manifest or to a new manifest under <path> and returns the
+// resulting manifest hash as a text/plain response
+func (s *Server) HandlePostFiles(w http.ResponseWriter, r *Request) {
+ contentType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
+ if err != nil {
+ s.BadRequest(w, r, err.Error())
+ return
+ }
- switch {
- case r.Method == "POST" || r.Method == "PUT":
- if r.Header.Get("content-length") == "" {
- http.Error(w, "Missing Content-Length header in request.", http.StatusBadRequest)
+ var key storage.Key
+ if r.uri.Addr != "" {
+ key, err = s.api.Resolve(r.uri)
+ if err != nil {
+ s.Error(w, r, fmt.Errorf("error resolving %s: %s", r.uri.Addr, err))
return
}
- key, err := s.api.Store(io.LimitReader(r.Body, r.ContentLength), r.ContentLength, nil)
- if err == nil {
- log.Debug(fmt.Sprintf("Content for %v stored", key.Log()))
- } else {
- http.Error(w, err.Error(), http.StatusBadRequest)
+ } else {
+ key, err = s.api.NewManifest()
+ if err != nil {
+ s.Error(w, r, err)
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
+ }
+
+ newKey, err := s.updateManifest(key, func(mw *api.ManifestWriter) error {
+ switch contentType {
+
+ case "application/x-tar":
+ return s.handleTarUpload(r, mw)
+
+ case "multipart/form-data":
+ return s.handleMultipartUpload(r, params["boundary"], mw)
+
+ default:
+ return s.handleDirectUpload(r, mw)
+ }
+ })
+ if err != nil {
+ s.Error(w, r, fmt.Errorf("error creating manifest: %s", err))
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/plain")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, newKey)
+}
+
+func (s *Server) handleTarUpload(req *Request, mw *api.ManifestWriter) error {
+ tr := tar.NewReader(req.Body)
+ for {
+ hdr, err := tr.Next()
+ if err == io.EOF {
+ return nil
+ } else if err != nil {
+ return fmt.Errorf("error reading tar stream: %s", err)
+ }
+
+ // only store regular files
+ if !hdr.FileInfo().Mode().IsRegular() {
+ continue
+ }
+
+ // add the entry under the path from the request
+ path := path.Join(req.uri.Path, hdr.Name)
+ entry := &api.ManifestEntry{
+ Path: path,
+ ContentType: hdr.Xattrs["user.swarm.content-type"],
+ Mode: hdr.Mode,
+ Size: hdr.Size,
+ ModTime: hdr.ModTime,
+ }
+ s.logDebug("adding %s (%d bytes) to new manifest", entry.Path, entry.Size)
+ contentKey, err := mw.AddEntry(tr, entry)
+ if err != nil {
+ return fmt.Errorf("error adding manifest entry from tar stream: %s", err)
+ }
+ s.logDebug("content for %s stored", contentKey.Log())
+ }
+}
+
+func (s *Server) handleMultipartUpload(req *Request, boundary string, mw *api.ManifestWriter) error {
+ mr := multipart.NewReader(req.Body, boundary)
+ for {
+ part, err := mr.NextPart()
+ if err == io.EOF {
+ return nil
+ } else if err != nil {
+ return fmt.Errorf("error reading multipart form: %s", err)
+ }
+
+ var size int64
+ var reader io.Reader = part
+ if contentLength := part.Header.Get("Content-Length"); contentLength != "" {
+ size, err = strconv.ParseInt(contentLength, 10, 64)
+ if err != nil {
+ return fmt.Errorf("error parsing multipart content length: %s", err)
}
+ reader = part
} 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
- log.Debug(fmt.Sprintf("Modify '%s' to store %v as '%s'.", path, key.Log(), mime))
- newKey, err := s.api.Modify(path, common.Bytes2Hex(key), mime, nameresolver)
- if err == nil {
- log.Debug(fmt.Sprintf("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
- }
+ // copy the part to a tmp file to get its size
+ tmp, err := ioutil.TempFile("", "swarm-multipart")
+ if err != nil {
+ return err
}
- }
- case r.Method == "DELETE":
- if raw {
- http.Error(w, "No DELETE to /raw allowed.", http.StatusBadRequest)
- return
- } else {
- path = api.RegularSlashes(path)
- log.Debug(fmt.Sprintf("Delete '%s'.", path))
- newKey, err := s.api.Modify(path, "", "", nameresolver)
- if err == nil {
- log.Debug(fmt.Sprintf("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
+ defer os.Remove(tmp.Name())
+ defer tmp.Close()
+ size, err = io.Copy(tmp, part)
+ if err != nil {
+ return fmt.Errorf("error copying multipart content: %s", err)
+ }
+ if _, err := tmp.Seek(0, os.SEEK_SET); err != nil {
+ return fmt.Errorf("error copying multipart content: %s", err)
}
+ reader = tmp
+ }
+
+ // add the entry under the path from the request
+ name := part.FileName()
+ if name == "" {
+ name = part.FormName()
+ }
+ path := path.Join(req.uri.Path, name)
+ entry := &api.ManifestEntry{
+ Path: path,
+ ContentType: part.Header.Get("Content-Type"),
+ Size: size,
+ ModTime: time.Now(),
}
- case r.Method == "GET" || r.Method == "HEAD":
- path = trailingSlashes.ReplaceAllString(path, "")
- if path == "" {
- http.Error(w, "Empty path not allowed", http.StatusBadRequest)
+ s.logDebug("adding %s (%d bytes) to new manifest", entry.Path, entry.Size)
+ contentKey, err := mw.AddEntry(reader, entry)
+ if err != nil {
+ return fmt.Errorf("error adding manifest entry from multipart form: %s", err)
+ }
+ s.logDebug("content for %s stored", contentKey.Log())
+ }
+}
+
+func (s *Server) handleDirectUpload(req *Request, mw *api.ManifestWriter) error {
+ key, err := mw.AddEntry(req.Body, &api.ManifestEntry{
+ Path: req.uri.Path,
+ ContentType: req.Header.Get("Content-Type"),
+ Mode: 0644,
+ Size: req.ContentLength,
+ ModTime: time.Now(),
+ })
+ if err != nil {
+ return err
+ }
+ s.logDebug("content for %s stored", key.Log())
+ return nil
+}
+
+// HandleDelete handles a DELETE request to bzz:/<manifest>/<path>, removes
+// <path> from <manifest> and returns the resulting manifest hash as a
+// text/plain response
+func (s *Server) HandleDelete(w http.ResponseWriter, r *Request) {
+ key, err := s.api.Resolve(r.uri)
+ if err != nil {
+ s.Error(w, r, fmt.Errorf("error resolving %s: %s", r.uri.Addr, err))
+ return
+ }
+
+ newKey, err := s.updateManifest(key, func(mw *api.ManifestWriter) error {
+ s.logDebug("removing %s from manifest %s", r.uri.Path, key.Log())
+ return mw.RemoveEntry(r.uri.Path)
+ })
+ if err != nil {
+ s.Error(w, r, fmt.Errorf("error updating manifest: %s", err))
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/plain")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, newKey)
+}
+
+// HandleGetRaw handles a GET request to bzzr://<key> and responds with
+// the raw content stored at the given storage key
+func (s *Server) HandleGetRaw(w http.ResponseWriter, r *Request) {
+ key, err := s.api.Resolve(r.uri)
+ if err != nil {
+ s.Error(w, r, fmt.Errorf("error resolving %s: %s", r.uri.Addr, err))
+ return
+ }
+
+ // if path is set, interpret <key> as a manifest and return the
+ // raw entry at the given path
+ if r.uri.Path != "" {
+ walker, err := s.api.NewManifestWalker(key, nil)
+ if err != nil {
+ s.BadRequest(w, r, fmt.Sprintf("%s is not a manifest", key))
return
}
- if raw {
- var reader storage.LazySectionReader
- parsedurl, _ := api.Parse(path)
-
- if parsedurl == path {
- key, err := s.api.Resolve(parsedurl, nameresolver)
- if err != nil {
- log.Error(fmt.Sprintf("%v", err))
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- reader = s.api.Retrieve(key)
- } else {
- var status int
- readertmp, _, status, err := s.api.Get(path, nameresolver)
- if err != nil {
- http.Error(w, err.Error(), status)
- return
- }
- reader = readertmp
+ var entry *api.ManifestEntry
+ walker.Walk(func(e *api.ManifestEntry) error {
+ // if the entry matches the path, set entry and stop
+ // the walk
+ if e.Path == r.uri.Path {
+ entry = e
+ // return an error to cancel the walk
+ return errors.New("found")
}
- // retrieving content
-
- quitC := make(chan bool)
- size, err := reader.Size(quitC)
- if err != nil {
- log.Debug(fmt.Sprintf("Could not determine size: %v", err.Error()))
- //An error on call to Size means we don't have the root chunk
- http.Error(w, err.Error(), http.StatusNotFound)
- return
+ // ignore non-manifest files
+ if e.ContentType != api.ManifestType {
+ return nil
}
- log.Debug(fmt.Sprintf("Reading %d bytes.", size))
- // setting mime type
- qv := requestURL.Query()
- mimeType := qv.Get("content_type")
- if mimeType == "" {
- mimeType = rawType
+ // if the manifest's path is a prefix of the
+ // requested path, recurse into it by returning
+ // nil and continuing the walk
+ if strings.HasPrefix(r.uri.Path, e.Path) {
+ return nil
}
- w.Header().Set("Content-Type", mimeType)
- http.ServeContent(w, r, uri, forever(), reader)
- log.Debug(fmt.Sprintf("Serve raw content '%s' (%d bytes) as '%s'", uri, size, mimeType))
+ return api.SkipManifest
+ })
+ if entry == nil {
+ http.NotFound(w, &r.Request)
+ return
+ }
+ key = storage.Key(common.Hex2Bytes(entry.Hash))
+ }
- // retrieve path via manifest
- } else {
- log.Debug(fmt.Sprintf("Structured GET request '%s' received.", uri))
- // add trailing slash, if missing
- if rootDocumentUri.MatchString(uri) {
- http.Redirect(w, r, path+"/", http.StatusFound)
- return
+ // check the root chunk exists by retrieving the file's size
+ reader := s.api.Retrieve(key)
+ if _, err := reader.Size(nil); err != nil {
+ s.logDebug("key not found %s: %s", key, err)
+ http.NotFound(w, &r.Request)
+ return
+ }
+
+ // allow the request to overwrite the content type using a query
+ // parameter
+ contentType := "application/octet-stream"
+ if typ := r.URL.Query().Get("content_type"); typ != "" {
+ contentType = typ
+ }
+ w.Header().Set("Content-Type", contentType)
+
+ http.ServeContent(w, &r.Request, "", time.Now(), reader)
+}
+
+// HandleGetFiles handles a GET request to bzz:/<manifest> with an Accept
+// header of "application/x-tar" and returns a tar stream of all files
+// contained in the manifest
+func (s *Server) HandleGetFiles(w http.ResponseWriter, r *Request) {
+ if r.uri.Path != "" {
+ s.BadRequest(w, r, "files request cannot contain a path")
+ return
+ }
+
+ key, err := s.api.Resolve(r.uri)
+ if err != nil {
+ s.Error(w, r, fmt.Errorf("error resolving %s: %s", r.uri.Addr, err))
+ return
+ }
+
+ walker, err := s.api.NewManifestWalker(key, nil)
+ if err != nil {
+ s.Error(w, r, err)
+ return
+ }
+
+ tw := tar.NewWriter(w)
+ defer tw.Close()
+ w.Header().Set("Content-Type", "application/x-tar")
+ w.WriteHeader(http.StatusOK)
+
+ err = walker.Walk(func(entry *api.ManifestEntry) error {
+ // ignore manifests (walk will recurse into them)
+ if entry.ContentType == api.ManifestType {
+ return nil
+ }
+
+ // retrieve the entry's key and size
+ reader := s.api.Retrieve(storage.Key(common.Hex2Bytes(entry.Hash)))
+ size, err := reader.Size(nil)
+ if err != nil {
+ return err
+ }
+
+ // write a tar header for the entry
+ hdr := &tar.Header{
+ Name: entry.Path,
+ Mode: entry.Mode,
+ Size: size,
+ ModTime: entry.ModTime,
+ Xattrs: map[string]string{
+ "user.swarm.content-type": entry.ContentType,
+ },
+ }
+ if err := tw.WriteHeader(hdr); err != nil {
+ return err
+ }
+
+ // copy the file into the tar stream
+ n, err := io.Copy(tw, io.LimitReader(reader, hdr.Size))
+ if err != nil {
+ return err
+ } else if n != size {
+ return fmt.Errorf("error writing %s: expected %d bytes but sent %d", entry.Path, size, n)
+ }
+
+ return nil
+ })
+ if err != nil {
+ s.logError("error generating tar stream: %s", err)
+ }
+}
+
+// HandleGetList handles a GET request to bzz:/<manifest>/<path> which has
+// the "list" query parameter set to "true" and returns a list of all files
+// contained in <manifest> under <path> grouped into common prefixes using
+// "/" as a delimiter
+func (s *Server) HandleGetList(w http.ResponseWriter, r *Request) {
+ // ensure the root path has a trailing slash so that relative URLs work
+ if r.uri.Path == "" && !strings.HasSuffix(r.URL.Path, "/") {
+ http.Redirect(w, &r.Request, r.URL.Path+"/?list=true", http.StatusMovedPermanently)
+ return
+ }
+
+ key, err := s.api.Resolve(r.uri)
+ if err != nil {
+ s.Error(w, r, fmt.Errorf("error resolving %s: %s", r.uri.Addr, err))
+ return
+ }
+
+ walker, err := s.api.NewManifestWalker(key, nil)
+ if err != nil {
+ s.Error(w, r, err)
+ return
+ }
+
+ var list api.ManifestList
+ prefix := r.uri.Path
+ err = walker.Walk(func(entry *api.ManifestEntry) error {
+ // handle non-manifest files
+ if entry.ContentType != api.ManifestType {
+ // ignore the file if it doesn't have the specified prefix
+ if !strings.HasPrefix(entry.Path, prefix) {
+ return nil
}
- reader, mimeType, status, err := s.api.Get(path, nameresolver)
- if err != nil {
- if _, ok := err.(api.ErrResolve); ok {
- log.Debug(fmt.Sprintf("%v", err))
- status = http.StatusBadRequest
- } else {
- log.Debug(fmt.Sprintf("error retrieving '%s': %v", uri, err))
- status = http.StatusNotFound
- }
- http.Error(w, err.Error(), status)
- return
+
+ // if the path after the prefix contains a slash, add a
+ // common prefix to the list, otherwise add the entry
+ suffix := strings.TrimPrefix(entry.Path, prefix)
+ if index := strings.Index(suffix, "/"); index > -1 {
+ list.CommonPrefixes = append(list.CommonPrefixes, prefix+suffix[:index+1])
+ return nil
}
- // set mime type and status headers
- w.Header().Set("Content-Type", mimeType)
- if status > 0 {
- w.WriteHeader(status)
- } else {
- status = 200
+ if entry.Path == "" {
+ entry.Path = "/"
}
- quitC := make(chan bool)
- size, err := reader.Size(quitC)
- if err != nil {
- log.Debug(fmt.Sprintf("Could not determine size: %v", err.Error()))
- //An error on call to Size means we don't have the root chunk
- http.Error(w, err.Error(), http.StatusNotFound)
- return
+ list.Entries = append(list.Entries, entry)
+ return nil
+ }
+
+ // if the manifest's path is a prefix of the specified prefix
+ // then just recurse into the manifest by returning nil and
+ // continuing the walk
+ if strings.HasPrefix(prefix, entry.Path) {
+ return nil
+ }
+
+ // if the manifest's path has the specified prefix, then if the
+ // path after the prefix contains a slash, add a common prefix
+ // to the list and skip the manifest, otherwise recurse into
+ // the manifest by returning nil and continuing the walk
+ if strings.HasPrefix(entry.Path, prefix) {
+ suffix := strings.TrimPrefix(entry.Path, prefix)
+ if index := strings.Index(suffix, "/"); index > -1 {
+ list.CommonPrefixes = append(list.CommonPrefixes, prefix+suffix[:index+1])
+ return api.SkipManifest
}
- log.Debug(fmt.Sprintf("Served '%s' (%d bytes) as '%s' (status code: %v)", uri, size, mimeType, status))
+ return nil
+ }
- http.ServeContent(w, r, path, forever(), reader)
+ // the manifest neither has the prefix or needs recursing in to
+ // so just skip it
+ return api.SkipManifest
+ })
+ if err != nil {
+ s.Error(w, r, err)
+ return
+ }
+ // if the client wants HTML (e.g. a browser) then render the list as a
+ // HTML index with relative URLs
+ if strings.Contains(r.Header.Get("Accept"), "text/html") {
+ w.Header().Set("Content-Type", "text/html")
+ err := htmlListTemplate.Execute(w, &htmlListData{
+ URI: r.uri,
+ List: &list,
+ })
+ if err != nil {
+ s.logError("error rendering list HTML: %s", err)
}
- default:
- http.Error(w, "Method "+r.Method+" is not supported.", http.StatusMethodNotAllowed)
+ return
}
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(&list)
}
-func (self *sequentialReader) ReadAt(target []byte, off int64) (n int, err error) {
- self.lock.Lock()
- // assert self.pos <= off
- if self.pos > off {
- log.Error(fmt.Sprintf("non-sequential read attempted from sequentialReader; %d > %d", self.pos, off))
- panic("Non-sequential read attempt")
- }
- if self.pos != off {
- log.Debug(fmt.Sprintf("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
+// HandleGetFile handles a GET request to bzz://<manifest>/<path> and responds
+// with the content of the file at <path> from the given <manifest>
+func (s *Server) HandleGetFile(w http.ResponseWriter, r *Request) {
+ key, err := s.api.Resolve(r.uri)
+ if err != nil {
+ s.Error(w, r, fmt.Errorf("error resolving %s: %s", r.uri.Addr, err))
+ return
+ }
+
+ reader, contentType, _, err := s.api.Get(key, r.uri.Path)
+ if err != nil {
+ s.Error(w, r, err)
+ return
+ }
+
+ // check the root chunk exists by retrieving the file's size
+ if _, err := reader.Size(nil); err != nil {
+ s.logDebug("file not found %s: %s", r.uri, err)
+ http.NotFound(w, &r.Request)
+ return
+ }
+
+ w.Header().Set("Content-Type", contentType)
+
+ http.ServeContent(w, &r.Request, "", time.Now(), reader)
+}
+
+func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ s.logDebug("HTTP %s request URL: '%s', Host: '%s', Path: '%s', Referer: '%s', Accept: '%s'", r.Method, r.RequestURI, r.URL.Host, r.URL.Path, r.Referer(), r.Header.Get("Accept"))
+
+ uri, err := api.Parse(strings.TrimLeft(r.URL.Path, "/"))
+ if err != nil {
+ s.logError("Invalid URI %q: %s", r.URL.Path, err)
+ http.Error(w, fmt.Sprintf("Invalid bzz URI: %s", err), http.StatusBadRequest)
+ return
+ }
+ s.logDebug("%s request received for %s", r.Method, uri)
+
+ req := &Request{Request: *r, uri: uri}
+ switch r.Method {
+ case "POST":
+ if uri.Raw() {
+ s.HandlePostRaw(w, req)
+ } else {
+ s.HandlePostFiles(w, req)
+ }
+
+ case "PUT":
+ // DEPRECATED:
+ // clients should send a POST request (the request creates a
+ // new manifest leaving the existing one intact, so it isn't
+ // strictly a traditional PUT request which replaces content
+ // at a URI, and POST is more ubiquitous)
+ if uri.Raw() {
+ http.Error(w, fmt.Sprintf("No PUT to %s allowed.", uri), http.StatusBadRequest)
return
+ } else {
+ s.HandlePostFiles(w, req)
}
- self.lock.Lock()
- }
- localPos := 0
- for localPos < len(target) {
- n, err = self.reader.Read(target[localPos:])
- localPos += n
- log.Debug(fmt.Sprintf("Read %d bytes into buffer size %d from POST, error %v.", n, len(target), err))
- if err != nil {
- log.Debug(fmt.Sprintf("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
+
+ case "DELETE":
+ if uri.Raw() {
+ http.Error(w, fmt.Sprintf("No DELETE to %s allowed.", uri), http.StatusBadRequest)
+ return
+ }
+ s.HandleDelete(w, req)
+
+ case "GET":
+ if uri.Raw() {
+ s.HandleGetRaw(w, req)
+ return
+ }
+
+ if r.Header.Get("Accept") == "application/x-tar" {
+ s.HandleGetFiles(w, req)
+ return
+ }
+
+ if r.URL.Query().Get("list") == "true" {
+ s.HandleGetList(w, req)
+ return
}
- self.pos += int64(n)
+
+ s.HandleGetFile(w, req)
+
+ default:
+ http.Error(w, "Method "+r.Method+" is not supported.", http.StatusMethodNotAllowed)
+
+ }
+}
+
+func (s *Server) updateManifest(key storage.Key, update func(mw *api.ManifestWriter) error) (storage.Key, error) {
+ mw, err := s.api.NewManifestWriter(key, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := update(mw); err != nil {
+ return nil, err
}
- wait := self.ahead[self.pos]
- if wait != nil {
- log.Debug(fmt.Sprintf("deferred read in POST at position %d triggered.", self.pos))
- delete(self.ahead, self.pos)
- close(wait)
+
+ key, err = mw.Store()
+ if err != nil {
+ return nil, err
}
- self.lock.Unlock()
- return localPos, err
+ s.logDebug("generated manifest %s", key)
+ return key, nil
+}
+
+func (s *Server) logDebug(format string, v ...interface{}) {
+ log.Debug(fmt.Sprintf("[BZZ] HTTP: "+format, v...))
+}
+
+func (s *Server) logError(format string, v ...interface{}) {
+ log.Error(fmt.Sprintf("[BZZ] HTTP: "+format, v...))
+}
+
+func (s *Server) BadRequest(w http.ResponseWriter, r *Request, reason string) {
+ s.logDebug("bad request %s %s: %s", r.Method, r.uri, reason)
+ http.Error(w, reason, http.StatusBadRequest)
+}
+
+func (s *Server) Error(w http.ResponseWriter, r *Request, err error) {
+ s.logError("error serving %s %s: %s", r.Method, r.uri, err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
}
diff --git a/swarm/api/http/server_test.go b/swarm/api/http/server_test.go
index 45a867f51..942f3ba0b 100644
--- a/swarm/api/http/server_test.go
+++ b/swarm/api/http/server_test.go
@@ -40,8 +40,8 @@ func TestBzzrGetPath(t *testing.T) {
testrequests := make(map[string]int)
testrequests["/"] = 0
- testrequests["/a"] = 1
- testrequests["/a/b"] = 2
+ testrequests["/a/"] = 1
+ testrequests["/a/b/"] = 2
testrequests["/x"] = 0
testrequests[""] = 0
diff --git a/swarm/api/http/templates.go b/swarm/api/http/templates.go
new file mode 100644
index 000000000..c3ef8c0f4
--- /dev/null
+++ b/swarm/api/http/templates.go
@@ -0,0 +1,71 @@
+// Copyright 2016 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 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 General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package http
+
+import (
+ "html/template"
+ "path"
+
+ "github.com/ethereum/go-ethereum/swarm/api"
+)
+
+type htmlListData struct {
+ URI *api.URI
+ List *api.ManifestList
+}
+
+var htmlListTemplate = template.Must(template.New("html-list").Funcs(template.FuncMap{"basename": path.Base}).Parse(`
+<!DOCTYPE html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Swarm index of {{ .URI }}</title>
+</head>
+
+<body>
+ <h1>Swarm index of {{ .URI }}</h1>
+ <hr>
+ <table>
+ <thead>
+ <tr>
+ <th>Path</th>
+ <th>Type</th>
+ <th>Size</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ {{ range .List.CommonPrefixes }}
+ <tr>
+ <td><a href="{{ basename . }}/?list=true">{{ basename . }}/</a></td>
+ <td>DIR</td>
+ <td>-</td>
+ </tr>
+ {{ end }}
+
+ {{ range .List.Entries }}
+ <tr>
+ <td><a href="{{ basename .Path }}">{{ basename .Path }}</a></td>
+ <td>{{ .ContentType }}</td>
+ <td>{{ .Size }}</td>
+ </tr>
+ {{ end }}
+ </table>
+ <hr>
+</body>
+`[1:]))
diff --git a/swarm/api/manifest.go b/swarm/api/manifest.go
index 199f259e1..6b3630fd0 100644
--- a/swarm/api/manifest.go
+++ b/swarm/api/manifest.go
@@ -19,8 +19,11 @@ package api
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
+ "io"
"sync"
+ "time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
@@ -28,25 +31,152 @@ import (
)
const (
- manifestType = "application/bzz-manifest+json"
+ ManifestType = "application/bzz-manifest+json"
)
+// Manifest represents a swarm manifest
+type Manifest struct {
+ Entries []ManifestEntry `json:"entries,omitempty"`
+}
+
+// ManifestEntry represents an entry in a swarm manifest
+type ManifestEntry struct {
+ Hash string `json:"hash,omitempty"`
+ Path string `json:"path,omitempty"`
+ ContentType string `json:"contentType,omitempty"`
+ Mode int64 `json:"mode,omitempty"`
+ Size int64 `json:"size,omitempty"`
+ ModTime time.Time `json:"mod_time,omitempty"`
+ Status int `json:"status,omitempty"`
+}
+
+// ManifestList represents the result of listing files in a manifest
+type ManifestList struct {
+ CommonPrefixes []string `json:"common_prefixes,omitempty"`
+ Entries []*ManifestEntry `json:"entries,omitempty"`
+}
+
+// NewManifest creates and stores a new, empty manifest
+func (a *Api) NewManifest() (storage.Key, error) {
+ var manifest Manifest
+ data, err := json.Marshal(&manifest)
+ if err != nil {
+ return nil, err
+ }
+ return a.Store(bytes.NewReader(data), int64(len(data)), nil)
+}
+
+// ManifestWriter is used to add and remove entries from an underlying manifest
+type ManifestWriter struct {
+ api *Api
+ trie *manifestTrie
+ quitC chan bool
+}
+
+func (a *Api) NewManifestWriter(key storage.Key, quitC chan bool) (*ManifestWriter, error) {
+ trie, err := loadManifest(a.dpa, key, quitC)
+ if err != nil {
+ return nil, fmt.Errorf("error loading manifest %s: %s", key, err)
+ }
+ return &ManifestWriter{a, trie, quitC}, nil
+}
+
+// AddEntry stores the given data and adds the resulting key to the manifest
+func (m *ManifestWriter) AddEntry(data io.Reader, e *ManifestEntry) (storage.Key, error) {
+ key, err := m.api.Store(data, e.Size, nil)
+ if err != nil {
+ return nil, err
+ }
+ entry := newManifestTrieEntry(e, nil)
+ entry.Hash = key.String()
+ m.trie.addEntry(entry, m.quitC)
+ return key, nil
+}
+
+// RemoveEntry removes the given path from the manifest
+func (m *ManifestWriter) RemoveEntry(path string) error {
+ m.trie.deleteEntry(path, m.quitC)
+ return nil
+}
+
+// Store stores the manifest, returning the resulting storage key
+func (m *ManifestWriter) Store() (storage.Key, error) {
+ return m.trie.hash, m.trie.recalcAndStore()
+}
+
+// ManifestWalker is used to recursively walk the entries in the manifest and
+// all of its submanifests
+type ManifestWalker struct {
+ api *Api
+ trie *manifestTrie
+ quitC chan bool
+}
+
+func (a *Api) NewManifestWalker(key storage.Key, quitC chan bool) (*ManifestWalker, error) {
+ trie, err := loadManifest(a.dpa, key, quitC)
+ if err != nil {
+ return nil, fmt.Errorf("error loading manifest %s: %s", key, err)
+ }
+ return &ManifestWalker{a, trie, quitC}, nil
+}
+
+// SkipManifest is used as a return value from WalkFn to indicate that the
+// manifest should be skipped
+var SkipManifest = errors.New("skip this manifest")
+
+// WalkFn is the type of function called for each entry visited by a recursive
+// manifest walk
+type WalkFn func(entry *ManifestEntry) error
+
+// Walk recursively walks the manifest calling walkFn for each entry in the
+// manifest, including submanifests
+func (m *ManifestWalker) Walk(walkFn WalkFn) error {
+ return m.walk(m.trie, "", walkFn)
+}
+
+func (m *ManifestWalker) walk(trie *manifestTrie, prefix string, walkFn WalkFn) error {
+ for _, entry := range trie.entries {
+ if entry == nil {
+ continue
+ }
+ entry.Path = prefix + entry.Path
+ err := walkFn(&entry.ManifestEntry)
+ if err != nil {
+ if entry.ContentType == ManifestType && err == SkipManifest {
+ continue
+ }
+ return err
+ }
+ if entry.ContentType != ManifestType {
+ continue
+ }
+ if err := trie.loadSubTrie(entry, nil); err != nil {
+ return err
+ }
+ if err := m.walk(entry.subtrie, entry.Path, walkFn); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
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"`
+func newManifestTrieEntry(entry *ManifestEntry, subtrie *manifestTrie) *manifestTrieEntry {
+ return &manifestTrieEntry{
+ ManifestEntry: *entry,
+ subtrie: subtrie,
+ }
}
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
+ ManifestEntry
+
+ subtrie *manifestTrie
}
func loadManifest(dpa *storage.DPA, hash storage.Key, quitC chan bool) (trie *manifestTrie, err error) { // non-recursive, subtrees are downloaded on-demand
@@ -77,7 +207,9 @@ func readManifest(manifestReader storage.LazySectionReader, hash storage.Key, dp
}
log.Trace(fmt.Sprintf("Manifest %v retrieved", hash.Log()))
- man := manifestJSON{}
+ var man struct {
+ Entries []*manifestTrieEntry `json:"entries"`
+ }
err = json.Unmarshal(manifestData, &man)
if err != nil {
err = fmt.Errorf("Manifest %v is malformed: %v", hash.Log(), err)
@@ -116,7 +248,7 @@ func (self *manifestTrie) addEntry(entry *manifestTrieEntry, quitC chan bool) {
cpl++
}
- if (oldentry.ContentType == manifestType) && (cpl == len(oldentry.Path)) {
+ if (oldentry.ContentType == ManifestType) && (cpl == len(oldentry.Path)) {
if self.loadSubTrie(oldentry, quitC) != nil {
return
}
@@ -136,12 +268,10 @@ func (self *manifestTrie) addEntry(entry *manifestTrieEntry, quitC chan bool) {
subtrie.addEntry(entry, quitC)
subtrie.addEntry(oldentry, quitC)
- self.entries[b] = &manifestTrieEntry{
+ self.entries[b] = newManifestTrieEntry(&ManifestEntry{
Path: commonPrefix,
- Hash: "",
- ContentType: manifestType,
- subtrie: subtrie,
- }
+ ContentType: ManifestType,
+ }, subtrie)
}
func (self *manifestTrie) getCountLast() (cnt int, entry *manifestTrieEntry) {
@@ -173,7 +303,7 @@ func (self *manifestTrie) deleteEntry(path string, quitC chan bool) {
}
epl := len(entry.Path)
- if (entry.ContentType == manifestType) && (len(path) >= epl) && (path[:epl] == entry.Path) {
+ if (entry.ContentType == ManifestType) && (len(path) >= epl) && (path[:epl] == entry.Path) {
if self.loadSubTrie(entry, quitC) != nil {
return
}
@@ -198,7 +328,7 @@ func (self *manifestTrie) recalcAndStore() error {
var buffer bytes.Buffer
buffer.WriteString(`{"entries":[`)
- list := &manifestJSON{}
+ list := &Manifest{}
for _, entry := range self.entries {
if entry != nil {
if entry.Hash == "" { // TODO: paralellize
@@ -208,7 +338,7 @@ func (self *manifestTrie) recalcAndStore() error {
}
entry.Hash = entry.subtrie.hash.String()
}
- list.Entries = append(list.Entries, entry)
+ list.Entries = append(list.Entries, entry.ManifestEntry)
}
}
@@ -254,7 +384,7 @@ func (self *manifestTrie) listWithPrefixInt(prefix, rp string, quitC chan bool,
entry := self.entries[i]
if entry != nil {
epl := len(entry.Path)
- if entry.ContentType == manifestType {
+ if entry.ContentType == ManifestType {
l := plen
if epl < l {
l = epl
@@ -300,7 +430,7 @@ func (self *manifestTrie) findPrefixOf(path string, quitC chan bool) (entry *man
log.Trace(fmt.Sprintf("path = %v entry.Path = %v epl = %v", path, entry.Path, epl))
if (len(path) >= epl) && (path[:epl] == entry.Path) {
log.Trace(fmt.Sprintf("entry.ContentType = %v", entry.ContentType))
- if entry.ContentType == manifestType {
+ if entry.ContentType == ManifestType {
err := self.loadSubTrie(entry, quitC)
if err != nil {
return nil, 0
diff --git a/swarm/api/storage.go b/swarm/api/storage.go
index 31b484675..7e94a9653 100644
--- a/swarm/api/storage.go
+++ b/swarm/api/storage.go
@@ -16,6 +16,8 @@
package api
+import "path"
+
type Response struct {
MimeType string
Status int
@@ -25,6 +27,8 @@ type Response struct {
}
// implements a service
+//
+// DEPRECATED: Use the HTTP API instead
type Storage struct {
api *Api
}
@@ -35,8 +39,14 @@ func NewStorage(api *Api) *Storage {
// Put uploads the content to the swarm with a simple manifest speficying
// its content type
+//
+// DEPRECATED: Use the HTTP API instead
func (self *Storage) Put(content, contentType string) (string, error) {
- return self.api.Put(content, contentType)
+ key, err := self.api.Put(content, contentType)
+ if err != nil {
+ return "", err
+ }
+ return key.String(), err
}
// Get retrieves the content from bzzpath and reads the response in full
@@ -45,8 +55,18 @@ func (self *Storage) Put(content, contentType string) (string, error) {
// 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
+//
+// DEPRECATED: Use the HTTP API instead
func (self *Storage) Get(bzzpath string) (*Response, error) {
- reader, mimeType, status, err := self.api.Get(bzzpath, true)
+ uri, err := Parse(path.Join("bzz:/", bzzpath))
+ if err != nil {
+ return nil, err
+ }
+ key, err := self.api.Resolve(uri)
+ if err != nil {
+ return nil, err
+ }
+ reader, mimeType, status, err := self.api.Get(key, uri.Path)
if err != nil {
return nil, err
}
@@ -65,6 +85,20 @@ func (self *Storage) Get(bzzpath string) (*Response, error) {
// Modify(rootHash, path, contentHash, contentType) takes th e manifest trie rooted in rootHash,
// and merge on to it. creating an entry w conentType (mime)
+//
+// DEPRECATED: Use the HTTP API instead
func (self *Storage) Modify(rootHash, path, contentHash, contentType string) (newRootHash string, err error) {
- return self.api.Modify(rootHash+"/"+path, contentHash, contentType, true)
+ uri, err := Parse("bzz:/" + rootHash)
+ if err != nil {
+ return "", err
+ }
+ key, err := self.api.Resolve(uri)
+ if err != nil {
+ return "", err
+ }
+ key, err = self.api.Modify(key, path, contentHash, contentType)
+ if err != nil {
+ return "", err
+ }
+ return key.String(), nil
}
diff --git a/swarm/api/storage_test.go b/swarm/api/storage_test.go
index 72caf52df..d260dd61d 100644
--- a/swarm/api/storage_test.go
+++ b/swarm/api/storage_test.go
@@ -36,7 +36,7 @@ func TestStoragePutGet(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}
// to check put against the Api#Get
- resp0 := testGet(t, api.api, bzzhash)
+ resp0 := testGet(t, api.api, bzzhash, "")
checkResponse(t, resp0, exp)
// check storage#Get
diff --git a/swarm/api/swarmfs_unix.go b/swarm/api/swarmfs_unix.go
index e696c6b9a..a704c1ec2 100644
--- a/swarm/api/swarmfs_unix.go
+++ b/swarm/api/swarmfs_unix.go
@@ -91,11 +91,16 @@ func (self *SwarmFS) Mount(mhash, mountpoint string) (*MountInfo, error) {
return nil, fmt.Errorf("%s is already mounted", cleanedMountPoint)
}
- key, _, path, err := self.swarmApi.parseAndResolve(mhash, true)
+ uri, err := Parse("bzz:/" + mhash)
if err != nil {
- return nil, fmt.Errorf("can't resolve %q: %v", mhash, err)
+ return nil, err
+ }
+ key, err := self.swarmApi.Resolve(uri)
+ if err != nil {
+ return nil, err
}
+ path := uri.Path
if len(path) > 0 {
path += "/"
}
diff --git a/swarm/api/uri.go b/swarm/api/uri.go
new file mode 100644
index 000000000..68ce04835
--- /dev/null
+++ b/swarm/api/uri.go
@@ -0,0 +1,96 @@
+// 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"
+ "net/url"
+ "strings"
+)
+
+// URI is a reference to content stored in swarm.
+type URI struct {
+ // Scheme has one of the following values:
+ //
+ // * bzz - an entry in a swarm manifest
+ // * bzzr - raw swarm content
+ // * bzzi - immutable URI of an entry in a swarm manifest
+ // (address is not resolved)
+ Scheme string
+
+ // Addr is either a hexadecimal storage key or it an address which
+ // resolves to a storage key
+ Addr string
+
+ // Path is the path to the content within a swarm manifest
+ Path string
+}
+
+// Parse parses rawuri into a URI struct, where rawuri is expected to have one
+// of the following formats:
+//
+// * <scheme>:/
+// * <scheme>:/<addr>
+// * <scheme>:/<addr>/<path>
+// * <scheme>://
+// * <scheme>://<addr>
+// * <scheme>://<addr>/<path>
+//
+// with scheme one of bzz, bzzr or bzzi
+func Parse(rawuri string) (*URI, error) {
+ u, err := url.Parse(rawuri)
+ if err != nil {
+ return nil, err
+ }
+ uri := &URI{Scheme: u.Scheme}
+
+ // check the scheme is valid
+ switch uri.Scheme {
+ case "bzz", "bzzi", "bzzr":
+ default:
+ return nil, fmt.Errorf("unknown scheme %q", u.Scheme)
+ }
+
+ // handle URIs like bzz://<addr>/<path> where the addr and path
+ // have already been split by url.Parse
+ if u.Host != "" {
+ uri.Addr = u.Host
+ uri.Path = strings.TrimLeft(u.Path, "/")
+ return uri, nil
+ }
+
+ // URI is like bzz:/<addr>/<path> so split the addr and path from
+ // the raw path (which will be /<addr>/<path>)
+ parts := strings.SplitN(strings.TrimLeft(u.Path, "/"), "/", 2)
+ uri.Addr = parts[0]
+ if len(parts) == 2 {
+ uri.Path = parts[1]
+ }
+ return uri, nil
+}
+
+func (u *URI) Raw() bool {
+ return u.Scheme == "bzzr"
+}
+
+func (u *URI) Immutable() bool {
+ return u.Scheme == "bzzi"
+}
+
+func (u *URI) String() string {
+ return u.Scheme + ":/" + u.Addr + "/" + u.Path
+}
diff --git a/swarm/api/uri_test.go b/swarm/api/uri_test.go
new file mode 100644
index 000000000..dcb5fbbff
--- /dev/null
+++ b/swarm/api/uri_test.go
@@ -0,0 +1,120 @@
+// 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 (
+ "reflect"
+ "testing"
+)
+
+func TestParseURI(t *testing.T) {
+ type test struct {
+ uri string
+ expectURI *URI
+ expectErr bool
+ expectRaw bool
+ expectImmutable bool
+ }
+ tests := []test{
+ {
+ uri: "",
+ expectErr: true,
+ },
+ {
+ uri: "foo",
+ expectErr: true,
+ },
+ {
+ uri: "bzz",
+ expectErr: true,
+ },
+ {
+ uri: "bzz:",
+ expectURI: &URI{Scheme: "bzz"},
+ },
+ {
+ uri: "bzzi:",
+ expectURI: &URI{Scheme: "bzzi"},
+ expectImmutable: true,
+ },
+ {
+ uri: "bzzr:",
+ expectURI: &URI{Scheme: "bzzr"},
+ expectRaw: true,
+ },
+ {
+ uri: "bzz:/",
+ expectURI: &URI{Scheme: "bzz"},
+ },
+ {
+ uri: "bzz:/abc123",
+ expectURI: &URI{Scheme: "bzz", Addr: "abc123"},
+ },
+ {
+ uri: "bzz:/abc123/path/to/entry",
+ expectURI: &URI{Scheme: "bzz", Addr: "abc123", Path: "path/to/entry"},
+ },
+ {
+ uri: "bzzr:/",
+ expectURI: &URI{Scheme: "bzzr"},
+ expectRaw: true,
+ },
+ {
+ uri: "bzzr:/abc123",
+ expectURI: &URI{Scheme: "bzzr", Addr: "abc123"},
+ expectRaw: true,
+ },
+ {
+ uri: "bzzr:/abc123/path/to/entry",
+ expectURI: &URI{Scheme: "bzzr", Addr: "abc123", Path: "path/to/entry"},
+ expectRaw: true,
+ },
+ {
+ uri: "bzz://",
+ expectURI: &URI{Scheme: "bzz"},
+ },
+ {
+ uri: "bzz://abc123",
+ expectURI: &URI{Scheme: "bzz", Addr: "abc123"},
+ },
+ {
+ uri: "bzz://abc123/path/to/entry",
+ expectURI: &URI{Scheme: "bzz", Addr: "abc123", Path: "path/to/entry"},
+ },
+ }
+ for _, x := range tests {
+ actual, err := Parse(x.uri)
+ if x.expectErr {
+ if err == nil {
+ t.Fatalf("expected %s to error", x.uri)
+ }
+ continue
+ }
+ if err != nil {
+ t.Fatalf("error parsing %s: %s", x.uri, err)
+ }
+ if !reflect.DeepEqual(actual, x.expectURI) {
+ t.Fatalf("expected %s to return %#v, got %#v", x.uri, x.expectURI, actual)
+ }
+ if actual.Raw() != x.expectRaw {
+ t.Fatalf("expected %s raw to be %t, got %t", x.uri, x.expectRaw, actual.Raw())
+ }
+ if actual.Immutable() != x.expectImmutable {
+ t.Fatalf("expected %s immutable to be %t, got %t", x.uri, x.expectImmutable, actual.Immutable())
+ }
+ }
+}
diff --git a/swarm/swarm.go b/swarm/swarm.go
index add28d205..5a7f43f8b 100644
--- a/swarm/swarm.go
+++ b/swarm/swarm.go
@@ -53,8 +53,8 @@ type Swarm struct {
privateKey *ecdsa.PrivateKey
corsString string
swapEnabled bool
- lstore *storage.LocalStore // local store, needs to store for releasing resources after node stopped
- sfs *api.SwarmFS // need this to cleanup all the active mounts on node exit
+ lstore *storage.LocalStore // local store, needs to store for releasing resources after node stopped
+ sfs *api.SwarmFS // need this to cleanup all the active mounts on node exit
}
type SwarmAPI struct {
@@ -244,13 +244,6 @@ func (self *Swarm) APIs() []rpc.API {
{
Namespace: "bzz",
Version: "0.1",
- Service: api.NewStorage(self.api),
- Public: true,
- },
-
- {
- Namespace: "bzz",
- Version: "0.1",
Service: &Info{self.config, chequebook.ContractParams},
Public: true,
},
@@ -258,11 +251,6 @@ func (self *Swarm) APIs() []rpc.API {
{
Namespace: "bzz",
Version: "0.1",
- Service: api.NewFileSystem(self.api),
- Public: false},
- {
- Namespace: "bzz",
- Version: "0.1",
Service: api.NewControl(self.api, self.hive),
Public: false,
},
@@ -278,6 +266,20 @@ func (self *Swarm) APIs() []rpc.API {
Service: self.sfs,
Public: false,
},
+ // storage APIs
+ // DEPRECATED: Use the HTTP API instead
+ {
+ Namespace: "bzz",
+ Version: "0.1",
+ Service: api.NewStorage(self.api),
+ Public: true,
+ },
+ {
+ Namespace: "bzz",
+ Version: "0.1",
+ Service: api.NewFileSystem(self.api),
+ Public: false,
+ },
// {Namespace, Version, api.NewAdmin(self), false},
}
}