diff --git a/commands/response.go b/commands/response.go index a720a8214542d5a6a42cd41f6be95bf700343e0b..657dd0403e2a647d80f708e89900016337b77cb7 100644 --- a/commands/response.go +++ b/commands/response.go @@ -15,8 +15,9 @@ type ErrorType uint // ErrorTypes convey what category of error ocurred const ( - ErrNormal ErrorType = iota // general errors - ErrClient // error was caused by the client, (e.g. invalid CLI usage) + ErrNormal ErrorType = iota // general errors + ErrClient // error was caused by the client, (e.g. invalid CLI usage) + ErrImplementation // programmer error in the server // TODO: add more types of errors for better error-specific handling ) diff --git a/core/commands/root.go b/core/commands/root.go index 916b32b63f8935878b2ff6a67184d029d757d77a..13d459162666c8bacc74138ebf8e8510d3612b1e 100644 --- a/core/commands/root.go +++ b/core/commands/root.go @@ -5,6 +5,7 @@ import ( "strings" cmds "github.com/ipfs/go-ipfs/commands" + unixfs "github.com/ipfs/go-ipfs/core/commands/unixfs" evlog "github.com/ipfs/go-ipfs/thirdparty/eventlog" ) @@ -35,6 +36,7 @@ DATA STRUCTURE COMMANDS block Interact with raw blocks in the datastore object Interact with raw dag nodes + file Interact with Unix filesystem objects ADVANCED COMMANDS @@ -102,6 +104,7 @@ var rootSubcommands = map[string]*cmds.Command{ "stats": StatsCmd, "swarm": SwarmCmd, "tour": tourCmd, + "file": unixfs.UnixFSCmd, "update": UpdateCmd, "version": VersionCmd, "bitswap": BitswapCmd, diff --git a/core/commands/unixfs/ls.go b/core/commands/unixfs/ls.go new file mode 100644 index 0000000000000000000000000000000000000000..989f6576022644482e3f905291e8d066be1dfab9 --- /dev/null +++ b/core/commands/unixfs/ls.go @@ -0,0 +1,202 @@ +package unixfs + +import ( + "bytes" + "fmt" + "io" + "sort" + "text/tabwriter" + "time" + + context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context" + + cmds "github.com/ipfs/go-ipfs/commands" + core "github.com/ipfs/go-ipfs/core" + path "github.com/ipfs/go-ipfs/path" + unixfs "github.com/ipfs/go-ipfs/unixfs" + unixfspb "github.com/ipfs/go-ipfs/unixfs/pb" +) + +type LsLink struct { + Name, Hash string + Size uint64 + Type string +} + +type LsObject struct { + Hash string + Size uint64 + Type string + Links []LsLink +} + +type LsOutput struct { + Arguments map[string]string + Objects map[string]*LsObject +} + +var LsCmd = &cmds.Command{ + Helptext: cmds.HelpText{ + Tagline: "List directory contents for Unix-filesystem objects", + ShortDescription: ` +Retrieves the object named by <ipfs-or-ipns-path> and displays the +contents with the following format: + + <hash> <type> <size> <name> + +For files, the child size is the total size of the file contents. For +directories, the child size is the IPFS link size. +`, + }, + + Arguments: []cmds.Argument{ + cmds.StringArg("ipfs-path", true, true, "The path to the IPFS object(s) to list links from").EnableStdin(), + }, + Run: func(req cmds.Request, res cmds.Response) { + node, err := req.Context().GetNode() + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + + paths := req.Arguments() + + output := LsOutput{ + Arguments: map[string]string{}, + Objects: map[string]*LsObject{}, + } + + for _, fpath := range paths { + ctx := req.Context().Context + merkleNode, err := core.Resolve(ctx, node, path.Path(fpath)) + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + + key, err := merkleNode.Key() + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + + hash := key.B58String() + output.Arguments[fpath] = hash + + if _, ok := output.Objects[hash]; ok { + // duplicate argument for an already-listed node + continue + } + + unixFSNode, err := unixfs.FromBytes(merkleNode.Data) + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + + t := unixFSNode.GetType() + + output.Objects[hash] = &LsObject{ + Hash: key.String(), + Type: t.String(), + Size: unixFSNode.GetFilesize(), + } + + switch t { + default: + res.SetError(fmt.Errorf("unrecognized type: %s", t), cmds.ErrImplementation) + return + case unixfspb.Data_File: + break + case unixfspb.Data_Directory: + links := make([]LsLink, len(merkleNode.Links)) + output.Objects[hash].Links = links + for i, link := range merkleNode.Links { + getCtx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + link.Node, err = link.GetNode(getCtx, node.DAG) + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + d, err := unixfs.FromBytes(link.Node.Data) + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + t := d.GetType() + lsLink := LsLink{ + Name: link.Name, + Hash: link.Hash.B58String(), + Type: t.String(), + } + if t == unixfspb.Data_File { + lsLink.Size = d.GetFilesize() + } else { + lsLink.Size = link.Size + } + links[i] = lsLink + } + } + } + + res.SetOutput(&output) + }, + Marshalers: cmds.MarshalerMap{ + cmds.Text: func(res cmds.Response) (io.Reader, error) { + + output := res.Output().(*LsOutput) + buf := new(bytes.Buffer) + w := tabwriter.NewWriter(buf, 1, 2, 1, ' ', 0) + + nonDirectories := []string{} + directories := []string{} + for argument, hash := range output.Arguments { + object, ok := output.Objects[hash] + if !ok { + return nil, fmt.Errorf("unresolved hash: %s", hash) + } + + if object.Type == "Directory" { + directories = append(directories, argument) + } else { + nonDirectories = append(nonDirectories, argument) + } + } + sort.Strings(nonDirectories) + sort.Strings(directories) + + for _, argument := range nonDirectories { + fmt.Fprintf(w, "%s\n", argument) + } + + seen := map[string]bool{} + for i, argument := range directories { + hash := output.Arguments[argument] + if _, ok := seen[hash]; ok { + continue + } + seen[hash] = true + + object := output.Objects[hash] + if i > 0 || len(nonDirectories) > 0 { + fmt.Fprintln(w) + } + if len(output.Arguments) > 1 { + for _, arg := range directories[i:] { + if output.Arguments[arg] == hash { + fmt.Fprintf(w, "%s:\n", arg) + } + } + } + for _, link := range object.Links { + fmt.Fprintf(w, "%s\n", link.Name) + } + } + w.Flush() + + return buf, nil + }, + }, + Type: LsOutput{}, +} diff --git a/core/commands/unixfs/unixfs.go b/core/commands/unixfs/unixfs.go new file mode 100644 index 0000000000000000000000000000000000000000..67a30c6cbd58bb7fabf8040316a1399a89d29b18 --- /dev/null +++ b/core/commands/unixfs/unixfs.go @@ -0,0 +1,21 @@ +package unixfs + +import cmds "github.com/ipfs/go-ipfs/commands" + +var UnixFSCmd = &cmds.Command{ + Helptext: cmds.HelpText{ + Tagline: "Interact with ipfs objects representing Unix filesystems", + ShortDescription: ` +'ipfs file' provides a familar interface to filesystems represtented +by IPFS objects that hides IPFS-implementation details like layout +objects (e.g. fanout and chunking). +`, + Synopsis: ` +ipfs file ls <path>... - List directory contents for <path>... +`, + }, + + Subcommands: map[string]*cmds.Command{ + "ls": LsCmd, + }, +} diff --git a/test/sharness/t0200-unixfs-ls.sh b/test/sharness/t0200-unixfs-ls.sh new file mode 100755 index 0000000000000000000000000000000000000000..653b3872f9c0335dd4bdd998ae24db8300c404e7 --- /dev/null +++ b/test/sharness/t0200-unixfs-ls.sh @@ -0,0 +1,185 @@ +#!/bin/sh +# +# Copyright (c) 2014 Christian Couder +# MIT Licensed; see the LICENSE file in this repository. +# + +test_description="Test file ls command" + +. lib/test-lib.sh + +test_init_ipfs + +test_ls_cmd() { + + test_expect_success "'ipfs add -r testData' succeeds" ' + mkdir -p testData testData/d1 testData/d2 && + echo "test" >testData/f1 && + echo "data" >testData/f2 && + echo "hello" >testData/d1/a && + random 128 42 >testData/d1/128 && + echo "world" >testData/d2/a && + random 1024 42 >testData/d2/1024 && + ipfs add -r testData >actual_add + ' + + test_expect_success "'ipfs add' output looks good" ' + cat <<-\EOF >expected_add && + added QmQNd6ubRXaNG6Prov8o6vk3bn6eWsj9FxLGrAVDUAGkGe testData/d1/128 + added QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN testData/d1/a + added QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss testData/d1 + added QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd testData/d2/1024 + added QmaRGe7bVmVaLmxbrMiVNXqW4pRNNp3xq7hFtyRKA3mtJL testData/d2/a + added QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy testData/d2 + added QmeomffUNfmQy76CQGy9NdmqEnnHU9soCexBnGU3ezPHVH testData/f1 + added QmNtocSs7MoDkJMc1RkyisCSKvLadujPsfJfSdJ3e1eA1M testData/f2 + added QmfNy183bXiRVyrhyWtq3TwHn79yHEkiAGFr18P7YNzESj testData + EOF + test_cmp expected_add actual_add + ' + + test_expect_success "'ipfs file ls <dir>' succeeds" ' + ipfs file ls QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy >actual_ls_one_directory + ' + + test_expect_success "'ipfs file ls <dir>' output looks good" ' + cat <<-\EOF >expected_ls_one_directory && + 1024 + a + EOF + test_cmp expected_ls_one_directory actual_ls_one_directory + ' + + test_expect_success "'ipfs file ls <three dir hashes>' succeeds" ' + ipfs file ls QmfNy183bXiRVyrhyWtq3TwHn79yHEkiAGFr18P7YNzESj QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss >actual_ls_three_directories + ' + + test_expect_success "'ipfs file ls <three dir hashes>' output looks good" ' + cat <<-\EOF >expected_ls_three_directories && + QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy: + 1024 + a + + QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss: + 128 + a + + QmfNy183bXiRVyrhyWtq3TwHn79yHEkiAGFr18P7YNzESj: + d1 + d2 + f1 + f2 + EOF + test_cmp expected_ls_three_directories actual_ls_three_directories + ' + + test_expect_success "'ipfs file ls <file hashes>' succeeds" ' + ipfs file ls /ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024 QmQNd6ubRXaNG6Prov8o6vk3bn6eWsj9FxLGrAVDUAGkGe >actual_ls_file + ' + + test_expect_success "'ipfs file ls <file hashes>' output looks good" ' + cat <<-\EOF >expected_ls_file && + /ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024 + QmQNd6ubRXaNG6Prov8o6vk3bn6eWsj9FxLGrAVDUAGkGe + EOF + test_cmp expected_ls_file actual_ls_file + ' + + test_expect_success "'ipfs file ls <duplicates>' succeeds" ' + ipfs file ls /ipfs/QmfNy183bXiRVyrhyWtq3TwHn79yHEkiAGFr18P7YNzESj/d1 /ipfs/QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss /ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024 /ipfs/QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd >actual_ls_duplicates_file + ' + + test_expect_success "'ipfs file ls <duplicates>' output looks good" ' + cat <<-\EOF >expected_ls_duplicates_file && + /ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024 + /ipfs/QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd + + /ipfs/QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss: + /ipfs/QmfNy183bXiRVyrhyWtq3TwHn79yHEkiAGFr18P7YNzESj/d1: + 128 + a + EOF + test_cmp expected_ls_duplicates_file actual_ls_duplicates_file + ' + + test_expect_success "'ipfs --encoding=json file ls <file hashes>' succeeds" ' + ipfs --encoding=json file ls /ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024 >actual_json_ls_file + ' + + test_expect_success "'ipfs --encoding=json file ls <file hashes>' output looks good" ' + cat <<-\EOF >expected_json_ls_file_trailing_newline && + { + "Arguments": { + "/ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024": "QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd" + }, + "Objects": { + "QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd": { + "Hash": "QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd", + "Size": 1024, + "Type": "File", + "Links": null + } + } + } + EOF + printf %s "$(cat expected_json_ls_file_trailing_newline)" >expected_json_ls_file && + test_cmp expected_json_ls_file actual_json_ls_file + ' + + test_expect_success "'ipfs --encoding=json file ls <duplicates>' succeeds" ' + ipfs --encoding=json file ls /ipfs/QmfNy183bXiRVyrhyWtq3TwHn79yHEkiAGFr18P7YNzESj/d1 /ipfs/QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss /ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024 /ipfs/QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd >actual_json_ls_duplicates_file + ' + + test_expect_success "'ipfs --encoding=json file ls <duplicates>' output looks good" ' + cat <<-\EOF >expected_json_ls_duplicates_file_trailing_newline && + { + "Arguments": { + "/ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024": "QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd", + "/ipfs/QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss": "QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss", + "/ipfs/QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd": "QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd", + "/ipfs/QmfNy183bXiRVyrhyWtq3TwHn79yHEkiAGFr18P7YNzESj/d1": "QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss" + }, + "Objects": { + "QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss": { + "Hash": "QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss", + "Size": 0, + "Type": "Directory", + "Links": [ + { + "Name": "128", + "Hash": "QmQNd6ubRXaNG6Prov8o6vk3bn6eWsj9FxLGrAVDUAGkGe", + "Size": 128, + "Type": "File" + }, + { + "Name": "a", + "Hash": "QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN", + "Size": 6, + "Type": "File" + } + ] + }, + "QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd": { + "Hash": "QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd", + "Size": 1024, + "Type": "File", + "Links": null + } + } + } + EOF + printf %s "$(cat expected_json_ls_duplicates_file_trailing_newline)" >expected_json_ls_duplicates_file && + test_cmp expected_json_ls_duplicates_file actual_json_ls_duplicates_file + ' +} + + +# should work offline +test_ls_cmd + +# should work online +test_launch_ipfs_daemon +test_ls_cmd +test_kill_ipfs_daemon + +test_done