Skip to content

Commit

Permalink
http,ipfs: better support for webapps
Browse files Browse the repository at this point in the history
  • Loading branch information
n8maninger committed Mar 20, 2024
1 parent 48fa245 commit 605cfc5
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 25 deletions.
24 changes: 18 additions & 6 deletions http/ipfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package http
import (
"context"
"errors"
"mime"
"net/http"
"net/url"
"path/filepath"
Expand Down Expand Up @@ -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 {
Expand All @@ -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 ""
}
Expand Down Expand Up @@ -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)

}

Check failure on line 206 in http/ipfs.go

View workflow job for this annotation

GitHub Actions / test

unnecessary trailing newline (whitespace)

Check failure on line 206 in http/ipfs.go

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 1.21)

unnecessary trailing newline (whitespace)
}

Expand Down
93 changes: 74 additions & 19 deletions ipfs/unixfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 605cfc5

Please sign in to comment.