Skip to content

Commit

Permalink
Merge pull request #77 from go-kivik/purge
Browse files Browse the repository at this point in the history
Purge & Copy
  • Loading branch information
flimzy committed Apr 25, 2021
2 parents a41312e + 8039fec commit f3dcc42
Show file tree
Hide file tree
Showing 39 changed files with 730 additions and 44 deletions.
104 changes: 104 additions & 0 deletions cmd/kouchctl/cmd/copy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

package cmd

import (
"fmt"

"github.com/spf13/cobra"

"github.com/go-kivik/xkivik/v4/cmd/kouchctl/config"
"github.com/go-kivik/xkivik/v4/cmd/kouchctl/errors"
)

type copy struct {
*root
}

func copyCmd(r *root) *cobra.Command {
c := &copy{
root: r,
}
cmd := &cobra.Command{
Use: "copy [source] [target]",
Short: "Copy a document",
Long: `Copy an existing document.`,
RunE: c.RunE,
}

return cmd
}

func (c *copy) RunE(cmd *cobra.Command, args []string) error {
client, err := c.client()
if err != nil {
return err
}
sourceDB, sourceDoc, err := c.conf.DBDoc()
if err != nil {
return err
}
if len(args) < 2 { // nolint:gomnd
return errors.Code(errors.ErrUsage, "missing target")
}
target, _, err := config.ContextFromDSN(args[1])
if err != nil {
return fmt.Errorf("invalid target: %w", err)
}

c.log.Debugf("[copy] Will copy: %s/%s/%s to %s", client.DSN(), sourceDB, sourceDoc, target.DSN())

source, _ := c.conf.CurrentCx()
if !shouldEmulateCopy(source, target) {
return c.retry(func() error {
rev, err := client.DB(sourceDB).Copy(cmd.Context(), target.DocID, sourceDoc)
if err != nil {
return err
}
return c.fmt.UpdateResult(target.DocID, rev)
})
}

tClient, err := target.KivikClient(c.parsedConnectTimeout, c.parsedRequestTimeout)
if err != nil {
return err
}

var doc map[string]interface{}
return c.retry(func() error {
if doc == nil {
row := client.DB(sourceDB).Get(cmd.Context(), sourceDoc, c.opts())
if err := row.Err; err != nil {
return err
}
if err := row.ScanDoc(&doc); err != nil {
return err
}
}
rev, err := tClient.DB(target.Database).Put(cmd.Context(), target.DocID, doc)
if err != nil {
return err
}
return c.fmt.UpdateResult(target.DocID, rev)
})
}

func shouldEmulateCopy(s, t *config.Context) bool {
if t.Host != "" && t.Host != s.Host {
return true
}
if t.Database != "" && t.Database != s.Database {
return true
}
return false
}
103 changes: 103 additions & 0 deletions cmd/kouchctl/cmd/copy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

package cmd

import (
"io/ioutil"
"net/http"
"strings"
"testing"

"gitlab.com/flimzy/testy"

"github.com/go-kivik/xkivik/v4/cmd/kouchctl/errors"
)

func Test_copy_RunE(t *testing.T) {
tests := testy.NewTable()

tests.Add("missing dsn", cmdTest{
args: []string{"copy"},
status: errors.ErrUsage,
})
tests.Add("missing target", cmdTest{
args: []string{"copy", "http://example.com/foo/bar"},
status: errors.ErrUsage,
})
tests.Add("invalid target", cmdTest{
args: []string{"copy", "http://example.com/foo/bar", "%xx"},
status: errors.ErrUsage,
})
tests.Add("remote COPY", func(t *testing.T) interface{} {
s := testy.ServeResponseValidator(t, &http.Response{
Header: http.Header{
"ETag": {`"2-62e778c9ec09214dd685a981dcc24074"`},
},
Body: ioutil.NopCloser(strings.NewReader(`{"id": "target","ok": true,"rev": "2-62e778c9ec09214dd685a981dcc24074"}`)),
}, func(t *testing.T, req *http.Request) {
if req.Method != "COPY" {
t.Errorf("Unexpected method: %v", req.Method)
}
if req.URL.Path != "/jkl/src" {
t.Errorf("Unexpected path: %s", req.URL.Path)
}
if d := testy.DiffHTTPRequest(testy.Snapshot(t), req, standardReplacements...); d != nil {
t.Error(d)
}
})

return cmdTest{
args: []string{"--debug", "copy", s.URL + "/jkl/src", "target"},
}
})
tests.Add("emulated COPY", func(t *testing.T) interface{} {
ss := testy.ServeResponseValidator(t, &http.Response{
Header: http.Header{
"Content-Type": {"application/json"},
"ETag": {`"2-62e778c9ec09214dd685a981dcc24074"`},
},
Body: ioutil.NopCloser(strings.NewReader(`{"id": "target","ok": true,"rev": "2-62e778c9ec09214dd685a981dcc24074"}`)),
}, func(t *testing.T, req *http.Request) {
if req.Method != http.MethodGet {
t.Errorf("Unexpected source method: %v", req.Method)
}
if req.URL.Path != "/asdf/src" {
t.Errorf("Unexpected source path: %s", req.URL.Path)
}
})
ts := testy.ServeResponseValidator(t, &http.Response{
Header: http.Header{
"ETag": {`"2-62e778c9ec09214dd685a981dcc24074"`},
},
Body: ioutil.NopCloser(strings.NewReader(`{"id": "target","ok": true,"rev": "2-62e778c9ec09214dd685a981dcc24074"}`)),
}, func(t *testing.T, req *http.Request) {
if req.Method != http.MethodPut {
t.Errorf("Unexpected target method: %v", req.Method)
}
if req.URL.Path != "/qwerty/target" {
t.Errorf("Unexpected target path: %s", req.URL.Path)
}
if d := testy.DiffHTTPRequest(testy.Snapshot(t), req, standardReplacements...); d != nil {
t.Error(d)
}
})

return cmdTest{
args: []string{"--debug", "copy", ss.URL + "/asdf/src", ts.URL + "/qwerty/target"},
}
})

tests.Run(t, func(t *testing.T, tt cmdTest) {
tt.Test(t)
})
}
6 changes: 5 additions & 1 deletion cmd/kouchctl/cmd/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
type post struct {
*root
*input.Input
doc, vc, flush, compact, cv *cobra.Command
doc, vc, flush, compact, cv, purge *cobra.Command
}

func postCmd(r *root) *cobra.Command {
Expand All @@ -38,6 +38,7 @@ func postCmd(r *root) *cobra.Command {
cv: postCompactViewsCmd(r),
}
c.doc = postDocCmd(c)
c.purge = postPurgeCmd(c)

cmd := &cobra.Command{
Use: "post",
Expand All @@ -53,6 +54,7 @@ func postCmd(r *root) *cobra.Command {
cmd.AddCommand(c.flush)
cmd.AddCommand(c.compact)
cmd.AddCommand(c.cv)
cmd.AddCommand(c.purge)

return cmd
}
Expand Down Expand Up @@ -80,6 +82,8 @@ func (c *post) RunE(cmd *cobra.Command, args []string) error {
return c.flush.RunE(cmd, args)
case "_compact":
return c.compact.RunE(cmd, args)
case "_purge":
return c.purge.RunE(cmd, args)
}
if c.conf.HasDB() {
return c.doc.RunE(cmd, args)
Expand Down
111 changes: 111 additions & 0 deletions cmd/kouchctl/cmd/post_purge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

package cmd

import (
"github.com/spf13/cobra"

"github.com/go-kivik/xkivik/v4/cmd/kouchctl/input"
"github.com/go-kivik/xkivik/v4/cmd/kouchctl/output"
)

type postPurge struct {
*root
*input.Input
revs []string
}

func postPurgeRootCmd(r *root) *cobra.Command {
c := &postPurge{
root: r,
Input: input.New(),
}
cmd := &cobra.Command{
Use: "purge [dsn]/[database]/[document]",
Aliases: []string{"ensure-full-commit"},
Short: "Purge document revision(s)",
Long: `Permanently remove the references to documents in the database. Provide the document ID in the DSN, or pass a map of document IDs to revisions via --data or similar.`,
RunE: c.RunE,
}

c.Input.ConfigFlags(cmd.PersistentFlags())

pf := cmd.PersistentFlags()
pf.StringSliceVarP(&c.revs, "revs", "R", nil, "List of revisions to purge")

return cmd
}

func postPurgeCmd(p *post) *cobra.Command {
c := &postPurge{
root: p.root,
Input: p.Input,
}
cmd := &cobra.Command{
Use: "purge [dsn]/[database]/[document]",
Aliases: []string{"ensure-full-commit"},
Short: "Purge document revision(s)",
Long: `Permanently remove the references to documents in the database. Provide the document ID in the DSN, or pass a map of document IDs to revisions via --data or similar.`,
RunE: c.RunE,
}

pf := cmd.PersistentFlags()
pf.StringSliceVarP(&c.revs, "revs", "R", nil, "List of revisions to purge")

return cmd
}

func (c *postPurge) RunE(cmd *cobra.Command, _ []string) error {
client, err := c.client()
if err != nil {
return err
}
var docRevMap map[string][]string
var db string
if c.HasInput() {
err := c.As(&docRevMap)
if err != nil {
return err
}
dsn, err := c.conf.URL()
if err != nil {
return err
}
if cmd, dsnDB := dbCommandFromDSN(dsn); cmd == "_purge" {
db = dsnDB
}
if db == "" {
db, err = c.conf.DB()
if err != nil {
return err
}
}
} else {
var doc string
db, doc, err = c.conf.DBDoc()
if err != nil {
return err
}
docRevMap = map[string][]string{
doc: c.revs,
}
}
c.log.Debugf("[post] Will purge: %s/%s (%v)", client.DSN(), db, docRevMap)
return c.retry(func() error {
result, err := client.DB(db).Purge(cmd.Context(), docRevMap)
if err != nil {
return err
}
return c.fmt.Output(output.JSONReader(result))
})
}
Loading

0 comments on commit f3dcc42

Please sign in to comment.