Skip to content

Commit

Permalink
wip: visualize dag-cbor and dag-json
Browse files Browse the repository at this point in the history
  • Loading branch information
hacdias committed May 26, 2023
1 parent 3203992 commit bd0d412
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 11 deletions.
1 change: 1 addition & 0 deletions gateway/assets/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ type DagTemplateData struct {
CID string
CodecName string
CodecHex string
Node *Node
}

type ErrorTemplateData struct {
Expand Down
22 changes: 22 additions & 0 deletions gateway/assets/dag.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,28 @@
<li><a href="?format=dag-cbor" rel="nofollow">Valid DAG-CBOR</a> (specs at <a href="https://ipld.io/specs/codecs/dag-cbor/spec/" target="_blank" rel="noopener noreferrer">IPLD</a> and <a href="https://www.iana.org/assignments/media-types/application/vnd.ipld.dag-cbor" target="_blank" rel="noopener noreferrer">IANA</a>)</li>
</ul>
</div>
{{ with .Node }}
<div class="table-responsive">
{{ template "node" . }}
</div>
{{ end }}
</main>
</body>
</html>

{{ define "node" }}
<table>
{{ if .HasChildren }}
{{ range $k, $v := .Children }}
<tr><td>{{ $k }}</td>
<td>{{ template "node" $v }}</td></tr>
{{ end }}
{{ else }}
<tr>
{{ with .Cid }}<a class="ipfs-hash">{{ end }}
{{- .String -}}
{{ with .Cid }}</a>{{ end }}
</tr>
{{ end }}
</table>
{{ end }}
144 changes: 144 additions & 0 deletions gateway/assets/node.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package assets

import (
"fmt"
"html/template"

"github.com/ipld/go-ipld-prime/datamodel"
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
)

type Node struct {
node datamodel.Node
path string
}

func NewNode(node datamodel.Node, path string) *Node {
return &Node{node, path}
}

func (n *Node) IsValid() bool {
return n.node.Kind() != datamodel.Kind_Invalid
}

func (n *Node) HasChildren() bool {
return n.node.Kind() == datamodel.Kind_List ||
n.node.Kind() == datamodel.Kind_Map
}

func (n *Node) Cid() string {
if n.node.Kind() != datamodel.Kind_Link {
return ""
}

lnk, err := n.node.AsLink()
if err != nil {
return ""
}

cl, isCid := lnk.(cidlink.Link)
if !isCid {
return ""
}

return cl.Cid.String()
}

func (n *Node) String() (string, error) {
switch n.node.Kind() {
case datamodel.Kind_Invalid:
return "INVALID", nil
case datamodel.Kind_Map:
return "MAP", nil
case datamodel.Kind_List:
return "LIST", nil
case datamodel.Kind_Null:
return "NULL", nil
case datamodel.Kind_Bool:
v, err := n.node.AsBool()
if err != nil {
return "", err
}
return fmt.Sprintf("%t", v), nil
case datamodel.Kind_Int:
v, err := n.node.AsInt()
if err != nil {
return "", err
}
return fmt.Sprintf("%d", v), nil
case datamodel.Kind_Float:
v, err := n.node.AsFloat()
if err != nil {
return "", err
}
return fmt.Sprintf("%f", v), nil
case datamodel.Kind_String:
v, err := n.node.AsString()
if err != nil {
return "", err
}
return template.HTMLEscapeString(v), nil
case datamodel.Kind_Bytes:
return "BYTES", nil
case datamodel.Kind_Link:
lnk, err := n.node.AsLink()
if err != nil {
return "", err
}
return lnk.String(), nil
default:
return "UNKNOWN", nil
}
}

func (n *Node) Children() (map[string]*Node, error) {
m := map[string]*Node{}

if n.node.Kind() == datamodel.Kind_List {
it := n.node.ListIterator()
for !it.Done() {
k, v, err := it.Next()
if err != nil {
return nil, err
}

key := fmt.Sprint(k)

m[key] = NewNode(v, n.path+ipldToPathSeg(key)+"/")
}
}

if n.node.Kind() == datamodel.Kind_Map {
it := n.node.MapIterator()

for !it.Done() {
k, v, err := it.Next()
if err != nil {
return nil, err
}

key, err := NewNode(k, n.path).String()
if err != nil {
return nil, err
}

m[key] = NewNode(v, n.path+ipldToPathSeg(key)+"/")
}
}

return m, nil
}

func ipldToPathSeg(is string) string {
if is == "" { // invalid, but don't panic
return ""
}
if is[0] != '~' {
return "~" + is
}
if len(is) < 2 { // invalid but panicking is bad
return "~~i"
}

return "~~i" + is[1:]
}
4 changes: 4 additions & 0 deletions gateway/assets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ table {
width: 100%;
}

tr {
background-color: white;
}

tr:first-child td {
border-top: 0;
}
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
79 changes: 70 additions & 9 deletions gateway/assets/test/main.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
package main

import (
"embed"
"fmt"
"net/http"
"os"
"strconv"
"strings"

"github.com/ipfs/boxo/gateway/assets"
"github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime/multicodec"
"github.com/ipld/go-ipld-prime/node/basicnode"
mc "github.com/multiformats/go-multicodec"

// Ensure basic codecs are registered.
_ "github.com/ipld/go-ipld-prime/codec/cbor"
_ "github.com/ipld/go-ipld-prime/codec/dagcbor"
_ "github.com/ipld/go-ipld-prime/codec/dagjson"
_ "github.com/ipld/go-ipld-prime/codec/json"
_ "github.com/ipld/go-ipld-prime/codec/raw"
)

//go:embed dag/*.block
var embeds embed.FS

const (
testPath = "/ipfs/QmFooBarQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7/a/b/c"
)
Expand Down Expand Up @@ -59,14 +75,51 @@ var directoryTestData = assets.DirectoryTemplateData{
Hash: "QmFooBazBar2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7",
}

var dagTestData = assets.DagTemplateData{
GlobalData: assets.GlobalData{
SupportURL: "http://example.com",
},
Path: "/ipfs/baguqeerabn4wonmz6icnk7dfckuizcsf4e4igua2ohdboecku225xxmujepa",
CID: "baguqeerabn4wonmz6icnk7dfckuizcsf4e4igua2ohdboecku225xxmujepa",
CodecName: "dag-json",
CodecHex: "0x129",
var dagTestData = map[string]*assets.DagTemplateData{}

func loadDagTestData() {
entries, err := embeds.ReadDir("dag")
if err != nil {
panic(err)
}

for _, entry := range entries {
cidStr := strings.TrimSuffix(entry.Name(), ".block")
cid, err := cid.Decode(cidStr)
if err != nil {
panic(err)
}

f, err := embeds.Open("dag/" + entry.Name())
if err != nil {
panic(err)
}

codec := cid.Prefix().Codec
decoder, err := multicodec.LookupDecoder(codec)
if err != nil {
panic(err)
}

node := basicnode.Prototype.Any.NewBuilder()
err = decoder(node, f)
if err != nil {
panic(err)
}

cidCodec := mc.Code(cid.Prefix().Codec)

dagTestData[cid.String()] = &assets.DagTemplateData{
GlobalData: assets.GlobalData{
SupportURL: "http://example.com",
},
Path: "/ipfs/" + cid.String(),
CID: cid.String(),
CodecName: cidCodec.String(),
CodecHex: fmt.Sprintf("0x%x", uint64(cidCodec)),
Node: assets.NewNode(node.Build(), "/"),
}
}
}

func init() {
Expand All @@ -80,6 +133,8 @@ func init() {
ShortHash: "QmbW\u2026sMnR",
})
}

loadDagTestData()
}

func runTemplate(w http.ResponseWriter, filename string, data interface{}) {
Expand All @@ -101,7 +156,13 @@ func main() {
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/dag":
runTemplate(w, "dag.html", dagTestData)

cid := r.URL.Query().Get("cid")
if cid == "" {
cid = "bafyreihnpl7ami7esahkfdnemm6idx4r2n6u3apmtcrxlqwuapgjsciihy"
}

runTemplate(w, "dag.html", dagTestData[cid])
case "/directory":
runTemplate(w, "directory.html", directoryTestData)
case "/error":
Expand Down
20 changes: 18 additions & 2 deletions gateway/handler_codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func (i *handler) renderCodec(ctx context.Context, w http.ResponseWriter, r *htt
download := r.URL.Query().Get("download") == "true"

if isDAG && acceptsHTML && !download {
return i.serveCodecHTML(ctx, w, r, resolvedPath, contentPath)
return i.serveCodecHTML(ctx, w, r, blockCid, blockData, resolvedPath, contentPath)
} else {
// This covers CIDs with codec 'json' and 'cbor' as those do not have
// an explicit requested content type.
Expand Down Expand Up @@ -153,7 +153,7 @@ func (i *handler) renderCodec(ctx context.Context, w http.ResponseWriter, r *htt
return i.serveCodecConverted(ctx, w, r, blockCid, blockData, contentPath, toCodec, modtime, begin)
}

func (i *handler) serveCodecHTML(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path) bool {
func (i *handler) serveCodecHTML(ctx context.Context, w http.ResponseWriter, r *http.Request, blockCid cid.Cid, blockData io.ReadSeekCloser, resolvedPath ipath.Resolved, contentPath ipath.Path) bool {
// A HTML directory index will be presented, be sure to set the correct
// type instead of relying on autodetection (which may fail).
w.Header().Set("Content-Type", "text/html")
Expand All @@ -170,6 +170,21 @@ func (i *handler) serveCodecHTML(ctx context.Context, w http.ResponseWriter, r *
// TODO: if we ever change behavior for UnixFS dir listings, same changes should be applied here
w.Header().Del("Cache-Control")

// Cleaaaan this up.
// codec := blockCid.Prefix().Codec
// decoder, err := multicodec.LookupDecoder(codec)
// if err != nil {
// i.webError(w, r, err, http.StatusInternalServerError)
// return false
// }

// node := basicnode.Prototype.Any.NewBuilder()
// err = decoder(node, blockData)
// if err != nil {
// i.webError(w, r, err, http.StatusInternalServerError)
// return false
// }

cidCodec := mc.Code(resolvedPath.Cid().Prefix().Codec)
if err := assets.DagTemplate.Execute(w, assets.DagTemplateData{
GlobalData: assets.GlobalData{
Expand All @@ -179,6 +194,7 @@ func (i *handler) serveCodecHTML(ctx context.Context, w http.ResponseWriter, r *
CID: resolvedPath.Cid().String(),
CodecName: cidCodec.String(),
CodecHex: fmt.Sprintf("0x%x", uint64(cidCodec)),
// Node: node.Build(),
}); err != nil {
err = fmt.Errorf("failed to generate HTML listing for this DAG: try fetching raw block with ?format=raw: %w", err)
i.webError(w, r, err, http.StatusInternalServerError)
Expand Down

0 comments on commit bd0d412

Please sign in to comment.