Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement RemovePath and pebble rm command #146

Merged
merged 5 commits into from Dec 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
72 changes: 67 additions & 5 deletions client/files.go
Expand Up @@ -251,11 +251,6 @@ type makeDirsItem struct {
Group string `json:"group"`
}

type fileResult struct {
Path string `json:"path"`
Error *Error `json:"error,omitempty"`
}

// MakeDir creates a directory or directory tree.
func (client *Client) MakeDir(opts *MakeDirOptions) error {
var permissions string
Expand Down Expand Up @@ -302,3 +297,70 @@ func (client *Client) MakeDir(opts *MakeDirOptions) error {

return nil
}

// RemovePathOptions holds the options for a call to RemovePath.
type RemovePathOptions struct {
// Path is the absolute path to be deleted (required).
Path string

// Recursive, if true, will delete all files and directories contained
// within the specified path, recursively. Defaults to false.
Recursive bool
}

type removePathsPayload struct {
Action string `json:"action"`
Paths []removePathsItem `json:"paths"`
}

type removePathsItem struct {
Path string `json:"path"`
Recursive bool `json:"recursive"`
}

type fileResult struct {
Path string `json:"path"`
Error *Error `json:"error,omitempty"`
}

// RemovePath deletes a file or directory.
// The error returned is a *Error if the request went through successfully
// but there was an OS-level error deleting a file or directory, with the Kind
// field set to the specific error kind, for example "permission-denied".
func (client *Client) RemovePath(opts *RemovePathOptions) error {
payload := &removePathsPayload{
Action: "remove",
Paths: []removePathsItem{
{
Path: opts.Path,
Recursive: opts.Recursive,
},
},
}

var body bytes.Buffer
if err := json.NewEncoder(&body).Encode(&payload); err != nil {
return fmt.Errorf("cannot encode JSON payload: %w", err)
}

var result []fileResult
headers := map[string]string{
"Content-Type": "application/json",
}
if _, err := client.doSync("POST", "/v1/files", nil, headers, &body, &result); err != nil {
return err
}

if len(result) != 1 {
return fmt.Errorf("expected exactly one result from API, got %d", len(result))
}
if result[0].Error != nil {
return &Error{
Kind: result[0].Error.Kind,
Value: result[0].Error.Value,
Message: result[0].Error.Message,
}
}

return nil
}
158 changes: 158 additions & 0 deletions client/files_test.go
Expand Up @@ -416,3 +416,161 @@ func (cs *clientSuite) TestMakeDirFailsWithMultipleAPIResults(c *C) {
}},
})
}

type removePathsPayload struct {
Action string `json:"action"`
Paths []removePathsItem `json:"paths"`
}

type removePathsItem struct {
Path string `json:"path"`
Recursive bool `json:"recursive"`
}

func (cs *clientSuite) TestRemovePath(c *C) {
cs.rsp = `{"type": "sync", "result": [{"path": "/foo/bar"}]}`

err := cs.cli.RemovePath(&client.RemovePathOptions{
Path: "/foo/bar",
})
c.Assert(err, IsNil)

c.Assert(cs.req.URL.Path, Equals, "/v1/files")
c.Assert(cs.req.Method, Equals, "POST")

var payload removePathsPayload
decoder := json.NewDecoder(cs.req.Body)
err = decoder.Decode(&payload)
c.Assert(err, IsNil)
c.Check(payload, DeepEquals, removePathsPayload{
Action: "remove",
Paths: []removePathsItem{{
Path: "/foo/bar",
}},
})
}

func (cs *clientSuite) TestRemovePathRecursive(c *C) {
cs.rsp = `{"type": "sync", "result": [{"path": "/foo/bar"}]}`

err := cs.cli.RemovePath(&client.RemovePathOptions{
Path: "/foo/bar",
Recursive: true,
})
c.Assert(err, IsNil)

c.Assert(cs.req.URL.Path, Equals, "/v1/files")
c.Assert(cs.req.Method, Equals, "POST")

var payload removePathsPayload
decoder := json.NewDecoder(cs.req.Body)
err = decoder.Decode(&payload)
c.Assert(err, IsNil)
c.Check(payload, DeepEquals, removePathsPayload{
Action: "remove",
Paths: []removePathsItem{{
Path: "/foo/bar",
Recursive: true,
}},
})
}

func (cs *clientSuite) TestRemovePathFails(c *C) {
cs.rsp = `{"type": "error", "result": {"message": "could not foo"}}`
err := cs.cli.RemovePath(&client.RemovePathOptions{
Path: "/foobar",
})
c.Assert(err, ErrorMatches, "could not foo")

c.Assert(cs.req.URL.Path, Equals, "/v1/files")
c.Assert(cs.req.Method, Equals, "POST")

var payload removePathsPayload
decoder := json.NewDecoder(cs.req.Body)
err = decoder.Decode(&payload)
c.Assert(err, IsNil)
c.Check(payload, DeepEquals, removePathsPayload{
Action: "remove",
Paths: []removePathsItem{{
Path: "/foobar",
}},
})
}

func (cs *clientSuite) TestRemovePathFailsOnPath(c *C) {
cs.rsp = ` {
"type": "sync",
"result": [{
"path": "/foo/bar/baz.qux",
"error": {
"message": "could not bar",
"kind": "permission-denied",
"value": 42
}
}]
}`

err := cs.cli.RemovePath(&client.RemovePathOptions{
Path: "/foo/bar",
Recursive: true,
})
clientErr, ok := err.(*client.Error)
c.Assert(ok, Equals, true)
c.Assert(clientErr.Message, Equals, "could not bar")
c.Assert(clientErr.Kind, Equals, "permission-denied")

c.Assert(cs.req.URL.Path, Equals, "/v1/files")
c.Assert(cs.req.Method, Equals, "POST")

var payload removePathsPayload
decoder := json.NewDecoder(cs.req.Body)
err = decoder.Decode(&payload)
c.Assert(err, IsNil)
c.Check(payload, DeepEquals, removePathsPayload{
Action: "remove",
Paths: []removePathsItem{{
Path: "/foo/bar",
Recursive: true,
}},
})
}

func (cs *clientSuite) TestRemovePathFailsWithMultipleAPIResults(c *C) {
cs.rsp = `{
"type": "sync",
"result": [{
"path": "/foobar",
"error": {
"message": "could not bar",
"kind": "permission-denied",
"value": 42
}
}, {
"path": "/foobar",
"error": {
"message": "could not baz",
"kind": "generic-file-error",
"value": 47
}
}]
}`

err := cs.cli.RemovePath(&client.RemovePathOptions{
Path: "/foobar",
})
c.Assert(err, ErrorMatches, "expected exactly one result from API, got 2")

c.Assert(cs.req.URL.Path, Equals, "/v1/files")
c.Assert(cs.req.Method, Equals, "POST")

var payload removePathsPayload
decoder := json.NewDecoder(cs.req.Body)
err = decoder.Decode(&payload)
c.Assert(err, IsNil)
c.Check(payload, DeepEquals, removePathsPayload{
Action: "remove",
Paths: []removePathsItem{{
Path: "/foobar",
}},
})
}
2 changes: 1 addition & 1 deletion cmd/pebble/cmd_help.go
Expand Up @@ -180,7 +180,7 @@ var helpCategories = []helpCategory{{
}, {
Label: "Files",
Description: "work with files and execute commands",
Commands: []string{"ls", "mkdir", "exec"},
Commands: []string{"ls", "mkdir", "rm", "exec"},
}, {
Label: "Changes",
Description: "manage changes and their tasks",
Expand Down
55 changes: 55 additions & 0 deletions cmd/pebble/cmd_rm.go
@@ -0,0 +1,55 @@
// Copyright (c) 2022 Canonical Ltd
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 3 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package main

import (
"github.com/jessevdk/go-flags"

"github.com/canonical/pebble/client"
)

type cmdRm struct {
clientMixin

Recursive bool `short:"r"`

Positional struct {
Path string `positional-arg-name:"<path>"`
} `positional-args:"yes" required:"yes"`
}

var rmDescs = map[string]string{
"r": "Remove all files and directories recursively in the specified path",
}

var shortRmHelp = "Remove a file or directory."
var longRmHelp = `
The rm command removes a file or directory.
`

func (cmd *cmdRm) Execute(args []string) error {
if len(args) > 0 {
return ErrExtraArgs
}

return cmd.client.RemovePath(&client.RemovePathOptions{
Path: cmd.Positional.Path,
Recursive: cmd.Recursive,
})
}

func init() {
addCommand("rm", shortRmHelp, longRmHelp, func() flags.Commander { return &cmdRm{} }, rmDescs, nil)
}