Skip to content

Commit e80601b

Browse files
authored
Merge pull request #7536 from neatonk/master
[BOUNTY] Directory page UI improvements
2 parents 2e7343c + 887de89 commit e80601b

10 files changed

+125
-35
lines changed

assets/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ commit.
4545
```bash
4646
> go generate .
4747
> git add bindata.go
48+
> git add bindata_version_hash.go
4849
> go mod tidy
4950
> git commit --amend --no-edit
5051

assets/bindata.go

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/bindata_version_hash.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
package assets
33

44
const (
5-
BindataVersionHash = "60ae1cfa3f31134017737125db89f905dd3eea98"
5+
BindataVersionHash = "514e5ae28d8adb84955801b56ef47aca44bf9cc8"
66
)

core/corehttp/gateway_handler.go

+37-5
Original file line numberDiff line numberDiff line change
@@ -328,8 +328,20 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
328328
size = humanize.Bytes(uint64(s))
329329
}
330330

331+
hash := ""
332+
if r, err := i.api.ResolvePath(r.Context(), ipath.Join(resolvedPath, dirit.Name())); err == nil {
333+
// Path may not be resolved. Continue anyways.
334+
hash = r.Cid().String()
335+
}
336+
331337
// See comment above where originalUrlPath is declared.
332-
di := directoryItem{size, dirit.Name(), gopath.Join(originalUrlPath, dirit.Name())}
338+
di := directoryItem{
339+
Size: size,
340+
Name: dirit.Name(),
341+
Path: gopath.Join(originalUrlPath, dirit.Name()),
342+
Hash: hash,
343+
ShortHash: shortHash(hash),
344+
}
333345
dirListing = append(dirListing, di)
334346
}
335347
if dirit.Err() != nil {
@@ -359,14 +371,34 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
359371
}
360372
}
361373

374+
size := "?"
375+
if s, err := dir.Size(); err == nil {
376+
// Size may not be defined/supported. Continue anyways.
377+
size = humanize.Bytes(uint64(s))
378+
}
379+
362380
hash := resolvedPath.Cid().String()
363381

382+
// Storage for gateway URL to be used when linking to other rootIDs. This
383+
// will be blank unless subdomain resolution is being used for this request.
384+
var gwURL string
385+
386+
// Get gateway hostname and build gateway URL.
387+
if h, ok := r.Context().Value("gw-hostname").(string); ok {
388+
gwURL = "//" + h
389+
} else {
390+
gwURL = ""
391+
}
392+
364393
// See comment above where originalUrlPath is declared.
365394
tplData := listingTemplateData{
366-
Listing: dirListing,
367-
Path: urlPath,
368-
BackLink: backLink,
369-
Hash: hash,
395+
GatewayURL: gwURL,
396+
Listing: dirListing,
397+
Size: size,
398+
Path: urlPath,
399+
Breadcrumbs: breadcrumbs(urlPath),
400+
BackLink: backLink,
401+
Hash: hash,
370402
}
371403

372404
err = listingTemplate.Execute(w, tplData)

core/corehttp/gateway_indexPage.go

+44-5
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,61 @@ import (
77
"strings"
88

99
"github.com/ipfs/go-ipfs/assets"
10+
ipfspath "github.com/ipfs/go-path"
1011
)
1112

1213
// structs for directory listing
1314
type listingTemplateData struct {
14-
Listing []directoryItem
15-
Path string
16-
BackLink string
17-
Hash string
15+
GatewayURL string
16+
Listing []directoryItem
17+
Size string
18+
Path string
19+
Breadcrumbs []breadcrumb
20+
BackLink string
21+
Hash string
1822
}
1923

2024
type directoryItem struct {
21-
Size string
25+
Size string
26+
Name string
27+
Path string
28+
Hash string
29+
ShortHash string
30+
}
31+
32+
type breadcrumb struct {
2233
Name string
2334
Path string
2435
}
2536

37+
func breadcrumbs(urlPath string) []breadcrumb {
38+
var ret []breadcrumb
39+
40+
p, err := ipfspath.ParsePath(urlPath)
41+
if err != nil {
42+
// No breadcrumbs, fallback to bare Path in template
43+
return ret
44+
}
45+
46+
segs := p.Segments()
47+
for i, seg := range segs {
48+
if i == 0 {
49+
ret = append(ret, breadcrumb{Name: seg})
50+
} else {
51+
ret = append(ret, breadcrumb{
52+
Name: seg,
53+
Path: "/" + strings.Join(segs[0:i+1], "/"),
54+
})
55+
}
56+
}
57+
58+
return ret
59+
}
60+
61+
func shortHash(hash string) string {
62+
return (hash[0:4] + "\u2026" + hash[len(hash)-4:])
63+
}
64+
2665
var listingTemplate *template.Template
2766

2867
func init() {

core/corehttp/gateway_test.go

+11-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io/ioutil"
77
"net/http"
88
"net/http/httptest"
9+
"regexp"
910
"strings"
1011
"testing"
1112
"time"
@@ -154,6 +155,11 @@ func newTestServerAndNode(t *testing.T, ns mockNamesys) (*httptest.Server, iface
154155
return ts, api, n.Context()
155156
}
156157

158+
func matchPathOrBreadcrumbs(s string, expected string) bool {
159+
matched, _ := regexp.MatchString("Index of\n[\t ]*"+regexp.QuoteMeta(expected), s)
160+
return matched
161+
}
162+
157163
func TestGatewayGet(t *testing.T) {
158164
ns := mockNamesys{}
159165
ts, api, ctx := newTestServerAndNode(t, ns)
@@ -442,7 +448,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
442448
s := string(body)
443449
t.Logf("body: %s\n", string(body))
444450

445-
if !strings.Contains(s, "Index of /ipns/example.net/foo? #<'/") {
451+
if !matchPathOrBreadcrumbs(s, "/ipns/<a href=\"/ipns/example.net\">example.net</a>/<a href=\"/ipns/example.net/foo%3F%20%23%3C%27\">foo? #&lt;&#39;</a>") {
446452
t.Fatalf("expected a path in directory listing")
447453
}
448454
if !strings.Contains(s, "<a href=\"/foo%3F%20%23%3C%27/./..\">") {
@@ -475,7 +481,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
475481
s = string(body)
476482
t.Logf("body: %s\n", string(body))
477483

478-
if !strings.Contains(s, "Index of /") {
484+
if !matchPathOrBreadcrumbs(s, "/") {
479485
t.Fatalf("expected a path in directory listing")
480486
}
481487
if !strings.Contains(s, "<a href=\"/\">") {
@@ -508,7 +514,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
508514
s = string(body)
509515
t.Logf("body: %s\n", string(body))
510516

511-
if !strings.Contains(s, "Index of /ipns/example.net/foo? #&lt;&#39;/bar/") {
517+
if !matchPathOrBreadcrumbs(s, "/ipns/<a href=\"/ipns/example.net\">example.net</a>/<a href=\"/ipns/example.net/foo%3F%20%23%3C%27\">foo? #&lt;&#39;</a>/<a href=\"/ipns/example.net/foo%3F%20%23%3C%27/bar\">bar</a>") {
512518
t.Fatalf("expected a path in directory listing")
513519
}
514520
if !strings.Contains(s, "<a href=\"/foo%3F%20%23%3C%27/bar/./..\">") {
@@ -542,7 +548,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
542548
s = string(body)
543549
t.Logf("body: %s\n", string(body))
544550

545-
if !strings.Contains(s, "Index of /ipns/example.net") {
551+
if !matchPathOrBreadcrumbs(s, "/ipns/<a href=\"/ipns/example.net\">example.net</a>") {
546552
t.Fatalf("expected a path in directory listing")
547553
}
548554
if !strings.Contains(s, "<a href=\"/good-prefix/\">") {
@@ -584,7 +590,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
584590
s = string(body)
585591
t.Logf("body: %s\n", string(body))
586592

587-
if !strings.Contains(s, "Index of /") {
593+
if !matchPathOrBreadcrumbs(s, "/") {
588594
t.Fatalf("expected a path in directory listing")
589595
}
590596
if !strings.Contains(s, "<a href=\"/\">") {

core/corehttp/hostname.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ func HostnameOption() ServeOption {
143143
if gw, hostname, ns, rootID, ok := knownSubdomainDetails(host, knownGateways); ok {
144144
// Looks like we're using a known gateway in subdomain mode.
145145

146+
// Add gateway hostname context for linking to other root ids.
147+
// Example: localhost/ipfs/{cid}
148+
ctx := context.WithValue(r.Context(), "gw-hostname", hostname)
149+
146150
// Assemble original path prefix.
147151
pathPrefix := "/" + ns + "/" + rootID
148152

@@ -197,7 +201,7 @@ func HostnameOption() ServeOption {
197201
r.URL.Path = pathPrefix + r.URL.Path
198202

199203
// Serve path request
200-
childMux.ServeHTTP(w, r)
204+
childMux.ServeHTTP(w, r.WithContext(ctx))
201205
return
202206
}
203207
// We don't have a known gateway. Fallback on DNSLink lookup

test/sharness/t0111-gateway-writeable.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ test_expect_success "HTTP GET empty directory" '
6363
URL="http://127.0.0.1:$port/ipfs/$HASH_EMPTY_DIR/" &&
6464
echo "GET $URL" &&
6565
curl -so outfile "$URL" 2>curl_getEmpty.out &&
66-
grep "Index of /ipfs/$HASH_EMPTY_DIR/" outfile
66+
cat outfile | tr -s "\n" " " | grep "Index of /ipfs/<a href=\"/ipfs/$HASH_EMPTY_DIR\">$HASH_EMPTY_DIR</a>"
6767
'
6868

6969
test_expect_success "HTTP PUT file to construct a hierarchy" '

test/sharness/t0114-gateway-subdomains.sh

+22-14
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,11 @@ test_expect_success "Add test text file" '
101101
echo CIDv0to1=${CIDv0to1}
102102
'
103103

104+
# Directory tree crafted to test for edge cases like "/ipfs/ipfs/ipns/bar"
104105
test_expect_success "Add the test directory" '
105-
mkdir -p testdirlisting/subdir1/subdir2 &&
106+
mkdir -p testdirlisting/ipfs/ipns &&
106107
echo "hello" > testdirlisting/hello &&
107-
echo "subdir2-bar" > testdirlisting/subdir1/subdir2/bar &&
108+
echo "text-file-content" > testdirlisting/ipfs/ipns/bar &&
108109
mkdir -p testdirlisting/api &&
109110
mkdir -p testdirlisting/ipfs &&
110111
echo "I am a txt file" > testdirlisting/api/file.txt &&
@@ -269,18 +270,18 @@ DIR_HOSTNAME="${DIR_CID}.ipfs.localhost:$GWAY_PORT"
269270
test_expect_success "valid file and subdirectory paths in directory listing at {cid}.ipfs.localhost" '
270271
curl -s --resolve $DIR_HOSTNAME:127.0.0.1 "http://$DIR_HOSTNAME" > list_response &&
271272
test_should_contain "<a href=\"/hello\">hello</a>" list_response &&
272-
test_should_contain "<a href=\"/subdir1\">subdir1</a>" list_response
273+
test_should_contain "<a href=\"/ipfs\">ipfs</a>" list_response
273274
'
274275

275276
test_expect_success "valid parent directory path in directory listing at {cid}.ipfs.localhost/sub/dir" '
276-
curl -s --resolve $DIR_HOSTNAME:127.0.0.1 "http://$DIR_HOSTNAME/subdir1/subdir2/" > list_response &&
277-
test_should_contain "<a href=\"/subdir1/subdir2/./..\">..</a>" list_response &&
278-
test_should_contain "<a href=\"/subdir1/subdir2/bar\">bar</a>" list_response
277+
curl -s --resolve $DIR_HOSTNAME:127.0.0.1 "http://$DIR_HOSTNAME/ipfs/ipns/" > list_response &&
278+
test_should_contain "<a href=\"/ipfs/ipns/./..\">..</a>" list_response &&
279+
test_should_contain "<a href=\"/ipfs/ipns/bar\">bar</a>" list_response
279280
'
280281

281282
test_expect_success "request for deep path resource at {cid}.ipfs.localhost/sub/dir/file" '
282-
curl -s --resolve $DIR_HOSTNAME:127.0.0.1 "http://$DIR_HOSTNAME/subdir1/subdir2/bar" > list_response &&
283-
test_should_contain "subdir2-bar" list_response
283+
curl -s --resolve $DIR_HOSTNAME:127.0.0.1 "http://$DIR_HOSTNAME/ipfs/ipns/bar" > list_response &&
284+
test_should_contain "text-file-content" list_response
284285
'
285286

286287

@@ -422,18 +423,25 @@ DIR_FQDN="${DIR_CID}.ipfs.example.com"
422423
test_expect_success "valid file and directory paths in directory listing at {cid}.ipfs.example.com" '
423424
curl -s -H "Host: $DIR_FQDN" http://127.0.0.1:$GWAY_PORT > list_response &&
424425
test_should_contain "<a href=\"/hello\">hello</a>" list_response &&
425-
test_should_contain "<a href=\"/subdir1\">subdir1</a>" list_response
426+
test_should_contain "<a href=\"/ipfs\">ipfs</a>" list_response
426427
'
427428

428429
test_expect_success "valid parent directory path in directory listing at {cid}.ipfs.example.com/sub/dir" '
429-
curl -s -H "Host: $DIR_FQDN" http://127.0.0.1:$GWAY_PORT/subdir1/subdir2/ > list_response &&
430-
test_should_contain "<a href=\"/subdir1/subdir2/./..\">..</a>" list_response &&
431-
test_should_contain "<a href=\"/subdir1/subdir2/bar\">bar</a>" list_response
430+
curl -s -H "Host: $DIR_FQDN" http://127.0.0.1:$GWAY_PORT/ipfs/ipns/ > list_response &&
431+
test_should_contain "<a href=\"/ipfs/ipns/./..\">..</a>" list_response &&
432+
test_should_contain "<a href=\"/ipfs/ipns/bar\">bar</a>" list_response
433+
'
434+
435+
# Note we test for sneaky subdir names {cid}.ipfs.example.com/ipfs/ipns/ :^)
436+
test_expect_success "valid breadcrumb links in the header of directory listing at {cid}.ipfs.example.com/sub/dir" '
437+
curl -s -H "Host: $DIR_FQDN" http://127.0.0.1:$GWAY_PORT/ipfs/ipns/ > list_response &&
438+
test_should_contain "Index of" list_response &&
439+
test_should_contain "/ipfs/<a href=\"//example.com/ipfs/${DIR_CID}\">${DIR_CID}</a>/<a href=\"//example.com/ipfs/${DIR_CID}/ipfs\">ipfs</a>/<a href=\"//example.com/ipfs/${DIR_CID}/ipfs/ipns\">ipns</a>" list_response
432440
'
433441

434442
test_expect_success "request for deep path resource {cid}.ipfs.example.com/sub/dir/file" '
435-
curl -s -H "Host: $DIR_FQDN" http://127.0.0.1:$GWAY_PORT/subdir1/subdir2/bar > list_response &&
436-
test_should_contain "subdir2-bar" list_response
443+
curl -s -H "Host: $DIR_FQDN" http://127.0.0.1:$GWAY_PORT/ipfs/ipns/bar > list_response &&
444+
test_should_contain "text-file-content" list_response
437445
'
438446

439447
# *.ipns.example.com

0 commit comments

Comments
 (0)