diff --git a/core/commands/unixfs/ls.go b/core/commands/unixfs/ls.go index 9708800f9159..8a6a8def0eba 100644 --- a/core/commands/unixfs/ls.go +++ b/core/commands/unixfs/ls.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io" + "sort" "text/tabwriter" "time" @@ -23,12 +24,12 @@ type LsLink struct { } type LsObject struct { - Argument string - Links []LsLink + Links []LsLink } type LsOutput struct { - Objects []*LsObject + Arguments map[string]string + Objects map[string]*LsObject } var LsCmd = &cmds.Command{ @@ -57,8 +58,12 @@ directories, the child size is the IPFS link size. paths := req.Arguments() - output := make([]*LsObject, len(paths)) - for i, fpath := range paths { + 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 { @@ -66,13 +71,27 @@ directories, the child size is the IPFS link size. return } - unixFSNode, err := unixfs.FromBytes(merkleNode.Data) + key, err := merkleNode.Key() if err != nil { res.SetError(err, cmds.ErrNormal) return } - output[i] = &LsObject{Argument: fpath} + hash := key.B58String() + output.Arguments[fpath] = hash + + if _, ok := output.Objects[hash]; ok { + // duplicate argument for an already-listed node + continue + } + + output.Objects[hash] = &LsObject{} + + unixFSNode, err := unixfs.FromBytes(merkleNode.Data) + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } t := unixFSNode.GetType() switch t { @@ -85,15 +104,16 @@ directories, the child size is the IPFS link size. res.SetError(err, cmds.ErrNormal) return } - output[i].Links = []LsLink{LsLink{ + output.Objects[hash].Links = []LsLink{LsLink{ Name: fpath, Hash: key.String(), Type: t.String(), Size: unixFSNode.GetFilesize(), }} case unixfspb.Data_Directory: - output[i].Links = make([]LsLink, len(merkleNode.Links)) - for j, link := range merkleNode.Links { + 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) @@ -117,12 +137,12 @@ directories, the child size is the IPFS link size. } else { lsLink.Size = link.Size } - output[i].Links[j] = lsLink + links[i] = lsLink } } } - res.SetOutput(&LsOutput{Objects: output}) + res.SetOutput(&output) }, Marshalers: cmds.MarshalerMap{ cmds.Text: func(res cmds.Response) (io.Reader, error) { @@ -130,21 +150,41 @@ directories, the child size is the IPFS link size. output := res.Output().(*LsOutput) buf := new(bytes.Buffer) w := tabwriter.NewWriter(buf, 1, 2, 1, ' ', 0) - lastObjectDirHeader := false - for i, object := range output.Objects { - singleObject := (len(object.Links) == 1 && - object.Links[0].Name == object.Argument) - if len(output.Objects) > 1 && !singleObject { - if i > 0 { - fmt.Fprintln(w) - } - fmt.Fprintf(w, "%s:\n", object.Argument) - lastObjectDirHeader = true + + nonDirectories := []string{} + directories := []string{} + for argument := range output.Arguments { + hash := output.Arguments[argument] + object := output.Objects[hash] + if len(object.Links) == 1 && object.Links[0].Hash == hash { + nonDirectories = append(nonDirectories, argument) } else { - if lastObjectDirHeader { - fmt.Fprintln(w) + directories = append(directories, 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) + } + for _, arg := range directories[i:] { + if output.Arguments[arg] == hash { + fmt.Fprintf(w, "%s:\n", arg) } - lastObjectDirHeader = false } for _, link := range object.Links { fmt.Fprintf(w, "%s\n", link.Name) diff --git a/test/sharness/t0200-unixfs-ls.sh b/test/sharness/t0200-unixfs-ls.sh index 414eece60c48..473b70a30cff 100755 --- a/test/sharness/t0200-unixfs-ls.sh +++ b/test/sharness/t0200-unixfs-ls.sh @@ -44,12 +44,6 @@ test_ls_cmd() { test_expect_success "'ipfs file ls ' output looks good" ' cat <<-\EOF >expected_ls && - QmfNy183bXiRVyrhyWtq3TwHn79yHEkiAGFr18P7YNzESj: - d1 - d2 - f1 - f2 - QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy: 1024 a @@ -57,6 +51,12 @@ test_ls_cmd() { QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss: 128 a + + QmfNy183bXiRVyrhyWtq3TwHn79yHEkiAGFr18P7YNzESj: + d1 + d2 + f1 + f2 EOF test_cmp expected_ls actual_ls ' @@ -73,6 +73,23 @@ test_ls_cmd() { test_cmp expected_ls_file actual_ls_file ' + test_expect_success "'ipfs file ls ' succeeds" ' + ipfs file ls /ipfs/QmfNy183bXiRVyrhyWtq3TwHn79yHEkiAGFr18P7YNzESj/d1 /ipfs/QmSix55yz8CzWXf5ZVM9vgEvijnEeeXiTSarVtsqiiCJss /ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024 /ipfs/QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd >actual_ls_duplicates_file + ' + + test_expect_success "'ipfs file ls ' 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 ' succeeds" ' ipfs --encoding=json file ls /ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024 >actual_json_ls_file ' @@ -80,9 +97,11 @@ test_ls_cmd() { test_expect_success "'ipfs --encoding=json file ls ' output looks good" ' cat <<-\EOF >expected_json_ls_file_trailing_newline && { - "Objects": [ - { - "Argument": "/ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024", + "Arguments": { + "/ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024": "QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd" + }, + "Objects": { + "QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd": { "Links": [ { "Name": "/ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024", @@ -92,12 +111,59 @@ test_ls_cmd() { } ] } - ] + } } 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 ' 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 ' 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": { + "Links": [ + { + "Name": "128", + "Hash": "QmQNd6ubRXaNG6Prov8o6vk3bn6eWsj9FxLGrAVDUAGkGe", + "Size": 128, + "Type": "File" + }, + { + "Name": "a", + "Hash": "QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN", + "Size": 6, + "Type": "File" + } + ] + }, + "QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd": { + "Links": [ + { + "Name": "/ipfs/QmR3jhV4XpxxPjPT3Y8vNnWvWNvakdcT3H6vqpRBsX1MLy/1024", + "Hash": "QmbQBUSRL9raZtNXfpTDeaxQapibJEG6qEY8WqAN22aUzd", + "Size": 1024, + "Type": "File" + } + ] + } + } + } + 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 + ' }