提交 4016d35a 作者: Juan Batiz-Benet

Merge pull request #617 from mildred/http-rest

HTTP: add handlers to allow object creation and modification
......@@ -17,6 +17,7 @@ import (
)
var (
writable = flag.Bool("writable", false, "enable writing objects (with POST, PUT and DELETE)")
refreshAssetsInterval = flag.Duration("refresh-assets-interval", 30*time.Second, "refresh assets")
garbageCollectInterval = flag.Duration("gc-interval", 24*time.Hour, "frequency of repo garbage collection")
assetsPath = flag.String("assets-path", "", "if provided, periodically adds contents of path to IPFS")
......@@ -93,7 +94,7 @@ func run() error {
}
opts := []corehttp.ServeOption{
corehttp.GatewayOption,
corehttp.GatewayOption(*writable),
}
if err := corehttp.ListenAndServe(node, *host, opts...); err != nil {
return err
......
......@@ -2,6 +2,7 @@ package main
import (
"bytes"
"fmt"
ma "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr"
cmds "github.com/jbenet/go-ipfs/commands"
......@@ -15,6 +16,7 @@ import (
const (
initOptionKwd = "init"
mountKwd = "mount"
writableKwd = "writable"
ipfsMountKwd = "mount-ipfs"
ipnsMountKwd = "mount-ipns"
// apiAddrKwd = "address-api"
......@@ -36,6 +38,7 @@ the daemon.
Options: []cmds.Option{
cmds.BoolOption(initOptionKwd, "Initialize IPFS with default settings if not already initialized"),
cmds.BoolOption(mountKwd, "Mounts IPFS to the filesystem"),
cmds.BoolOption(writableKwd, "Enable writing objects (with POST, PUT and DELETE)"),
cmds.StringOption(ipfsMountKwd, "Path to the mountpoint for IPFS (if using --mount)"),
cmds.StringOption(ipnsMountKwd, "Path to the mountpoint for IPNS (if using --mount)"),
......@@ -161,9 +164,22 @@ func daemonFunc(req cmds.Request, res cmds.Response) {
rootRedirect = corehttp.RedirectOption("", cfg.Gateway.RootRedirect)
}
writable, writableOptionFound, err := req.Option(writableKwd).Bool()
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
if !writableOptionFound {
writable = cfg.Gateway.Writable
}
if writable {
fmt.Printf("IPNS gateway mounted read-write\n")
}
if gatewayMaddr != nil {
go func() {
var opts = []corehttp.ServeOption{corehttp.GatewayOption}
var opts = []corehttp.ServeOption{corehttp.GatewayOption(writable)}
if rootRedirect != nil {
opts = append(opts, rootRedirect)
}
......@@ -177,7 +193,7 @@ func daemonFunc(req cmds.Request, res cmds.Response) {
var opts = []corehttp.ServeOption{
corehttp.CommandsOption(*req.Context()),
corehttp.WebUIOption,
corehttp.GatewayOption,
corehttp.GatewayOption(true),
}
if rootRedirect != nil {
opts = append(opts, rootRedirect)
......
......@@ -80,7 +80,7 @@ func run(ipfsPath, watchPath string) error {
if *http {
addr := "/ip4/127.0.0.1/tcp/5001"
var opts = []corehttp.ServeOption{
corehttp.GatewayOption,
corehttp.GatewayOption(true),
corehttp.WebUIOption,
corehttp.CommandsOption(cmdCtx(node, ipfsPath)),
}
......
......@@ -57,6 +57,9 @@ Set the value of the 'datastore.path' key:
cmds.StringArg("key", true, false, "The key of the config entry (e.g. \"Addresses.API\")"),
cmds.StringArg("value", false, false, "The value to set the config entry to"),
},
Options: []cmds.Option{
cmds.BoolOption("bool", "Set a boolean value"),
},
Run: func(req cmds.Request, res cmds.Response) {
args := req.Arguments()
key := args[0]
......@@ -72,7 +75,11 @@ Set the value of the 'datastore.path' key:
var output *ConfigField
if len(args) == 2 {
value := args[1]
output, err = setConfig(r, key, value)
if isbool, _, _ := req.Option("bool").Bool(); isbool {
output, err = setConfig(r, key, value == "true")
} else {
output, err = setConfig(r, key, value)
}
} else {
output, err = getConfig(r, key)
}
......@@ -208,7 +215,7 @@ func getConfig(r repo.Repo, key string) (*ConfigField, error) {
}, nil
}
func setConfig(r repo.Repo, key, value string) (*ConfigField, error) {
func setConfig(r repo.Repo, key string, value interface{}) (*ConfigField, error) {
err := r.SetConfigKey(key, value)
if err != nil {
return nil, fmt.Errorf("Failed to set config value: %s", err)
......
......@@ -6,12 +6,14 @@ import (
core "github.com/jbenet/go-ipfs/core"
)
func GatewayOption(n *core.IpfsNode, mux *http.ServeMux) error {
gateway, err := newGatewayHandler(n)
if err != nil {
return err
func GatewayOption(writable bool) ServeOption {
return func(n *core.IpfsNode, mux *http.ServeMux) error {
gateway, err := newGatewayHandler(n, writable)
if err != nil {
return err
}
mux.Handle("/ipfs/", gateway)
mux.Handle("/ipns/", gateway)
return nil
}
mux.Handle("/ipfs/", gateway)
mux.Handle("/ipns/", gateway)
return nil
}
package corehttp
import (
"fmt"
"html/template"
"io"
"net/http"
......@@ -17,6 +18,7 @@ import (
dag "github.com/jbenet/go-ipfs/merkledag"
path "github.com/jbenet/go-ipfs/path"
"github.com/jbenet/go-ipfs/routing"
ufs "github.com/jbenet/go-ipfs/unixfs"
uio "github.com/jbenet/go-ipfs/unixfs/io"
u "github.com/jbenet/go-ipfs/util"
)
......@@ -46,13 +48,15 @@ type directoryItem struct {
// gatewayHandler is a HTTP handler that serves IPFS objects (accessible by default at /ipfs/<path>)
// (it serves requests like GET /ipfs/QmVRzPKPzNtSrEzBFm2UZfxmPAgnaLke4DMcerbsGGSaFe/link)
type gatewayHandler struct {
node *core.IpfsNode
dirList *template.Template
node *core.IpfsNode
dirList *template.Template
writable bool
}
func newGatewayHandler(node *core.IpfsNode) (*gatewayHandler, error) {
func newGatewayHandler(node *core.IpfsNode, writable bool) (*gatewayHandler, error) {
i := &gatewayHandler{
node: node,
node: node,
writable: writable,
}
err := i.loadTemplate()
if err != nil {
......@@ -71,7 +75,7 @@ func (i *gatewayHandler) loadTemplate() error {
return nil
}
func (i *gatewayHandler) ResolvePath(ctx context.Context, p string) (*dag.Node, string, error) {
func (i *gatewayHandler) resolveNamePath(ctx context.Context, p string) (string, error) {
p = gopath.Clean(p)
if strings.HasPrefix(p, IpnsPathPrefix) {
......@@ -79,7 +83,7 @@ func (i *gatewayHandler) ResolvePath(ctx context.Context, p string) (*dag.Node,
hash := elements[0]
k, err := i.node.Namesys.Resolve(ctx, hash)
if err != nil {
return nil, "", err
return "", err
}
elements[0] = k.Pretty()
......@@ -88,6 +92,14 @@ func (i *gatewayHandler) ResolvePath(ctx context.Context, p string) (*dag.Node,
if !strings.HasPrefix(p, IpfsPathPrefix) {
p = gopath.Join(IpfsPathPrefix, p)
}
return p, nil
}
func (i *gatewayHandler) ResolvePath(ctx context.Context, p string) (*dag.Node, string, error) {
p, err := i.resolveNamePath(ctx, p)
if err != nil {
return nil, "", err
}
node, err := i.node.Resolver.ResolvePath(path.Path(p))
if err != nil {
......@@ -101,6 +113,10 @@ func (i *gatewayHandler) NewDagFromReader(r io.Reader) (*dag.Node, error) {
r, i.node.DAG, i.node.Pinning.GetManual(), chunk.DefaultSplitter)
}
func NewDagEmptyDir() *dag.Node {
return &dag.Node{Data: ufs.FolderPBData()}
}
func (i *gatewayHandler) AddNodeToDAG(nd *dag.Node) (u.Key, error) {
return i.node.DAG.Add(nd)
}
......@@ -110,6 +126,39 @@ func (i *gatewayHandler) NewDagReader(nd *dag.Node) (uio.ReadSeekCloser, error)
}
func (i *gatewayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if i.writable && r.Method == "POST" {
i.postHandler(w, r)
return
}
if i.writable && r.Method == "PUT" {
i.putHandler(w, r)
return
}
if i.writable && r.Method == "DELETE" {
i.deleteHandler(w, r)
return
}
if r.Method == "GET" {
i.getHandler(w, r)
return
}
errmsg := "Method " + r.Method + " not allowed: "
if !i.writable {
w.WriteHeader(http.StatusMethodNotAllowed)
errmsg = errmsg + "read only access"
} else {
w.WriteHeader(http.StatusBadRequest)
errmsg = errmsg + "bad request for " + r.URL.Path
}
w.Write([]byte(errmsg))
log.Error(errmsg)
}
func (i *gatewayHandler) getHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithCancel(i.node.Context())
defer cancel()
......@@ -209,23 +258,200 @@ func (i *gatewayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (i *gatewayHandler) postHandler(w http.ResponseWriter, r *http.Request) {
nd, err := i.NewDagFromReader(r.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Error(err)
w.Write([]byte(err.Error()))
internalWebError(w, err)
return
}
k, err := i.AddNodeToDAG(nd)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Error(err)
internalWebError(w, err)
return
}
h := mh.Multihash(k).B58String()
w.Header().Set("IPFS-Hash", h)
http.Redirect(w, r, IpfsPathPrefix+h, http.StatusCreated)
}
func (i *gatewayHandler) putEmptyDirHandler(w http.ResponseWriter, r *http.Request) {
newnode := NewDagEmptyDir()
key, err := i.node.DAG.Add(newnode)
if err != nil {
webError(w, "Could not recursively add new node", err, http.StatusInternalServerError)
return
}
w.Header().Set("IPFS-Hash", key.String())
http.Redirect(w, r, IpfsPathPrefix+key.String()+"/", http.StatusCreated)
}
func (i *gatewayHandler) putHandler(w http.ResponseWriter, r *http.Request) {
urlPath := r.URL.Path
pathext := urlPath[5:]
var err error
if urlPath == IpfsPathPrefix + "QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn/" {
i.putEmptyDirHandler(w, r)
return
}
var newnode *dag.Node
if pathext[len(pathext)-1] == '/' {
newnode = NewDagEmptyDir()
} else {
newnode, err = i.NewDagFromReader(r.Body)
if err != nil {
webError(w, "Could not create DAG from request", err, http.StatusInternalServerError)
return
}
}
ctx, cancel := context.WithCancel(i.node.Context())
defer cancel()
ipfspath, err := i.resolveNamePath(ctx, urlPath)
if err != nil {
// FIXME HTTP error code
webError(w, "Could not resolve name", err, http.StatusInternalServerError)
return
}
h, components, err := path.SplitAbsPath(path.Path(ipfspath))
if err != nil {
webError(w, "Could not split path", err, http.StatusInternalServerError)
return
}
if len(components) < 1 {
err = fmt.Errorf("Cannot override existing object")
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
log.Error("%s", err)
return
}
rootnd, err := i.node.Resolver.DAG.Get(u.Key(h))
if err != nil {
webError(w, "Could not resolve root object", err, http.StatusBadRequest)
return
}
// resolving path components into merkledag nodes. if a component does not
// resolve, create empty directories (which will be linked and populated below.)
path_nodes, err := i.node.Resolver.ResolveLinks(rootnd, components[:len(components)-1])
if _, ok := err.(path.ErrNoLink); ok {
// Create empty directories, links will be made further down the code
for len(path_nodes) < len(components) {
path_nodes = append(path_nodes, NewDagEmptyDir())
}
} else if err != nil {
webError(w, "Could not resolve parent object", err, http.StatusBadRequest)
return
}
for i := len(path_nodes) - 1; i >= 0; i-- {
newnode, err = path_nodes[i].UpdateNodeLink(components[i], newnode)
if err != nil {
webError(w, "Could not update node links", err, http.StatusInternalServerError)
return
}
}
err = i.node.DAG.AddRecursive(newnode)
if err != nil {
webError(w, "Could not add recursively new node", err, http.StatusInternalServerError)
return
}
// Redirect to new path
key, err := newnode.Key()
if err != nil {
webError(w, "Could not get key of new node", err, http.StatusInternalServerError)
return
}
w.Header().Set("IPFS-Hash", key.String())
http.Redirect(w, r, IpfsPathPrefix+key.String()+"/"+strings.Join(components, "/"), http.StatusCreated)
}
func (i *gatewayHandler) deleteHandler(w http.ResponseWriter, r *http.Request) {
urlPath := r.URL.Path
ctx, cancel := context.WithCancel(i.node.Context())
defer cancel()
ipfspath, err := i.resolveNamePath(ctx, urlPath)
if err != nil {
// FIXME HTTP error code
webError(w, "Could not resolve name", err, http.StatusInternalServerError)
return
}
//TODO: return json representation of list instead
w.WriteHeader(http.StatusCreated)
w.Write([]byte(mh.Multihash(k).B58String()))
h, components, err := path.SplitAbsPath(path.Path(ipfspath))
if err != nil {
webError(w, "Could not split path", err, http.StatusInternalServerError)
return
}
rootnd, err := i.node.Resolver.DAG.Get(u.Key(h))
if err != nil {
webError(w, "Could not resolve root object", err, http.StatusBadRequest)
return
}
path_nodes, err := i.node.Resolver.ResolveLinks(rootnd, components[:len(components)-1])
if err != nil {
webError(w, "Could not resolve parent object", err, http.StatusBadRequest)
return
}
err = path_nodes[len(path_nodes)-1].RemoveNodeLink(components[len(components)-1])
if err != nil {
webError(w, "Could not delete link", err, http.StatusBadRequest)
return
}
newnode := path_nodes[len(path_nodes)-1]
for i := len(path_nodes) - 2; i >= 0; i-- {
newnode, err = path_nodes[i].UpdateNodeLink(components[i], newnode)
if err != nil {
webError(w, "Could not update node links", err, http.StatusInternalServerError)
return
}
}
err = i.node.DAG.AddRecursive(newnode)
if err != nil {
webError(w, "Could not add recursively new node", err, http.StatusInternalServerError)
return
}
// Redirect to new path
key, err := newnode.Key()
if err != nil {
webError(w, "Could not get key of new node", err, http.StatusInternalServerError)
return
}
w.Header().Set("IPFS-Hash", key.String())
http.Redirect(w, r, IpfsPathPrefix+key.String()+"/"+strings.Join(components[:len(components)-1], "/"), http.StatusCreated)
}
func webError(w http.ResponseWriter, message string, err error, defaultCode int) {
if _, ok := err.(path.ErrNoLink); ok {
webErrorWithCode(w, message, err, http.StatusNotFound)
} else if err == routing.ErrNotFound {
webErrorWithCode(w, message, err, http.StatusNotFound)
} else if err == context.DeadlineExceeded {
webErrorWithCode(w, message, err, http.StatusRequestTimeout)
} else {
webErrorWithCode(w, message, err, defaultCode)
}
}
func webErrorWithCode(w http.ResponseWriter, message string, err error, code int) {
w.WriteHeader(code)
log.Errorf("%s: %s", message, err)
w.Write([]byte(message + ": " + err.Error()))
}
// return a 500 error and log
......
......@@ -292,13 +292,13 @@ func (s *Node) Attr() fuse.Attr {
// Lookup performs a lookup under this node.
func (s *Node) Lookup(name string, intr fs.Intr) (fs.Node, fuse.Error) {
log.Debugf("ipns: node[%s] Lookup '%s'", s.name, name)
nd, err := s.Ipfs.Resolver.ResolveLinks(s.Nd, []string{name})
nodes, err := s.Ipfs.Resolver.ResolveLinks(s.Nd, []string{name})
if err != nil {
// todo: make this error more versatile.
return nil, fuse.ENOENT
}
return s.makeChild(name, nd), nil
return s.makeChild(name, nodes[len(nodes)-1]), nil
}
func (n *Node) makeChild(name string, node *mdag.Node) *Node {
......@@ -650,12 +650,11 @@ func (n *Node) Rename(req *fuse.RenameRequest, newDir fs.Node, intr fs.Intr) fus
// Updates the child of this node, specified by name to the given newnode
func (n *Node) update(name string, newnode *mdag.Node) error {
log.Debugf("update '%s' in '%s'", name, n.name)
nnode := n.Nd.Copy()
err := nnode.RemoveNodeLink(name)
nnode, err := n.Nd.UpdateNodeLink(name, newnode)
if err != nil {
return err
}
nnode.AddNodeLink(name, newnode)
if n.parent != nil {
err := n.parent.update(n.name, nnode)
......
......@@ -118,13 +118,13 @@ func (s *Node) Attr() fuse.Attr {
// Lookup performs a lookup under this node.
func (s *Node) Lookup(name string, intr fs.Intr) (fs.Node, fuse.Error) {
log.Debugf("Lookup '%s'", name)
nd, err := s.Ipfs.Resolver.ResolveLinks(s.Nd, []string{name})
nodes, err := s.Ipfs.Resolver.ResolveLinks(s.Nd, []string{name})
if err != nil {
// todo: make this error more versatile.
return nil, fuse.ENOENT
}
return &Node{Ipfs: s.Ipfs, Nd: nd}, nil
return &Node{Ipfs: s.Ipfs, Nd: nodes[len(nodes)-1]}, nil
}
// ReadDir reads the link structure as directory entries
......
......@@ -134,6 +134,16 @@ func (n *Node) Copy() *Node {
return nnode
}
// UpdateNodeLink return a copy of the node with the link name set to point to
// that. If a link of the same name existed, it is removed.
func (n *Node) UpdateNodeLink(name string, that *Node) (*Node, error) {
newnode := n.Copy()
err := newnode.RemoveNodeLink(name)
err = nil // ignore error
err = newnode.AddNodeLink(name, that)
return newnode, err
}
// Size returns the total size of the data addressed by node,
// including the total sizes of references.
func (n *Node) Size() (uint64, error) {
......
......@@ -11,16 +11,26 @@ import (
var log = u.Logger("path")
// ErrNoLink is returned when a link is not found in a path
type ErrNoLink struct {
name string
node mh.Multihash
}
func (e ErrNoLink) Error() string {
return fmt.Sprintf("no link named %q under %s", e.name, e.node.B58String())
}
// Resolver provides path resolution to IPFS
// It has a pointer to a DAGService, which is uses to resolve nodes.
type Resolver struct {
DAG merkledag.DAGService
}
// ResolvePath fetches the node for given path. It uses the first
// path component as a hash (key) of the first node, then resolves
// all other components walking the links, with ResolveLinks.
func (s *Resolver) ResolvePath(fpath Path) (*merkledag.Node, error) {
// SplitAbsPath clean up and split fpath. It extracts the first component (which
// must be a Multihash) and return it separately.
func SplitAbsPath(fpath Path) (mh.Multihash, []string, error) {
log.Debugf("Resolve: '%s'", fpath)
parts := fpath.Segments()
......@@ -30,13 +40,36 @@ func (s *Resolver) ResolvePath(fpath Path) (*merkledag.Node, error) {
// if nothing, bail.
if len(parts) == 0 {
return nil, fmt.Errorf("ipfs path must contain at least one component")
return nil, nil, fmt.Errorf("ipfs path must contain at least one component")
}
// first element in the path is a b58 hash (for now)
h, err := mh.FromB58String(parts[0])
if err != nil {
log.Debug("given path element is not a base58 string.\n")
return nil, nil, err
}
return h, parts[1:], nil
}
// ResolvePath fetches the node for given path. It returns the last item
// returned by ResolvePathComponents.
func (s *Resolver) ResolvePath(fpath Path) (*merkledag.Node, error) {
nodes, err := s.ResolvePathComponents(fpath)
if err != nil || nodes == nil {
return nil, err
} else {
return nodes[len(nodes)-1], err
}
}
// ResolvePathComponents fetches the nodes for each segment of the given path.
// It uses the first path component as a hash (key) of the first node, then
// resolves all other components walking the links, with ResolveLinks.
func (s *Resolver) ResolvePathComponents(fpath Path) ([]*merkledag.Node, error) {
h, parts, err := SplitAbsPath(fpath)
if err != nil {
return nil, err
}
......@@ -46,19 +79,22 @@ func (s *Resolver) ResolvePath(fpath Path) (*merkledag.Node, error) {
return nil, err
}
return s.ResolveLinks(nd, parts[1:])
return s.ResolveLinks(nd, parts)
}
// ResolveLinks iteratively resolves names by walking the link hierarchy.
// Every node is fetched from the DAGService, resolving the next name.
// Returns the last node found.
// Returns the list of nodes forming the path, starting with ndd. This list is
// guaranteed never to be empty.
//
// ResolveLinks(nd, []string{"foo", "bar", "baz"})
// would retrieve "baz" in ("bar" in ("foo" in nd.Links).Links).Links
func (s *Resolver) ResolveLinks(ndd *merkledag.Node, names []string) (
nd *merkledag.Node, err error) {
result []*merkledag.Node, err error) {
nd = ndd // dup arg workaround
result = make([]*merkledag.Node, 0, len(names)+1)
result = append(result, ndd)
nd := ndd // dup arg workaround
// for each of the path components
for _, name := range names {
......@@ -75,21 +111,22 @@ func (s *Resolver) ResolveLinks(ndd *merkledag.Node, names []string) (
}
if next == "" {
h1, _ := nd.Multihash()
h2 := h1.B58String()
return nil, fmt.Errorf("no link named %q under %s", name, h2)
n, _ := nd.Multihash()
return result, ErrNoLink{name: name, node: n}
}
if nlink.Node == nil {
// fetch object for link and assign to nd
nd, err = s.DAG.Get(next)
if err != nil {
return nd, err
return append(result, nd), err
}
nlink.Node = nd
} else {
nd = nlink.Node
}
result = append(result, nlink.Node)
}
return
}
......@@ -3,4 +3,5 @@ package config
// Gateway contains options for the HTTP gateway server.
type Gateway struct {
RootRedirect string
Writable bool
}
......@@ -54,6 +54,7 @@ func Init(out io.Writer, nBitsForKeypair int) (*Config, error) {
Gateway: Gateway{
RootRedirect: "",
Writable: false,
},
}
......
......@@ -64,6 +64,16 @@ test_wait_output_n_lines_60_sec() {
test_cmp "expected_waitn" "actual_waitn"
}
test_wait_open_tcp_port_10_sec() {
for i in 1 2 3 4 5 6 7 8 9 10; do
if [ $(ss -lt "sport == :$1" | wc -l) -gt 1 ]; then
return 0
fi
sleep 1
done
return 1
}
test_init_ipfs() {
test_expect_success "ipfs init succeeds" '
......
#!/bin/sh
#
# Copyright (c) 2014 Christian Couder
# MIT Licensed; see the LICENSE file in this repository.
#
test_description="Test HTTP Gateway"
exec 3>&1 4>&2
. lib/test-lib.sh
test_expect_success "Configure http gateway" '
export IPFS_PATH="$PWD/.go-ipfs";
if ! [ -e ../ipfs-path ]; then
IPFS_PATH="$PWD/../ipfs-path" ipfs init;
fi;
cp -R ../ipfs-path "$IPFS_PATH" &&
ipfs config Addresses.Gateway /ip4/0.0.0.0/tcp/5002 &&
ipfs config -bool Gateway.Writable true
'
test_expect_success "ipfs daemon --init launches and listen to TCP port 5002" '
export IPFS_PATH="$PWD/.go-ipfs" &&
ipfs daemon 2>&1 >actual_init &
IPFS_PID=$(ps | grep ipfs | awk "{print \$1}");
test_wait_open_tcp_port_10_sec 5002
'
test_expect_success "HTTP gateway gives access to sample file" '
curl -s -o welcome http://localhost:5002/ipfs/QmTTFXiXoixwT53tcGPu419udsHEHYu6AHrQC8HAKdJYaZ &&
grep "Hello and Welcome to IPFS!" welcome
'
test_expect_success "HTTP POST file gives Hash" '
echo "$RANDOM" >infile
curl -svX POST --data-binary @infile http://localhost:5002/ipfs/ 2>curl.out &&
grep "HTTP/1.1 201 Created" curl.out
'
test_expect_success "We can HTTP GET file just created" '
path=$(grep Location curl.out | cut -d" " -f3- | tr -d "\r")
curl -so outfile http://localhost:5002$path &&
diff -u infile outfile
'
test_expect_success "HTTP PUT empty directory" '
echo "PUT http://localhost:5002/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn/" &&
curl -svX PUT http://localhost:5002/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn/ 2>curl.out &&
cat curl.out &&
grep "Ipfs-Hash: QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn" curl.out &&
grep "Location: /ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn/" curl.out &&
grep "HTTP/1.1 201 Created" curl.out
'
test_expect_success "HTTP GET empty directory" '
echo "GET http://localhost:5002/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn/" &&
curl -so outfile http://localhost:5002/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn/ 2>curl.out &&
grep "Index of /ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn/" outfile
'
test_expect_success "HTTP PUT file to construct a hierarchy" '
echo "$RANDOM" >infile
echo "PUT http://localhost:5002/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn/test.txt" &&
curl -svX PUT --data-binary @infile http://localhost:5002/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn/test.txt 2>curl.out &&
grep "HTTP/1.1 201 Created" curl.out &&
grep Location curl.out
'
test_expect_success "We can HTTP GET file just created" '
path=$(grep Location curl.out | cut -d" " -f3- | tr -d "\r");
echo "$path" = "${path%/test.txt}/test.txt";
[ "$path" = "${path%/test.txt}/test.txt" ] &&
echo "GET http://localhost:5002$path" &&
curl -so outfile http://localhost:5002$path &&
diff -u infile outfile
'
test_expect_success "HTTP PUT file to append to existing hierarchy" '
echo "$RANDOM" >infile2;
echo "PUT http://localhost:5002${path%/test.txt}/test/test.txt" &&
curl -svX PUT --data-binary @infile2 http://localhost:5002${path%/test.txt}/test/test.txt 2>curl.out &&
grep "HTTP/1.1 201 Created" curl.out &&
grep Location curl.out
'
test_expect_success "We can HTTP GET file just created" '
path=$(grep Location curl.out | cut -d" " -f3- | tr -d "\r");
[ "$path" = "${path%/test/test.txt}/test/test.txt" ] &&
echo "GET http://localhost:5002$path" &&
curl -so outfile2 http://localhost:5002$path &&
diff -u infile2 outfile2 &&
echo "GET http://localhost:5002${path%/test/test.txt}/test.txt" &&
curl -so outfile http://localhost:5002${path%/test/test.txt}/test.txt &&
diff -u infile outfile
'
test_expect_success "daemon is still running" '
echo IPFS_PID=$IPFS_PID;
kill -15 $IPFS_PID
'
test_expect_success "'ipfs daemon' can be killed" '
test_kill_repeat_10_sec $IPFS_PID
'
test_done
......@@ -144,3 +144,19 @@ func (m MultiErr) Error() string {
}
return s
}
func Partition(subject string, sep string) (string, string, string) {
if i := strings.Index(subject, sep); i != -1 {
return subject[:i], subject[i : i+len(sep)], subject[i+len(sep):]
} else {
return subject, "", ""
}
}
func RPartition(subject string, sep string) (string, string, string) {
if i := strings.LastIndex(subject, sep); i != -1 {
return subject[:i], subject[i : i+len(sep)], subject[i+len(sep):]
} else {
return subject, "", ""
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论