fix and improve the writable gateway
1. Fix handling of PUT. The simple implementation was the correct implementation, I have no idea what was going on here. 2. Use MFS everywhere to reduce code duplication and add support for sharded directories. 3. _Correctly_ block IPNS. 4. Remove the dependency on `core.IpfsNode`. 5. Remove support for putting empty directories with a well-known CID. It was useless as directories are automatically created.
Steven Allen committed
Jul 25, 2019 at 17:30 UTC
521a29956b35ceeba775d6f699edc02c29c1857e
5 files changed
+155
-167
core/corehttp/gateway.go
+1
-1
@@ -87,7 +87,7 @@ func GatewayOption(writable bool, paths ...string) ServeOption {
87
"X-Stream-Output",
88
}, headers[ACEHeadersName]...))
89
90
- gateway := newGatewayHandler(n, GatewayConfig{
90
+ gateway := newGatewayHandler(GatewayConfig{
91
Headers: headers,
92
Writable: writable,
93
PathPrefixes: cfg.Gateway.PathPrefixes,
core/corehttp/gateway_handler.go
+129
-152
@@ -2,30 +2,23 @@ package corehttp
2
3
import (
4
"context"
5
- "errors"
5
"fmt"
6
"io"
7
"net/http"
8
"net/url"
9
+ "os"
10
gopath "path"
11
"runtime/debug"
12
"strings"
13
"time"
14
15
- "github.com/ipfs/go-ipfs/core"
16
- "github.com/ipfs/go-ipfs/dagutils"
17
- "github.com/ipfs/go-ipfs/namesys/resolve"
18
-
15
"github.com/dustin/go-humanize"
16
"github.com/ipfs/go-cid"
21
- chunker "github.com/ipfs/go-ipfs-chunker"
17
files "github.com/ipfs/go-ipfs-files"
23
- ipld "github.com/ipfs/go-ipld-format"
18
dag "github.com/ipfs/go-merkledag"
19
+ "github.com/ipfs/go-mfs"
20
"github.com/ipfs/go-path"
21
"github.com/ipfs/go-path/resolver"
27
- ft "github.com/ipfs/go-unixfs"
28
- "github.com/ipfs/go-unixfs/importer"
22
coreiface "github.com/ipfs/interface-go-ipfs-core"
23
ipath "github.com/ipfs/interface-go-ipfs-core/path"
24
routing "github.com/libp2p/go-libp2p-core/routing"
@@ -40,27 +33,36 @@ const (
33
// gatewayHandler is a HTTP handler that serves IPFS objects (accessible by default at /ipfs/<path>)
34
// (it serves requests like GET /ipfs/QmVRzPKPzNtSrEzBFm2UZfxmPAgnaLke4DMcerbsGGSaFe/link)
35
type gatewayHandler struct {
43
- node *core.IpfsNode
36
config GatewayConfig
37
api coreiface.CoreAPI
38
}
39
48
-func newGatewayHandler(n *core.IpfsNode, c GatewayConfig, api coreiface.CoreAPI) *gatewayHandler {
40
+func newGatewayHandler(c GatewayConfig, api coreiface.CoreAPI) *gatewayHandler {
41
i := &gatewayHandler{
50
- node: n,
42
config: c,
43
api: api,
44
}
45
return i
46
}
47
57
-// TODO(cryptix): find these helpers somewhere else
58
-func (i *gatewayHandler) newDagFromReader(r io.Reader) (ipld.Node, error) {
59
- // TODO(cryptix): change and remove this helper once PR1136 is merged
60
- // return ufs.AddFromReader(i.node, r.Body)
61
- return importer.BuildDagFromReader(
62
- i.node.DAG,
63
- chunker.DefaultSplitter(r))
48
+func parseIpfsPath(p string) (cid.Cid, string, error) {
49
+ rootPath, err := path.ParsePath(p)
50
+ if err != nil {
51
+ return cid.Cid{}, "", err
52
+ }
53
+
54
+ // Check the path.
55
+ rsegs := rootPath.Segments()
56
+ if rsegs[0] != "ipfs" {
57
+ return cid.Cid{}, "", fmt.Errorf("WritableGateway: only ipfs paths supported")
58
+ }
59
+
60
+ rootCid, err := cid.Decode(rsegs[1])
61
+ if err != nil {
62
+ return cid.Cid{}, "", err
63
+ }
64
+
65
+ return rootCid, path.Join(rsegs[2:]), nil
66
}
67
68
func (i *gatewayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -160,10 +162,12 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
162
163
// Resolve path to the final DAG node for the ETag
164
resolvedPath, err := i.api.ResolvePath(r.Context(), parsedPath)
163
- if err == coreiface.ErrOffline && !i.node.IsOnline {
165
+ switch err {
166
+ case nil:
167
+ case coreiface.ErrOffline:
168
webError(w, "ipfs resolve -r "+escapedURLPath, err, http.StatusServiceUnavailable)
169
return
166
- } else if err != nil {
170
+ default:
171
webError(w, "ipfs resolve -r "+escapedURLPath, err, http.StatusNotFound)
172
return
173
}
@@ -395,102 +399,91 @@ func (i *gatewayHandler) postHandler(w http.ResponseWriter, r *http.Request) {
399
}
400
401
func (i *gatewayHandler) putHandler(w http.ResponseWriter, r *http.Request) {
398
- rootPath, err := path.ParsePath(r.URL.Path)
402
+ ctx := r.Context()
403
+ ds := i.api.Dag()
404
+
405
+ // Parse the path
406
+ rootCid, newPath, err := parseIpfsPath(r.URL.Path)
407
if err != nil {
400
- webError(w, "putHandler: IPFS path not valid", err, http.StatusBadRequest)
408
+ webError(w, "WritableGateway: failed to parse the path", err, http.StatusBadRequest)
409
return
410
}
403
-
404
- rsegs := rootPath.Segments()
405
- if rsegs[0] == ipnsPathPrefix {
406
- webError(w, "putHandler: updating named entries not supported", errors.New("WritableGateway: ipns put not supported"), http.StatusBadRequest)
411
+ if newPath == "" || newPath == "/" {
412
+ http.Error(w, "WritableGateway: empty path", http.StatusBadRequest)
413
return
414
}
415
+ newDirectory, newFileName := gopath.Split(newPath)
416
410
- var newnode ipld.Node
411
- if rsegs[len(rsegs)-1] == "QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn" {
412
- newnode = ft.EmptyDirNode()
413
- } else {
414
- putNode, err := i.newDagFromReader(r.Body)
415
- if err != nil {
416
- webError(w, "putHandler: Could not create DAG from request", err, http.StatusInternalServerError)
417
- return
418
- }
419
- newnode = putNode
420
- }
417
+ // Resolve the old root.
418
422
- var newPath string
423
- if len(rsegs) > 1 {
424
- newPath = path.Join(rsegs[2:])
419
+ rnode, err := ds.Get(ctx, rootCid)
420
+ if err != nil {
421
+ webError(w, "WritableGateway: Could not create DAG from request", err, http.StatusInternalServerError)
422
+ return
423
}
424
427
- var newcid cid.Cid
428
- rnode, err := resolve.Resolve(r.Context(), i.node.Namesys, i.node.Resolver, rootPath)
429
- switch ev := err.(type) {
430
- case resolver.ErrNoLink:
431
- // ev.Node < node where resolve failed
432
- // ev.Name < new link
433
- // but we need to patch from the root
434
- c, err := cid.Decode(rsegs[1])
435
- if err != nil {
436
- webError(w, "putHandler: bad input path", err, http.StatusBadRequest)
437
- return
438
- }
439
-
440
- rnode, err := i.node.DAG.Get(r.Context(), c)
441
- if err != nil {
442
- webError(w, "putHandler: Could not create DAG from request", err, http.StatusInternalServerError)
443
- return
444
- }
445
-
446
- pbnd, ok := rnode.(*dag.ProtoNode)
447
- if !ok {
448
- webError(w, "Cannot read non protobuf nodes through gateway", dag.ErrNotProtobuf, http.StatusBadRequest)
449
- return
450
- }
451
-
452
- e := dagutils.NewDagEditor(pbnd, i.node.DAG)
453
- err = e.InsertNodeAtPath(r.Context(), newPath, newnode, ft.EmptyDirNode)
454
- if err != nil {
455
- webError(w, "putHandler: InsertNodeAtPath failed", err, http.StatusInternalServerError)
456
- return
457
- }
458
-
459
- nnode, err := e.Finalize(r.Context(), i.node.DAG)
460
- if err != nil {
461
- webError(w, "putHandler: could not get node", err, http.StatusInternalServerError)
462
- return
463
- }
425
+ pbnd, ok := rnode.(*dag.ProtoNode)
426
+ if !ok {
427
+ webError(w, "Cannot read non protobuf nodes through gateway", dag.ErrNotProtobuf, http.StatusBadRequest)
428
+ return
429
+ }
430
465
- newcid = nnode.Cid()
431
+ // Create the new file.
432
+ newFilePath, err := i.api.Unixfs().Add(ctx, files.NewReaderFile(r.Body))
433
+ if err != nil {
434
+ webError(w, "WritableGateway: could not create DAG from request", err, http.StatusInternalServerError)
435
+ return
436
+ }
437
467
- case nil:
468
- pbnd, ok := rnode.(*dag.ProtoNode)
469
- if !ok {
470
- webError(w, "Cannot read non protobuf nodes through gateway", dag.ErrNotProtobuf, http.StatusBadRequest)
471
- return
472
- }
438
+ newFile, err := ds.Get(ctx, newFilePath.Cid())
439
+ if err != nil {
440
+ webError(w, "WritableGateway: failed to resolve new file", err, http.StatusInternalServerError)
441
+ return
442
+ }
443
474
- pbnewnode, ok := newnode.(*dag.ProtoNode)
475
- if !ok {
476
- webError(w, "Cannot read non protobuf nodes through gateway", dag.ErrNotProtobuf, http.StatusBadRequest)
477
- return
478
- }
444
+ // Patch the new file into the old root.
445
480
- // object set-data case
481
- pbnd.SetData(pbnewnode.Data())
446
+ root, err := mfs.NewRoot(ctx, ds, pbnd, nil)
447
+ if err != nil {
448
+ webError(w, "WritableGateway: failed to create MFS root", err, http.StatusBadRequest)
449
+ return
450
+ }
451
483
- newcid = pbnd.Cid()
484
- err = i.node.DAG.Add(r.Context(), pbnd)
452
+ if newDirectory != "" {
453
+ err := mfs.Mkdir(root, newDirectory, mfs.MkdirOpts{Mkparents: true, Flush: false})
454
if err != nil {
486
- nnk := newnode.Cid()
487
- webError(w, fmt.Sprintf("putHandler: Could not add newnode(%q) to root(%q)", nnk.String(), newcid.String()), err, http.StatusInternalServerError)
455
+ webError(w, "WritableGateway: failed to create MFS directory", err, http.StatusInternalServerError)
456
return
457
}
458
+ }
459
+ dirNode, err := mfs.Lookup(root, newDirectory)
460
+ if err != nil {
461
+ webError(w, "WritableGateway: failed to lookup directory", err, http.StatusInternalServerError)
462
+ return
463
+ }
464
+ dir, ok := dirNode.(*mfs.Directory)
465
+ if !ok {
466
+ http.Error(w, "WritableGateway: target directory is not a directory", http.StatusBadRequest)
467
+ return
468
+ }
469
+ err = dir.Unlink(newFileName)
470
+ switch err {
471
+ case os.ErrNotExist, nil:
472
default:
491
- webError(w, "could not resolve root DAG", ev, http.StatusInternalServerError)
473
+ webError(w, "WritableGateway: failed to replace existing file", err, http.StatusBadRequest)
474
return
475
}
476
+ err = dir.AddChild(newFileName, newFile)
477
+ if err != nil {
478
+ webError(w, "WritableGateway: failed to link file into directory", err, http.StatusInternalServerError)
479
+ return
480
+ }
481
+ nnode, err := root.GetDirectory().GetNode()
482
+ if err != nil {
483
+ webError(w, "WritableGateway: failed to finalize", err, http.StatusInternalServerError)
484
+ return
485
+ }
486
+ newcid := nnode.Cid()
487
488
i.addUserHeaders(w) // ok, _now_ write user's headers.
489
w.Header().Set("IPFS-Hash", newcid.String())
@@ -498,91 +491,75 @@ func (i *gatewayHandler) putHandler(w http.ResponseWriter, r *http.Request) {
491
}
492
493
func (i *gatewayHandler) deleteHandler(w http.ResponseWriter, r *http.Request) {
501
- urlPath := r.URL.Path
494
+ ctx := r.Context()
495
+
496
+ // parse the path
497
503
- p, err := path.ParsePath(urlPath)
498
+ rootCid, newPath, err := parseIpfsPath(r.URL.Path)
499
if err != nil {
505
- webError(w, "failed to parse path", err, http.StatusBadRequest)
500
+ webError(w, "WritableGateway: failed to parse the path", err, http.StatusBadRequest)
501
return
502
}
508
-
509
- c, components, err := path.SplitAbsPath(p)
510
- if err != nil {
511
- webError(w, "Could not split path", err, http.StatusInternalServerError)
503
+ if newPath == "" || newPath == "/" {
504
+ http.Error(w, "WritableGateway: empty path", http.StatusBadRequest)
505
return
506
}
507
+ directory, filename := gopath.Split(newPath)
508
+
509
+ // lookup the root
510
515
- pathNodes, err := i.resolvePathComponents(r.Context(), c, components)
511
+ rootNodeIPLD, err := i.api.Dag().Get(ctx, rootCid)
512
if err != nil {
517
- webError(w, "Could not resolve path components", err, http.StatusBadRequest)
513
+ webError(w, "WritableGateway: failed to resolve root CID", err, http.StatusInternalServerError)
514
return
515
}
520
-
521
- pbnd, ok := pathNodes[len(pathNodes)-1].(*dag.ProtoNode)
516
+ rootNode, ok := rootNodeIPLD.(*dag.ProtoNode)
517
if !ok {
523
- webError(w, "Cannot read non protobuf nodes through gateway", dag.ErrNotProtobuf, http.StatusBadRequest)
518
+ http.Error(w, "WritableGateway: empty path", http.StatusInternalServerError)
519
return
520
}
521
527
- // TODO(cyrptix): assumes len(pathNodes) > 1 - not found is an error above?
528
- err = pbnd.RemoveNodeLink(components[len(components)-1])
522
+ // construct the mfs root
523
+
524
+ root, err := mfs.NewRoot(ctx, i.api.Dag(), rootNode, nil)
525
if err != nil {
530
- webError(w, "Could not delete link", err, http.StatusBadRequest)
526
+ webError(w, "WritableGateway: failed to construct the MFS root", err, http.StatusBadRequest)
527
return
528
}
529
534
- var newnode *dag.ProtoNode = pbnd
535
- for j := len(pathNodes) - 2; j >= 0; j-- {
536
- if err := i.node.DAG.Add(r.Context(), newnode); err != nil {
537
- webError(w, "Could not add node", err, http.StatusInternalServerError)
538
- return
539
- }
530
+ // lookup the parent directory
531
541
- pathpb, ok := pathNodes[j].(*dag.ProtoNode)
542
- if !ok {
543
- webError(w, "Cannot read non protobuf nodes through gateway", dag.ErrNotProtobuf, http.StatusBadRequest)
544
- return
545
- }
546
-
547
- newnode, err = pathpb.UpdateNodeLink(components[j], newnode)
548
- if err != nil {
549
- webError(w, "Could not update node links", err, http.StatusInternalServerError)
550
- return
551
- }
532
+ parentNode, err := mfs.Lookup(root, directory)
533
+ if err != nil {
534
+ webError(w, "WritableGateway: failed to look up parent", err, http.StatusInternalServerError)
535
+ return
536
}
537
554
- if err := i.node.DAG.Add(r.Context(), newnode); err != nil {
555
- webError(w, "Could not add root node", err, http.StatusInternalServerError)
538
+ parent, ok := parentNode.(*mfs.Directory)
539
+ if !ok {
540
+ http.Error(w, "WritableGateway: parent is not a directory", http.StatusInternalServerError)
541
return
542
}
543
559
- // Redirect to new path
560
- ncid := newnode.Cid()
544
+ // delete the file
545
562
- i.addUserHeaders(w) // ok, _now_ write user's headers.
563
- w.Header().Set("IPFS-Hash", ncid.String())
564
- http.Redirect(w, r, gopath.Join(ipfsPathPrefix+ncid.String(), path.Join(components[:len(components)-1])), http.StatusCreated)
565
-}
566
-
567
-func (i *gatewayHandler) resolvePathComponents(
568
- ctx context.Context,
569
- c cid.Cid,
570
- components []string,
571
-) ([]ipld.Node, error) {
572
- tctx, cancel := context.WithTimeout(ctx, time.Minute)
573
- defer cancel()
574
-
575
- rootnd, err := i.node.Resolver.DAG.Get(tctx, c)
576
- if err != nil {
577
- return nil, fmt.Errorf("Could not resolve root object: %s", err)
546
+ switch parent.Unlink(filename) {
547
+ case nil, os.ErrNotExist:
548
+ default:
549
+ webError(w, "WritableGateway: failed to remove file", err, http.StatusInternalServerError)
550
+ return
551
}
552
580
- pathNodes, err := i.node.Resolver.ResolveLinks(tctx, rootnd, components[:len(components)-1])
553
+ nnode, err := root.GetDirectory().GetNode()
554
if err != nil {
582
- return nil, fmt.Errorf("Could not resolve parent object: %s", err)
555
+ webError(w, "WritableGateway: failed to finalize", err, http.StatusInternalServerError)
556
}
557
+ ncid := nnode.Cid()
558
585
- return pathNodes, nil
559
+ i.addUserHeaders(w) // ok, _now_ write user's headers.
560
+ w.Header().Set("IPFS-Hash", ncid.String())
561
+ // note: StatusCreated is technically correct here as we created a new resource.
562
+ http.Redirect(w, r, gopath.Join(ipfsPathPrefix+ncid.String(), directory), http.StatusCreated)
563
}
564
565
func (i *gatewayHandler) addUserHeaders(w http.ResponseWriter) {
go.mod
+2
-2
@@ -45,12 +45,12 @@ require (
45
github.com/ipfs/go-ipld-git v0.0.2
46
github.com/ipfs/go-ipns v0.0.1
47
github.com/ipfs/go-log v0.0.1
48
- github.com/ipfs/go-merkledag v0.2.0
48
+ github.com/ipfs/go-merkledag v0.2.3
49
github.com/ipfs/go-metrics-interface v0.0.1
50
github.com/ipfs/go-metrics-prometheus v0.0.2
51
github.com/ipfs/go-mfs v0.1.0
52
github.com/ipfs/go-path v0.0.7
53
- github.com/ipfs/go-unixfs v0.2.0
53
+ github.com/ipfs/go-unixfs v0.2.1
54
github.com/ipfs/go-verifcid v0.0.1
55
github.com/ipfs/hang-fds v0.0.1
56
github.com/ipfs/interface-go-ipfs-core v0.1.0
go.sum
+4
-2
@@ -301,6 +301,8 @@ github.com/ipfs/go-merkledag v0.1.0 h1:CAEXjRFEDPvealQj3TgEjV1IJckwjvmxAqtq5QSXJ
301
github.com/ipfs/go-merkledag v0.1.0/go.mod h1:SQiXrtSts3KGNmgOzMICy5c0POOpUNQLvB3ClKnBAlk=
302
github.com/ipfs/go-merkledag v0.2.0 h1:EAjIQCgZ6/DnOAlKY3+59j72FD9BsYtNaCRSmN0xIbU=
303
github.com/ipfs/go-merkledag v0.2.0/go.mod h1:SQiXrtSts3KGNmgOzMICy5c0POOpUNQLvB3ClKnBAlk=
304
+github.com/ipfs/go-merkledag v0.2.3 h1:aMdkK9G1hEeNvn3VXfiEMLY0iJnbiQQUHnM0HFJREsE=
305
+github.com/ipfs/go-merkledag v0.2.3/go.mod h1:SQiXrtSts3KGNmgOzMICy5c0POOpUNQLvB3ClKnBAlk=
306
github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg=
307
github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY=
308
github.com/ipfs/go-metrics-prometheus v0.0.2 h1:9i2iljLg12S78OhC6UAiXi176xvQGiZaGVF1CUVdE+s=
@@ -320,8 +322,8 @@ github.com/ipfs/go-todocounter v0.0.1/go.mod h1:l5aErvQc8qKE2r7NDMjmq5UNAvuZy0rC
322
github.com/ipfs/go-unixfs v0.0.4/go.mod h1:eIo/p9ADu/MFOuyxzwU+Th8D6xoxU//r590vUpWyfz8=
323
github.com/ipfs/go-unixfs v0.0.8 h1:AHahQ+gdNZd9BhKVLf8XP1EWeKa78eTzYgCygp7N/Pg=
324
github.com/ipfs/go-unixfs v0.0.8/go.mod h1:cK2vDJ7L4YnWB6oLefpVNesgx0x/zPTRVDw6B4Y+03U=
323
-github.com/ipfs/go-unixfs v0.2.0 h1:mfdI8rgsEifWfhLECrH2WphHvslNoPbdvlmsJ05Fu0M=
324
-github.com/ipfs/go-unixfs v0.2.0/go.mod h1:sy/j20FKUxdUYOl6GZb3wsQ593C2xk5r1i8U7W+5iio=
325
+github.com/ipfs/go-unixfs v0.2.1 h1:g51t9ODICFZ3F51FPivm8dE7NzYcdAQNUL9wGP5AYa0=
326
+github.com/ipfs/go-unixfs v0.2.1/go.mod h1:IwAAgul1UQIcNZzKPYZWOCijryFBeCV79cNubPzol+k=
327
github.com/ipfs/go-verifcid v0.0.1 h1:m2HI7zIuR5TFyQ1b79Da5N9dnnCP1vcu2QqawmWlK2E=
328
github.com/ipfs/go-verifcid v0.0.1/go.mod h1:5Hrva5KBeIog4A+UpqlaIU+DEstipcJYQQZc0g37pY0=
329
github.com/ipfs/hang-fds v0.0.1 h1:KGAxiGtJPT3THVRNT6yxgpdFPeX4ZemUjENOt6NlOn4=
test/sharness/t0111-gateway-writeable.sh
+19
-10
@@ -54,17 +54,8 @@ test_expect_success "We can HTTP GET file just created" '
54
test_cmp infile outfile
55
'
56
57
-test_expect_success "HTTP PUT empty directory" '
58
- URL="http://localhost:$port/ipfs/$HASH_EMPTY_DIR/" &&
59
- echo "PUT $URL" &&
60
- curl -svX PUT "$URL" 2>curl_putEmpty.out &&
61
- cat curl_putEmpty.out &&
62
- grep "Ipfs-Hash: $HASH_EMPTY_DIR" curl_putEmpty.out &&
63
- grep "Location: /ipfs/$HASH_EMPTY_DIR" curl_putEmpty.out &&
64
- grep "HTTP/1.1 201 Created" curl_putEmpty.out
65
-'
66
-
57
test_expect_success "HTTP GET empty directory" '
58
+ URL="http://localhost:$port/ipfs/$HASH_EMPTY_DIR/" &&
59
echo "GET $URL" &&
60
curl -so outfile "$URL" 2>curl_getEmpty.out &&
61
grep "Index of /ipfs/$HASH_EMPTY_DIR/" outfile
@@ -105,6 +96,24 @@ test_expect_success "We can HTTP GET file just updated" '
96
test_cmp infile2 outfile2
97
'
98
99
+test_expect_success "HTTP PUT to replace a directory" '
100
+ echo "$RANDOM" >infile3 &&
101
+ URL="http://localhost:$port/ipfs/$HASH/test" &&
102
+ echo "PUT $URL" &&
103
+ curl -svX PUT --data-binary @infile3 "$URL" 2>curl_putOverDirectory.out &&
104
+ grep "HTTP/1.1 201 Created" curl_putOverDirectory.out &&
105
+ LOCATION=$(grep Location curl_putOverDirectory.out) &&
106
+ HASH=$(expr "$LOCATION" : "< Location: /ipfs/\(.*\)/test")
107
+'
108
+
109
+test_expect_success "We can HTTP GET file just put over a directory" '
110
+ URL="http://localhost:$port/ipfs/$HASH/test" &&
111
+ echo "GET $URL" &&
112
+ curl -svo outfile3 "$URL" 2>curl_getOverDirectory.out &&
113
+ test_cmp infile3 outfile3
114
+'
115
+
116
+
117
test_kill_ipfs_daemon
118
119
test_done