diff --git a/etcc.go b/etcc.go index a3920f3..1b0fc93 100644 --- a/etcc.go +++ b/etcc.go @@ -11,13 +11,13 @@ import ( "golang.org/x/net/context" ) -type Etcd struct { +type Etcc struct { api client.KeysAPI } // New trys to connect to etcd server. endpoints must be addreses // delimited by comma, like "http://127.0.0.1:4001,http://127.0.0.1:2379". -func New(endpoints string) (*Etcd, error) { +func New(endpoints string) (*Etcc, error) { eps := strings.Split(endpoints, ",") for i, ep := range eps { u, e := url.Parse(ep) @@ -31,68 +31,95 @@ func New(endpoints string) (*Etcd, error) { eps[i] = u.String() } - tr, e := transport.NewTransport(transport.TLSInfo{}, - 10*time.Second) // timeout = 10sec + tr, e := transport.NewTransport(transport.TLSInfo{}, 10*time.Second) if e != nil { - return nil, fmt.Errorf("transport.NewTransport: %v", e) + return nil, e } c, e := client.New(client.Config{Endpoints: eps, Transport: tr}) if e != nil { - return nil, fmt.Errorf("client.New: %v", e) + return nil, e } - ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout) + ctx, cancel := timeoutContext() e = c.Sync(ctx) cancel() if e != nil { - return nil, fmt.Errorf("(etc)client.Sync: %v", e) + return nil, e } - return &Etcd{client.NewKeysAPI(c)}, nil + return &Etcc{client.NewKeysAPI(c)}, nil +} + +func timeoutContext() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), client.DefaultRequestTimeout) } // Mkdir creates a directory. The directory could be multiple-level, // like /home/yi/hello. But it must not exist before; otherwise Mkdir // returns an error. -func (c *Etcd) Mkdir(dir string) error { - ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout) +func (c *Etcc) Mkdir(dir string) error { + ctx, cancel := timeoutContext() defer cancel() - if _, e := c.api.Set(ctx, dir, "", &client.SetOptions{Dir: true, PrevExist: client.PrevNoExist}); e != nil { - return fmt.Errorf("Etcd.Mkdir: %v", e) - } - return nil + _, e := c.api.Set(ctx, dir, "", &client.SetOptions{Dir: true, PrevExist: client.PrevNoExist}) + return e } -func (c *Etcd) SetWithTTL(key, value string, ttl time.Duration) error { - ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout) +func (c *Etcc) SetWithTTL(key, value string, ttl time.Duration) error { + ctx, cancel := timeoutContext() defer cancel() - if _, e := c.api.Set(ctx, key, value, &client.SetOptions{TTL: ttl}); e != nil { - return fmt.Errorf("Etcd.Set: %v", e) - } - return nil + _, e := c.api.Set(ctx, key, value, &client.SetOptions{TTL: ttl}) + return e } -func (c *Etcd) Set(key, value string) error { +func (c *Etcc) Set(key, value string) error { return c.SetWithTTL(key, value, time.Duration(0)) } -func (c *Etcd) Get(key string) (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout) +func (c *Etcc) Get(key string) (string, error) { + ctx, cancel := timeoutContext() defer cancel() r, e := c.api.Get(ctx, key, &client.GetOptions{Sort: true}) if e != nil { - return "", fmt.Errorf("Etcd.Get: %v", e) + return "", e } return r.Node.Value, nil } -// Rmdir removes a directory and recursively its all content, like bash command `rm -rf`. -func (c *Etcd) Rmdir(dir string) error { - ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout) +// Rm removes a either a key-value pair or a directory. If it is a +// directory, Rm removes all recursive content as well. +func (c *Etcc) Rm(key string) error { + ctx, cancel := timeoutContext() + defer cancel() + _, e := c.api.Delete(ctx, key, &client.DeleteOptions{Recursive: true}) + return e +} + +// Ls returns a list if key is a directory, or the key itself +// otherwise. Ls uses quorum=true read. For more about what does +// quorum=true mean, please refer to +// https://github.com/philips/etcd/blob/05bfb369ef1a8d6c56c9eed7e1ec972dccb25492/Documentation/api.md#read-linearization +func (c *Etcc) Ls(key string) ([]string, error) { + if len(key) == 0 { + key = "/" + } + + ctx, cancel := timeoutContext() defer cancel() - if _, e := c.api.Delete(ctx, dir, &client.DeleteOptions{Dir: true, Recursive: true}); e != nil { - return fmt.Errorf("Etcd.Rmdir: %v", e) + + resp, e := c.api.Get(ctx, key, &client.GetOptions{Sort: false, Recursive: false, Quorum: true}) + + if e != nil { + return nil, e + } + + if !resp.Node.Dir { + return []string{resp.Node.Key}, nil + } + + var keys []string + for _, node := range resp.Node.Nodes { + keys = append(keys, node.Key) } - return nil + return keys, e } diff --git a/etcc_test.go b/etcc_test.go index b1649ea..16510a9 100644 --- a/etcc_test.go +++ b/etcc_test.go @@ -20,11 +20,11 @@ func ExampleNew() { c.Set("/home/yi/b", "Banana") c.Get("/home/yi/a") c.Get("home/yi/b") - c.Rmdir("/home") + c.Rm("/home") } var ( - c *Etcd + c *Etcc ) func init() { @@ -38,10 +38,10 @@ func init() { Must(e) } -func TestEtcdBasicSession(t *testing.T) { +func TestEtccBasicSession(t *testing.T) { assert := assert.New(t) - c.Rmdir("/home/yi") + c.Rm("/home/yi") assert.Nil(c.Mkdir("/home/yi")) assert.NotNil(c.Mkdir("/home/yi")) @@ -62,7 +62,7 @@ func TestEtcdBasicSession(t *testing.T) { assert.Nil(e) assert.Equal("Aloha", r) - assert.Nil(c.Rmdir("/home")) + assert.Nil(c.Rm("/home")) } func TestSetWithTTL(t *testing.T) { @@ -82,3 +82,36 @@ func TestSetWithTTL(t *testing.T) { assert.NotNil(e) assert.Equal("", r) } + +func TestLs(t *testing.T) { + assert := assert.New(t) + + c.Rm("/home/yi") + + assert.Nil(c.Mkdir("/home/yi")) + { + ns, e := c.Ls("") + assert.Nil(e) + assert.Equal(1, len(ns)) + assert.Equal("/home", ns[0]) + } + + assert.Nil(c.Set("/home/yi/a", "Apple")) + { + ns, e := c.Ls("/home/yi") + assert.Nil(e) + assert.Equal(1, len(ns)) + assert.Equal("/home/yi/a", ns[0]) + } + + assert.Nil(c.Mkdir("home/yi/b")) + assert.Nil(c.Rm("/home/yi/a")) + { + ns, e := c.Ls("/home/yi") + assert.Nil(e) + assert.Equal(1, len(ns)) + assert.Equal("/home/yi/b", ns[0]) + } + + assert.Nil(c.Rm("/home")) +}