2018-05-14 16:26:38 +00:00
|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
2018-05-22 22:05:17 +00:00
|
|
|
"errors"
|
2018-05-14 16:26:38 +00:00
|
|
|
"net/http"
|
2018-05-16 18:59:35 +00:00
|
|
|
"path"
|
2018-05-14 16:26:38 +00:00
|
|
|
|
|
|
|
"github.com/influxdata/platform"
|
2018-05-22 22:05:17 +00:00
|
|
|
kerrors "github.com/influxdata/platform/kit/errors"
|
2018-05-14 16:26:38 +00:00
|
|
|
"github.com/julienschmidt/httprouter"
|
|
|
|
)
|
|
|
|
|
|
|
|
// BucketHandler represents an HTTP API handler for buckets.
|
|
|
|
type BucketHandler struct {
|
|
|
|
*httprouter.Router
|
|
|
|
|
|
|
|
BucketService platform.BucketService
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewBucketHandler returns a new instance of BucketHandler.
|
|
|
|
func NewBucketHandler() *BucketHandler {
|
|
|
|
h := &BucketHandler{
|
|
|
|
Router: httprouter.New(),
|
|
|
|
}
|
|
|
|
|
|
|
|
h.HandlerFunc("POST", "/v1/buckets", h.handlePostBucket)
|
|
|
|
h.HandlerFunc("GET", "/v1/buckets", h.handleGetBuckets)
|
|
|
|
h.HandlerFunc("GET", "/v1/buckets/:id", h.handleGetBucket)
|
|
|
|
h.HandlerFunc("PATCH", "/v1/buckets/:id", h.handlePatchBucket)
|
2018-05-16 18:59:35 +00:00
|
|
|
h.HandlerFunc("DELETE", "/v1/buckets/:id", h.handleDeleteBucket)
|
2018-05-14 16:26:38 +00:00
|
|
|
return h
|
|
|
|
}
|
|
|
|
|
|
|
|
// handlePostBucket is the HTTP handler for the POST /v1/buckets route.
|
|
|
|
func (h *BucketHandler) handlePostBucket(w http.ResponseWriter, r *http.Request) {
|
|
|
|
ctx := r.Context()
|
|
|
|
|
|
|
|
req, err := decodePostBucketRequest(ctx, r)
|
|
|
|
if err != nil {
|
2018-06-28 19:32:16 +00:00
|
|
|
EncodeError(ctx, err, w)
|
2018-05-14 16:26:38 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := h.BucketService.CreateBucket(ctx, req.Bucket); err != nil {
|
2018-06-28 19:32:16 +00:00
|
|
|
EncodeError(ctx, err, w)
|
2018-05-14 16:26:38 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := encodeResponse(ctx, w, http.StatusCreated, req.Bucket); err != nil {
|
2018-06-28 19:32:16 +00:00
|
|
|
EncodeError(ctx, err, w)
|
2018-05-14 16:26:38 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type postBucketRequest struct {
|
|
|
|
Bucket *platform.Bucket
|
|
|
|
}
|
|
|
|
|
|
|
|
func decodePostBucketRequest(ctx context.Context, r *http.Request) (*postBucketRequest, error) {
|
|
|
|
b := &platform.Bucket{}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(b); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &postBucketRequest{
|
|
|
|
Bucket: b,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleGetBucket is the HTTP handler for the GET /v1/buckets/:id route.
|
|
|
|
func (h *BucketHandler) handleGetBucket(w http.ResponseWriter, r *http.Request) {
|
|
|
|
ctx := r.Context()
|
|
|
|
|
|
|
|
req, err := decodeGetBucketRequest(ctx, r)
|
|
|
|
if err != nil {
|
2018-06-28 19:32:16 +00:00
|
|
|
EncodeError(ctx, err, w)
|
2018-05-14 16:26:38 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err := h.BucketService.FindBucketByID(ctx, req.BucketID)
|
|
|
|
if err != nil {
|
2018-06-28 19:32:16 +00:00
|
|
|
EncodeError(ctx, err, w)
|
2018-05-14 16:26:38 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := encodeResponse(ctx, w, http.StatusOK, b); err != nil {
|
2018-06-28 19:32:16 +00:00
|
|
|
EncodeError(ctx, err, w)
|
2018-05-14 16:26:38 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type getBucketRequest struct {
|
|
|
|
BucketID platform.ID
|
|
|
|
}
|
|
|
|
|
|
|
|
func decodeGetBucketRequest(ctx context.Context, r *http.Request) (*getBucketRequest, error) {
|
|
|
|
params := httprouter.ParamsFromContext(ctx)
|
|
|
|
id := params.ByName("id")
|
|
|
|
if id == "" {
|
2018-05-22 22:05:17 +00:00
|
|
|
return nil, kerrors.InvalidDataf("url missing id")
|
2018-05-14 16:26:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var i platform.ID
|
2018-05-16 18:59:35 +00:00
|
|
|
if err := i.DecodeFromString(id); err != nil {
|
2018-05-14 16:26:38 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
req := &getBucketRequest{
|
|
|
|
BucketID: i,
|
|
|
|
}
|
|
|
|
|
|
|
|
return req, nil
|
|
|
|
}
|
|
|
|
|
2018-05-16 18:59:35 +00:00
|
|
|
// handleDeleteBucket is the HTTP handler for the DELETE /v1/buckets/:id route.
|
|
|
|
func (h *BucketHandler) handleDeleteBucket(w http.ResponseWriter, r *http.Request) {
|
|
|
|
ctx := r.Context()
|
|
|
|
|
|
|
|
req, err := decodeDeleteBucketRequest(ctx, r)
|
|
|
|
if err != nil {
|
2018-06-28 19:32:16 +00:00
|
|
|
EncodeError(ctx, err, w)
|
2018-05-16 18:59:35 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := h.BucketService.DeleteBucket(ctx, req.BucketID); err != nil {
|
2018-06-28 19:32:16 +00:00
|
|
|
EncodeError(ctx, err, w)
|
2018-05-16 18:59:35 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
w.WriteHeader(http.StatusAccepted)
|
|
|
|
}
|
|
|
|
|
|
|
|
type deleteBucketRequest struct {
|
|
|
|
BucketID platform.ID
|
|
|
|
}
|
|
|
|
|
|
|
|
func decodeDeleteBucketRequest(ctx context.Context, r *http.Request) (*deleteBucketRequest, error) {
|
|
|
|
params := httprouter.ParamsFromContext(ctx)
|
|
|
|
id := params.ByName("id")
|
|
|
|
if id == "" {
|
2018-05-22 22:05:17 +00:00
|
|
|
return nil, kerrors.InvalidDataf("url missing id")
|
2018-05-16 18:59:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var i platform.ID
|
|
|
|
if err := i.DecodeFromString(id); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
req := &deleteBucketRequest{
|
|
|
|
BucketID: i,
|
|
|
|
}
|
|
|
|
|
|
|
|
return req, nil
|
|
|
|
}
|
|
|
|
|
2018-05-14 16:26:38 +00:00
|
|
|
// handleGetBuckets is the HTTP handler for the GET /v1/buckets route.
|
|
|
|
func (h *BucketHandler) handleGetBuckets(w http.ResponseWriter, r *http.Request) {
|
|
|
|
ctx := r.Context()
|
|
|
|
|
|
|
|
req, err := decodeGetBucketsRequest(ctx, r)
|
|
|
|
if err != nil {
|
2018-06-28 19:32:16 +00:00
|
|
|
EncodeError(ctx, err, w)
|
2018-05-14 16:26:38 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
bs, _, err := h.BucketService.FindBuckets(ctx, req.filter)
|
|
|
|
if err != nil {
|
2018-06-28 19:32:16 +00:00
|
|
|
EncodeError(ctx, err, w)
|
2018-05-14 16:26:38 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := encodeResponse(ctx, w, http.StatusOK, bs); err != nil {
|
2018-06-28 19:32:16 +00:00
|
|
|
EncodeError(ctx, err, w)
|
2018-05-14 16:26:38 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type getBucketsRequest struct {
|
|
|
|
filter platform.BucketFilter
|
|
|
|
}
|
|
|
|
|
|
|
|
func decodeGetBucketsRequest(ctx context.Context, r *http.Request) (*getBucketsRequest, error) {
|
|
|
|
qp := r.URL.Query()
|
|
|
|
req := &getBucketsRequest{}
|
|
|
|
|
|
|
|
if id := qp.Get("orgID"); id != "" {
|
2018-08-01 18:54:32 +00:00
|
|
|
req.filter.OrganizationID = &platform.ID{}
|
|
|
|
if err := req.filter.OrganizationID.DecodeFromString(id); err != nil {
|
2018-05-14 16:26:38 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-16 18:59:35 +00:00
|
|
|
if org := qp.Get("org"); org != "" {
|
|
|
|
req.filter.Organization = &org
|
|
|
|
}
|
|
|
|
|
|
|
|
if id := qp.Get("id"); id != "" {
|
2018-08-01 18:54:32 +00:00
|
|
|
req.filter.ID = &platform.ID{}
|
|
|
|
if err := req.filter.ID.DecodeFromString(id); err != nil {
|
2018-05-14 16:26:38 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-16 18:59:35 +00:00
|
|
|
if name := qp.Get("name"); name != "" {
|
2018-05-14 16:26:38 +00:00
|
|
|
req.filter.Name = &name
|
|
|
|
}
|
|
|
|
|
|
|
|
return req, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// handlePatchBucket is the HTTP handler for the PATH /v1/buckets route.
|
|
|
|
func (h *BucketHandler) handlePatchBucket(w http.ResponseWriter, r *http.Request) {
|
|
|
|
ctx := r.Context()
|
|
|
|
|
|
|
|
req, err := decodePatchBucketRequest(ctx, r)
|
|
|
|
if err != nil {
|
2018-06-28 19:32:16 +00:00
|
|
|
EncodeError(ctx, err, w)
|
2018-05-14 16:26:38 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err := h.BucketService.UpdateBucket(ctx, req.BucketID, req.Update)
|
|
|
|
if err != nil {
|
2018-06-28 19:32:16 +00:00
|
|
|
EncodeError(ctx, err, w)
|
2018-05-14 16:26:38 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := encodeResponse(ctx, w, http.StatusOK, b); err != nil {
|
2018-06-28 19:32:16 +00:00
|
|
|
EncodeError(ctx, err, w)
|
2018-05-14 16:26:38 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type patchBucketRequest struct {
|
|
|
|
Update platform.BucketUpdate
|
|
|
|
BucketID platform.ID
|
|
|
|
}
|
|
|
|
|
|
|
|
func decodePatchBucketRequest(ctx context.Context, r *http.Request) (*patchBucketRequest, error) {
|
|
|
|
params := httprouter.ParamsFromContext(ctx)
|
|
|
|
id := params.ByName("id")
|
|
|
|
if id == "" {
|
2018-05-22 22:05:17 +00:00
|
|
|
return nil, kerrors.InvalidDataf("url missing id")
|
2018-05-14 16:26:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var i platform.ID
|
2018-05-16 18:59:35 +00:00
|
|
|
if err := i.DecodeFromString(id); err != nil {
|
2018-05-14 16:26:38 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var upd platform.BucketUpdate
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&upd); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &patchBucketRequest{
|
|
|
|
Update: upd,
|
|
|
|
BucketID: i,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
bucketPath = "/v1/buckets"
|
|
|
|
)
|
|
|
|
|
|
|
|
// BucketService connects to Influx via HTTP using tokens to manage buckets
|
|
|
|
type BucketService struct {
|
|
|
|
Addr string
|
|
|
|
Token string
|
|
|
|
InsecureSkipVerify bool
|
|
|
|
}
|
|
|
|
|
|
|
|
// FindBucketByID returns a single bucket by ID.
|
|
|
|
func (s *BucketService) FindBucketByID(ctx context.Context, id platform.ID) (*platform.Bucket, error) {
|
2018-05-16 18:59:35 +00:00
|
|
|
u, err := newURL(s.Addr, bucketIDPath(id))
|
2018-05-14 16:26:38 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest("GET", u.String(), nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
req.Header.Set("Authorization", s.Token)
|
|
|
|
|
|
|
|
hc := newClient(u.Scheme, s.InsecureSkipVerify)
|
|
|
|
resp, err := hc.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-05-23 18:29:01 +00:00
|
|
|
if err := CheckError(resp); err != nil {
|
|
|
|
return nil, err
|
2018-05-14 16:26:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var b platform.Bucket
|
2018-05-16 18:59:35 +00:00
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&b); err != nil {
|
2018-05-14 16:26:38 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2018-05-16 18:59:35 +00:00
|
|
|
defer resp.Body.Close()
|
2018-05-14 16:26:38 +00:00
|
|
|
|
|
|
|
return &b, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// FindBucket returns the first bucket that matches filter.
|
|
|
|
func (s *BucketService) FindBucket(ctx context.Context, filter platform.BucketFilter) (*platform.Bucket, error) {
|
|
|
|
bs, n, err := s.FindBuckets(ctx, filter)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if n == 0 {
|
2018-05-22 22:05:17 +00:00
|
|
|
return nil, errors.New("found no matching buckets")
|
2018-05-14 16:26:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return bs[0], nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// FindBuckets returns a list of buckets that match filter and the total count of matching buckets.
|
|
|
|
// Additional options provide pagination & sorting.
|
|
|
|
func (s *BucketService) FindBuckets(ctx context.Context, filter platform.BucketFilter, opt ...platform.FindOptions) ([]*platform.Bucket, int, error) {
|
|
|
|
u, err := newURL(s.Addr, bucketPath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
query := u.Query()
|
|
|
|
if filter.OrganizationID != nil {
|
|
|
|
query.Add("orgID", filter.OrganizationID.String())
|
|
|
|
}
|
2018-05-16 18:59:35 +00:00
|
|
|
if filter.Organization != nil {
|
|
|
|
query.Add("org", *filter.Organization)
|
|
|
|
}
|
2018-05-14 16:26:38 +00:00
|
|
|
if filter.ID != nil {
|
2018-05-16 18:59:35 +00:00
|
|
|
query.Add("id", filter.ID.String())
|
2018-05-14 16:26:38 +00:00
|
|
|
}
|
|
|
|
if filter.Name != nil {
|
2018-05-16 18:59:35 +00:00
|
|
|
query.Add("name", *filter.Name)
|
2018-05-14 16:26:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest("GET", u.String(), nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
req.URL.RawQuery = query.Encode()
|
|
|
|
req.Header.Set("Authorization", s.Token)
|
|
|
|
|
|
|
|
hc := newClient(u.Scheme, s.InsecureSkipVerify)
|
|
|
|
resp, err := hc.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, 0, err
|
|
|
|
}
|
|
|
|
|
2018-05-23 18:29:01 +00:00
|
|
|
if err := CheckError(resp); err != nil {
|
|
|
|
return nil, 0, err
|
2018-05-14 16:26:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var bs []*platform.Bucket
|
2018-05-16 18:59:35 +00:00
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&bs); err != nil {
|
2018-05-14 16:26:38 +00:00
|
|
|
return nil, 0, err
|
|
|
|
}
|
2018-05-16 18:59:35 +00:00
|
|
|
defer resp.Body.Close()
|
2018-05-14 16:26:38 +00:00
|
|
|
|
|
|
|
return bs, len(bs), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// CreateBucket creates a new bucket and sets b.ID with the new identifier.
|
|
|
|
func (s *BucketService) CreateBucket(ctx context.Context, b *platform.Bucket) error {
|
|
|
|
u, err := newURL(s.Addr, bucketPath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
octets, err := json.Marshal(b)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest("POST", u.String(), bytes.NewReader(octets))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
req.Header.Set("Authorization", s.Token)
|
|
|
|
|
|
|
|
hc := newClient(u.Scheme, s.InsecureSkipVerify)
|
|
|
|
|
|
|
|
resp, err := hc.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-05-23 18:29:01 +00:00
|
|
|
// TODO(jsternberg): Should this check for a 201 explicitly?
|
|
|
|
if err := CheckError(resp); err != nil {
|
|
|
|
return err
|
2018-05-14 16:26:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(b); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdateBucket updates a single bucket with changeset.
|
|
|
|
// Returns the new bucket state after update.
|
|
|
|
func (s *BucketService) UpdateBucket(ctx context.Context, id platform.ID, upd platform.BucketUpdate) (*platform.Bucket, error) {
|
2018-05-16 18:59:35 +00:00
|
|
|
u, err := newURL(s.Addr, bucketIDPath(id))
|
2018-05-14 16:26:38 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
octets, err := json.Marshal(upd)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest("PATCH", u.String(), bytes.NewReader(octets))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
req.Header.Set("Authorization", s.Token)
|
|
|
|
|
|
|
|
hc := newClient(u.Scheme, s.InsecureSkipVerify)
|
|
|
|
|
|
|
|
resp, err := hc.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-05-23 18:29:01 +00:00
|
|
|
if err := CheckError(resp); err != nil {
|
|
|
|
return nil, err
|
2018-05-14 16:26:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var b platform.Bucket
|
2018-05-16 18:59:35 +00:00
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&b); err != nil {
|
2018-05-14 16:26:38 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2018-05-16 18:59:35 +00:00
|
|
|
defer resp.Body.Close()
|
2018-05-14 16:26:38 +00:00
|
|
|
|
|
|
|
return &b, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteBucket removes a bucket by ID.
|
|
|
|
func (s *BucketService) DeleteBucket(ctx context.Context, id platform.ID) error {
|
2018-05-16 18:59:35 +00:00
|
|
|
u, err := newURL(s.Addr, bucketIDPath(id))
|
2018-05-14 16:26:38 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest("DELETE", u.String(), nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
req.Header.Set("Authorization", s.Token)
|
|
|
|
|
|
|
|
hc := newClient(u.Scheme, s.InsecureSkipVerify)
|
|
|
|
resp, err := hc.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-05-23 18:29:01 +00:00
|
|
|
return CheckError(resp)
|
2018-05-14 16:26:38 +00:00
|
|
|
}
|
|
|
|
|
2018-05-16 18:59:35 +00:00
|
|
|
func bucketIDPath(id platform.ID) string {
|
|
|
|
return path.Join(bucketPath, id.String())
|
2018-05-14 16:26:38 +00:00
|
|
|
}
|