From 605cfc55943d03d07b27d744879b8483c41e13a6 Mon Sep 17 00:00:00 2001 From: Nate Maninger Date: Wed, 20 Mar 2024 16:32:45 -0700 Subject: [PATCH] http,ipfs: better support for webapps --- http/ipfs.go | 24 +++++++++---- ipfs/unixfs.go | 93 +++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 92 insertions(+), 25 deletions(-) diff --git a/http/ipfs.go b/http/ipfs.go index 48ff8c0..b7f7129 100644 --- a/http/ipfs.go +++ b/http/ipfs.go @@ -3,7 +3,6 @@ package http import ( "context" "errors" - "mime" "net/http" "net/url" "path/filepath" @@ -101,7 +100,7 @@ func (is *ipfsGatewayServer) fetchAllowed(ctx context.Context, c cid.Cid) bool { func buildDispositionHeader(params url.Values) string { disposition := "inline" - if download, ok := params["download"]; ok && strings.EqualFold(download[0], "true") { + if strings.EqualFold(params.Get("download"), "true") { disposition = "attachment" } if filename, ok := params["filename"]; ok { @@ -112,9 +111,9 @@ func buildDispositionHeader(params url.Values) string { func getFileName(path []string, params url.Values) string { if filename, ok := params["filename"]; ok { - return mime.TypeByExtension(filepath.Ext(filename[0])) + return filepath.Ext(filename[0]) } else if len(path) > 0 { - return mime.TypeByExtension(filepath.Ext(path[len(path)-1])) + return filepath.Ext(path[len(path)-1]) } return "" } @@ -183,14 +182,27 @@ func (is *ipfsGatewayServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { } w.Write(block.RawData()) default: - rsc, err := is.ipfs.DownloadUnixFile(ctx, c, path) + if strings.EqualFold(query.Get("download"), "true") { + rsc, err := is.ipfs.DownloadUnixFile(ctx, c, path) + if err != nil { + http.Error(w, "", http.StatusNotFound) + is.log.Error("failed to download cid", zap.Error(err)) + return + } + defer rsc.Close() + http.ServeContent(w, r, getFileName(path, query), time.Now(), rsc) + return + } + + rsc, filename, err := is.ipfs.ServeUnixFile(ctx, c, path) if err != nil { http.Error(w, "", http.StatusNotFound) is.log.Error("failed to download cid", zap.Error(err)) return } defer rsc.Close() - http.ServeContent(w, r, getFileName(path, query), time.Now(), rsc) + http.ServeContent(w, r, filename, time.Now(), rsc) + } } diff --git a/ipfs/unixfs.go b/ipfs/unixfs.go index 43b2022..9e0bbd6 100644 --- a/ipfs/unixfs.go +++ b/ipfs/unixfs.go @@ -8,6 +8,7 @@ import ( chunker "github.com/ipfs/boxo/chunker" "github.com/ipfs/boxo/ipld/merkledag" + "github.com/ipfs/boxo/ipld/unixfs" "github.com/ipfs/boxo/ipld/unixfs/importer/balanced" ihelpers "github.com/ipfs/boxo/ipld/unixfs/importer/helpers" fsio "github.com/ipfs/boxo/ipld/unixfs/io" @@ -54,44 +55,98 @@ func UnixFSWithBlockSize(b int64) UnixFSOption { } } +func traverseNode(ctx context.Context, ng format.NodeGetter, parent format.Node, path []string) (format.Node, error) { + if len(path) == 0 { + return parent, nil + } + + childLink, rem, err := parent.Resolve(path) + if err != nil { + return nil, fmt.Errorf("failed to resolve path %q: %w", strings.Join(path, "/"), err) + } + + switch v := childLink.(type) { + case *format.Link: + childNode, err := ng.Get(ctx, v.Cid) + if err != nil { + return nil, fmt.Errorf("failed to get child node %q: %w", v.Cid, err) + } + return traverseNode(ctx, ng, childNode, rem) + default: + return nil, fmt.Errorf("expected link node, got %T", childLink) + } +} + // DownloadUnixFile downloads a UnixFS CID from IPFS func (n *Node) DownloadUnixFile(ctx context.Context, c cid.Cid, path []string) (io.ReadSeekCloser, error) { dagSess := merkledag.NewSession(ctx, n.dagService) + rootNode, err := dagSess.Get(ctx, c) if err != nil { return nil, fmt.Errorf("failed to get root node: %w", err) } - var traverse func(context.Context, format.Node, []string) (format.Node, error) - traverse = func(ctx context.Context, parent format.Node, path []string) (format.Node, error) { - if len(path) == 0 { - return parent, nil + node, err := traverseNode(ctx, n.dagService, rootNode, path) + if err != nil { + return nil, fmt.Errorf("failed to get root node: %w", err) + } + + dr, err := fsio.NewDagReader(ctx, node, dagSess) + return dr, err +} + +// ServeUnixFile is a special case of DownloadUnixFile for single-page webapps +// that serves the index file if the cid is a directory or the path is not found +func (n *Node) ServeUnixFile(ctx context.Context, c cid.Cid, path []string) (io.ReadSeekCloser, string, error) { + dagSess := merkledag.NewSession(ctx, n.dagService) + + rootNode, err := dagSess.Get(ctx, c) + if err != nil { + return nil, "", fmt.Errorf("failed to get root node: %w", err) + } + + var indexFileCid *cid.Cid + for _, link := range rootNode.Links() { + if link.Name == "index.html" { + indexFileCid = &link.Cid + break } + } - childLink, rem, err := parent.Resolve(path) + serveIndex := func() (io.ReadSeekCloser, string, error) { + node, err := dagSess.Get(ctx, *indexFileCid) if err != nil { - return nil, fmt.Errorf("failed to resolve path %q: %w", strings.Join(path, "/"), err) + return nil, "", fmt.Errorf("failed to get index file: %w", err) } + dr, err := fsio.NewDagReader(ctx, node, dagSess) + return dr, "index.html", err + } - switch v := childLink.(type) { - case *format.Link: - childNode, err := dagSess.Get(ctx, v.Cid) - if err != nil { - return nil, fmt.Errorf("failed to get child node %q: %w", v.Cid, err) - } - return traverse(ctx, childNode, rem) - default: - return nil, fmt.Errorf("expected link node, got %T", childLink) + node, err := traverseNode(ctx, n.dagService, rootNode, path) + if err != nil && strings.Contains(err.Error(), "failed to resolve path") { + if indexFileCid != nil { + return serveIndex() } + } else if err != nil { + return nil, "", fmt.Errorf("failed to traverse node: %w", err) } - node, err := traverse(ctx, rootNode, path) - if err != nil { - return nil, fmt.Errorf("failed to traverse path: %w", err) + // if the resolved node is a directory and the index file exists + // serve it. + fsnode, err := unixfs.ExtractFSNode(node) + if err == nil { + if fsnode.IsDir() && indexFileCid != nil { + return serveIndex() + } + } + + filename := c.String() + if len(path) > 0 { + filename = path[len(path)-1] } dr, err := fsio.NewDagReader(ctx, node, dagSess) - return dr, err + return dr, filename, err } // UploadUnixFile uploads a UnixFS file to IPFS