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 2 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
96 changes: 96 additions & 0 deletions client/files.go
@@ -0,0 +1,96 @@
// 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 client

import (
"bytes"
"encoding/json"
"fmt"
)

// 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 *errorResult `json:"error,omitempty"`
}

type errorResult struct {
anpep marked this conversation as resolved.
Show resolved Hide resolved
Message string `json:"message"`
Kind string `json:"kind,omitempty"`
Value interface{} `json:"value,omitempty"`
}
anpep marked this conversation as resolved.
Show resolved Hide resolved

// 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
err := json.NewEncoder(&body).Encode(&payload)
if err != nil {
return err
}

var result []fileResult
_, err = client.doSync("POST", "/v1/files", nil, map[string]string{
"Content-Type": "application/json",
}, &body, &result)
if 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
}
188 changes: 188 additions & 0 deletions client/files_test.go
@@ -0,0 +1,188 @@
// 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 client_test

import (
"encoding/json"

. "gopkg.in/check.v1"

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

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) TestMakeDirFailsWithMultipleAPIResults(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{"exec"},
Commands: []string{"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" long:"recursive"`
anpep marked this conversation as resolved.
Show resolved Hide resolved

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

var rmDescs = map[string]string{
"recursive": "Remove all files and directories contained within 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)
}