Skip to content

Commit 0332f3d

Browse files
committedJun 25, 2015
Merge pull request #1348 from ipfs/tk/unixfs-ls
Add 'ipfs file ls …'
2 parents aefdb4e + 4acab79 commit 0332f3d

File tree

5 files changed

+414
-2
lines changed

5 files changed

+414
-2
lines changed
 

‎commands/response.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ type ErrorType uint
1515

1616
// ErrorTypes convey what category of error ocurred
1717
const (
18-
ErrNormal ErrorType = iota // general errors
19-
ErrClient // error was caused by the client, (e.g. invalid CLI usage)
18+
ErrNormal ErrorType = iota // general errors
19+
ErrClient // error was caused by the client, (e.g. invalid CLI usage)
20+
ErrImplementation // programmer error in the server
2021
// TODO: add more types of errors for better error-specific handling
2122
)
2223

‎core/commands/root.go

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strings"
66

77
cmds "github.com/ipfs/go-ipfs/commands"
8+
unixfs "github.com/ipfs/go-ipfs/core/commands/unixfs"
89
evlog "github.com/ipfs/go-ipfs/thirdparty/eventlog"
910
)
1011

@@ -35,6 +36,7 @@ DATA STRUCTURE COMMANDS
3536
3637
block Interact with raw blocks in the datastore
3738
object Interact with raw dag nodes
39+
file Interact with Unix filesystem objects
3840
3941
ADVANCED COMMANDS
4042
@@ -102,6 +104,7 @@ var rootSubcommands = map[string]*cmds.Command{
102104
"stats": StatsCmd,
103105
"swarm": SwarmCmd,
104106
"tour": tourCmd,
107+
"file": unixfs.UnixFSCmd,
105108
"update": UpdateCmd,
106109
"version": VersionCmd,
107110
"bitswap": BitswapCmd,

‎core/commands/unixfs/ls.go

+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package unixfs
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"io"
7+
"sort"
8+
"text/tabwriter"
9+
"time"
10+
11+
context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context"
12+
13+
cmds "github.com/ipfs/go-ipfs/commands"
14+
core "github.com/ipfs/go-ipfs/core"
15+
path "github.com/ipfs/go-ipfs/path"
16+
unixfs "github.com/ipfs/go-ipfs/unixfs"
17+
unixfspb "github.com/ipfs/go-ipfs/unixfs/pb"
18+
)
19+
20+
type LsLink struct {
21+
Name, Hash string
22+
Size uint64
23+
Type string
24+
}
25+
26+
type LsObject struct {
27+
Hash string
28+
Size uint64
29+
Type string
30+
Links []LsLink
31+
}
32+
33+
type LsOutput struct {
34+
Arguments map[string]string
35+
Objects map[string]*LsObject
36+
}
37+
38+
var LsCmd = &cmds.Command{
39+
Helptext: cmds.HelpText{
40+
Tagline: "List directory contents for Unix-filesystem objects",
41+
ShortDescription: `
42+
Retrieves the object named by <ipfs-or-ipns-path> and displays the
43+
contents with the following format:
44+
45+
<hash> <type> <size> <name>
46+
47+
For files, the child size is the total size of the file contents. For
48+
directories, the child size is the IPFS link size.
49+
`,
50+
},
51+
52+
Arguments: []cmds.Argument{
53+
cmds.StringArg("ipfs-path", true, true, "The path to the IPFS object(s) to list links from").EnableStdin(),
54+
},
55+
Run: func(req cmds.Request, res cmds.Response) {
56+
node, err := req.Context().GetNode()
57+
if err != nil {
58+
res.SetError(err, cmds.ErrNormal)
59+
return
60+
}
61+
62+
paths := req.Arguments()
63+
64+
output := LsOutput{
65+
Arguments: map[string]string{},
66+
Objects: map[string]*LsObject{},
67+
}
68+
69+
for _, fpath := range paths {
70+
ctx := req.Context().Context
71+
merkleNode, err := core.Resolve(ctx, node, path.Path(fpath))
72+
if err != nil {
73+
res.SetError(err, cmds.ErrNormal)
74+
return
75+
}
76+
77+
key, err := merkleNode.Key()
78+
if err != nil {
79+
res.SetError(err, cmds.ErrNormal)
80+
return
81+
}
82+
83+
hash := key.B58String()
84+
output.Arguments[fpath] = hash
85+
86+
if _, ok := output.Objects[hash]; ok {
87+
// duplicate argument for an already-listed node
88+
continue
89+
}
90+
91+
unixFSNode, err := unixfs.FromBytes(merkleNode.Data)
92+
if err != nil {
93+
res.SetError(err, cmds.ErrNormal)
94+
return
95+
}
96+
97+
t := unixFSNode.GetType()
98+
99+
output.Objects[hash] = &LsObject{
100+
Hash: key.String(),
101+
Type: t.String(),
102+
Size: unixFSNode.GetFilesize(),
103+
}
104+
105+
switch t {
106+
default:
107+
res.SetError(fmt.Errorf("unrecognized type: %s", t), cmds.ErrImplementation)
108+
return
109+
case unixfspb.Data_File:
110+
break
111+
case unixfspb.Data_Directory:
112+
links := make([]LsLink, len(merkleNode.Links))
113+
output.Objects[hash].Links = links
114+
for i, link := range merkleNode.Links {
115+
getCtx, cancel := context.WithTimeout(ctx, time.Minute)
116+
defer cancel()
117+
link.Node, err = link.GetNode(getCtx, node.DAG)
118+
if err != nil {
119+
res.SetError(err, cmds.ErrNormal)
120+
return
121+
}
122+
d, err := unixfs.FromBytes(link.Node.Data)
123+
if err != nil {
124+
res.SetError(err, cmds.ErrNormal)
125+
return
126+
}
127+
t := d.GetType()
128+
lsLink := LsLink{
129+
Name: link.Name,
130+
Hash: link.Hash.B58String(),
131+
Type: t.String(),
132+
}
133+
if t == unixfspb.Data_File {
134+
lsLink.Size = d.GetFilesize()
135+
} else {
136+
lsLink.Size = link.Size
137+
}
138+
links[i] = lsLink
139+
}
140+
}
141+
}
142+
143+
res.SetOutput(&output)
144+
},
145+
Marshalers: cmds.MarshalerMap{
146+
cmds.Text: func(res cmds.Response) (io.Reader, error) {
147+
148+
output := res.Output().(*LsOutput)
149+
buf := new(bytes.Buffer)
150+
w := tabwriter.NewWriter(buf, 1, 2, 1, ' ', 0)
151+
152+
nonDirectories := []string{}
153+
directories := []string{}
154+
for argument, hash := range output.Arguments {
155+
object, ok := output.Objects[hash]
156+
if !ok {
157+
return nil, fmt.Errorf("unresolved hash: %s", hash)
158+
}
159+
160+
if object.Type == "Directory" {
161+
directories = append(directories, argument)
162+
} else {
163+
nonDirectories = append(nonDirectories, argument)
164+
}
165+
}
166+
sort.Strings(nonDirectories)
167+
sort.Strings(directories)
168+
169+
for _, argument := range nonDirectories {
170+
fmt.Fprintf(w, "%s\n", argument)
171+
}
172+
173+
seen := map[string]bool{}
174+
for i, argument := range directories {
175+
hash := output.Arguments[argument]
176+
if _, ok := seen[hash]; ok {
177+
continue
178+
}
179+
seen[hash] = true
180+
181+
object := output.Objects[hash]
182+
if i > 0 || len(nonDirectories) > 0 {
183+
fmt.Fprintln(w)
184+
}
185+
if len(output.Arguments) > 1 {
186+
for _, arg := range directories[i:] {
187+
if output.Arguments[arg] == hash {
188+
fmt.Fprintf(w, "%s:\n", arg)
189+
}
190+
}
191+
}
192+
for _, link := range object.Links {
193+
fmt.Fprintf(w, "%s\n", link.Name)
194+
}
195+
}
196+
w.Flush()
197+
198+
return buf, nil
199+
},
200+
},
201+
Type: LsOutput{},
202+
}

‎core/commands/unixfs/unixfs.go

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package unixfs
2+
3+
import cmds "github.com/ipfs/go-ipfs/commands"
4+
5+
var UnixFSCmd = &cmds.Command{
6+
Helptext: cmds.HelpText{
7+
Tagline: "Interact with ipfs objects representing Unix filesystems",
8+
ShortDescription: `
9+
'ipfs file' provides a familar interface to filesystems represtented
10+
by IPFS objects that hides IPFS-implementation details like layout
11+
objects (e.g. fanout and chunking).
12+
`,
13+
Synopsis: `
14+
ipfs file ls <path>... - List directory contents for <path>...
15+
`,
16+
},
17+
18+
Subcommands: map[string]*cmds.Command{
19+
"ls": LsCmd,
20+
},
21+
}

‎test/sharness/t0200-unixfs-ls.sh

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
#!/bin/sh
2+
#
3+
# Copyright (c) 2014 Christian Couder
4+
# MIT Licensed; see the LICENSE file in this repository.
5+
#
6+
7+
test_description="Test file ls command"
8+
9+
. lib/test-lib.sh
10+
11+
test_init_ipfs
12+
13+
test_ls_cmd() {
14+
15+
test_expect_success "'ipfs add -r testData' succeeds" '
16+
mkdir -p testData testData/d1 testData/d2 &&
17+
echo "test" >testData/f1 &&
18+
echo "data" >testData/f2 &&
19+
echo "hello" >testData/d1/a &&
20+
random 128 42 >testData/d1/128 &&
21+
echo "world" >testData/d2/a &&
22+
random 1024 42 >testData/d2/1024 &&
23+
ipfs add -r testData >actual_add
24+
'
25+
26+
test_expect_success "'ipfs add' output looks good" '
27+
cat <<-\EOF >expected_add &&
28+
added QmQNd6ubRXaNG6Prov8o6vk3bn6eWsj9FxLGrAVDUAGkGe testData/d1/128
29+
added QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN testData/d1/a
30+
added QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss testData/d1
31+
added QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd testData/d2/1024
32+
added QmaRGe7bVmVaLmxbrMiVNXqW4pRNNp3xq7hFtyRKA3mtJL testData/d2/a
33+
added QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy testData/d2
34+
added QmeomffUNfmQy76CQGy9NdmqEnnHU9soCexBnGU3ezPHVH testData/f1
35+
added QmNtocSs7MoDkJMc1RkyisCSKvLadujPsfJfSdJ3e1eA1M testData/f2
36+
added QmfNy183bXiRVyrhyWtq3TwHn79yHEkiAGFr18P7YNzESj testData
37+
EOF
38+
test_cmp expected_add actual_add
39+
'
40+
41+
test_expect_success "'ipfs file ls <dir>' succeeds" '
42+
ipfs file ls QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy >actual_ls_one_directory
43+
'
44+
45+
test_expect_success "'ipfs file ls <dir>' output looks good" '
46+
cat <<-\EOF >expected_ls_one_directory &&
47+
1024
48+
a
49+
EOF
50+
test_cmp expected_ls_one_directory actual_ls_one_directory
51+
'
52+
53+
test_expect_success "'ipfs file ls <three dir hashes>' succeeds" '
54+
ipfs file ls QmfNy183bXiRVyrhyWtq3TwHn79yHEkiAGFr18P7YNzESj QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss >actual_ls_three_directories
55+
'
56+
57+
test_expect_success "'ipfs file ls <three dir hashes>' output looks good" '
58+
cat <<-\EOF >expected_ls_three_directories &&
59+
QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy:
60+
1024
61+
a
62+
63+
QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss:
64+
128
65+
a
66+
67+
QmfNy183bXiRVyrhyWtq3TwHn79yHEkiAGFr18P7YNzESj:
68+
d1
69+
d2
70+
f1
71+
f2
72+
EOF
73+
test_cmp expected_ls_three_directories actual_ls_three_directories
74+
'
75+
76+
test_expect_success "'ipfs file ls <file hashes>' succeeds" '
77+
ipfs file ls /ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024 QmQNd6ubRXaNG6Prov8o6vk3bn6eWsj9FxLGrAVDUAGkGe >actual_ls_file
78+
'
79+
80+
test_expect_success "'ipfs file ls <file hashes>' output looks good" '
81+
cat <<-\EOF >expected_ls_file &&
82+
/ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024
83+
QmQNd6ubRXaNG6Prov8o6vk3bn6eWsj9FxLGrAVDUAGkGe
84+
EOF
85+
test_cmp expected_ls_file actual_ls_file
86+
'
87+
88+
test_expect_success "'ipfs file ls <duplicates>' succeeds" '
89+
ipfs file ls /ipfs/QmfNy183bXiRVyrhyWtq3TwHn79yHEkiAGFr18P7YNzESj/d1 /ipfs/QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss /ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024 /ipfs/QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd >actual_ls_duplicates_file
90+
'
91+
92+
test_expect_success "'ipfs file ls <duplicates>' output looks good" '
93+
cat <<-\EOF >expected_ls_duplicates_file &&
94+
/ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024
95+
/ipfs/QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd
96+
97+
/ipfs/QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss:
98+
/ipfs/QmfNy183bXiRVyrhyWtq3TwHn79yHEkiAGFr18P7YNzESj/d1:
99+
128
100+
a
101+
EOF
102+
test_cmp expected_ls_duplicates_file actual_ls_duplicates_file
103+
'
104+
105+
test_expect_success "'ipfs --encoding=json file ls <file hashes>' succeeds" '
106+
ipfs --encoding=json file ls /ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024 >actual_json_ls_file
107+
'
108+
109+
test_expect_success "'ipfs --encoding=json file ls <file hashes>' output looks good" '
110+
cat <<-\EOF >expected_json_ls_file_trailing_newline &&
111+
{
112+
"Arguments": {
113+
"/ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024": "QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd"
114+
},
115+
"Objects": {
116+
"QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd": {
117+
"Hash": "QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd",
118+
"Size": 1024,
119+
"Type": "File",
120+
"Links": null
121+
}
122+
}
123+
}
124+
EOF
125+
printf %s "$(cat expected_json_ls_file_trailing_newline)" >expected_json_ls_file &&
126+
test_cmp expected_json_ls_file actual_json_ls_file
127+
'
128+
129+
test_expect_success "'ipfs --encoding=json file ls <duplicates>' succeeds" '
130+
ipfs --encoding=json file ls /ipfs/QmfNy183bXiRVyrhyWtq3TwHn79yHEkiAGFr18P7YNzESj/d1 /ipfs/QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss /ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024 /ipfs/QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd >actual_json_ls_duplicates_file
131+
'
132+
133+
test_expect_success "'ipfs --encoding=json file ls <duplicates>' output looks good" '
134+
cat <<-\EOF >expected_json_ls_duplicates_file_trailing_newline &&
135+
{
136+
"Arguments": {
137+
"/ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024": "QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd",
138+
"/ipfs/QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss": "QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss",
139+
"/ipfs/QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd": "QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd",
140+
"/ipfs/QmfNy183bXiRVyrhyWtq3TwHn79yHEkiAGFr18P7YNzESj/d1": "QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss"
141+
},
142+
"Objects": {
143+
"QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss": {
144+
"Hash": "QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss",
145+
"Size": 0,
146+
"Type": "Directory",
147+
"Links": [
148+
{
149+
"Name": "128",
150+
"Hash": "QmQNd6ubRXaNG6Prov8o6vk3bn6eWsj9FxLGrAVDUAGkGe",
151+
"Size": 128,
152+
"Type": "File"
153+
},
154+
{
155+
"Name": "a",
156+
"Hash": "QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN",
157+
"Size": 6,
158+
"Type": "File"
159+
}
160+
]
161+
},
162+
"QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd": {
163+
"Hash": "QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd",
164+
"Size": 1024,
165+
"Type": "File",
166+
"Links": null
167+
}
168+
}
169+
}
170+
EOF
171+
printf %s "$(cat expected_json_ls_duplicates_file_trailing_newline)" >expected_json_ls_duplicates_file &&
172+
test_cmp expected_json_ls_duplicates_file actual_json_ls_duplicates_file
173+
'
174+
}
175+
176+
177+
# should work offline
178+
test_ls_cmd
179+
180+
# should work online
181+
test_launch_ipfs_daemon
182+
test_ls_cmd
183+
test_kill_ipfs_daemon
184+
185+
test_done

0 commit comments

Comments
 (0)
Please sign in to comment.