mirror of
https://github.com/ceph/go-ceph
synced 2024-12-22 06:10:09 +00:00
cephfs: add path-based xattr functions
This change adds wrappers for: * ceph_getxattr * ceph_listxattr * ceph_removexattr * ceph_setxattr As well as: * ceph_lgetxattr * ceph_llistxattr * ceph_lremovexattr * ceph_lsetxattr Signed-off-by: John Mulligan <jmulligan@redhat.com>
This commit is contained in:
parent
80ce962b68
commit
f020a3ffac
283
cephfs/path_xattr.go
Normal file
283
cephfs/path_xattr.go
Normal file
@ -0,0 +1,283 @@
|
||||
package cephfs
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lcephfs
|
||||
#cgo CPPFLAGS: -D_FILE_OFFSET_BITS=64
|
||||
#define _GNU_SOURCE
|
||||
#include <stdlib.h>
|
||||
#include <cephfs/libcephfs.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/ceph/go-ceph/internal/cutil"
|
||||
"github.com/ceph/go-ceph/internal/retry"
|
||||
)
|
||||
|
||||
// SetXattr sets an extended attribute on the file at the supplied path.
|
||||
//
|
||||
// NOTE: Attempting to set an xattr value with an empty value may cause
|
||||
// the xattr to be unset. Please refer to https://tracker.ceph.com/issues/46084
|
||||
//
|
||||
// Implements:
|
||||
// int ceph_setxattr(struct ceph_mount_info *cmount, const char *path, const char *name,
|
||||
// const void *value, size_t size, int flags);
|
||||
func (mount *MountInfo) SetXattr(path, name string, value []byte, flags XattrFlags) error {
|
||||
if err := mount.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if name == "" {
|
||||
return errInvalid
|
||||
}
|
||||
var vptr unsafe.Pointer
|
||||
if len(value) > 0 {
|
||||
vptr = unsafe.Pointer(&value[0])
|
||||
}
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
cName := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(cName))
|
||||
|
||||
ret := C.ceph_setxattr(
|
||||
mount.mount,
|
||||
cPath,
|
||||
cName,
|
||||
vptr,
|
||||
C.size_t(len(value)),
|
||||
C.int(flags))
|
||||
return getError(ret)
|
||||
}
|
||||
|
||||
// GetXattr gets an extended attribute from the file at the supplied path.
|
||||
//
|
||||
// Implements:
|
||||
// int ceph_getxattr(struct ceph_mount_info *cmount, const char *path, const char *name,
|
||||
// void *value, size_t size);
|
||||
func (mount *MountInfo) GetXattr(path, name string) ([]byte, error) {
|
||||
if err := mount.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if name == "" {
|
||||
return nil, errInvalid
|
||||
}
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
cName := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(cName))
|
||||
|
||||
var (
|
||||
ret C.int
|
||||
err error
|
||||
buf []byte
|
||||
)
|
||||
// range from 1k to 64KiB
|
||||
retry.WithSizes(1024, 1<<16, func(size int) retry.Hint {
|
||||
buf = make([]byte, size)
|
||||
ret = C.ceph_getxattr(
|
||||
mount.mount,
|
||||
cPath,
|
||||
cName,
|
||||
unsafe.Pointer(&buf[0]),
|
||||
C.size_t(size))
|
||||
err = getErrorIfNegative(ret)
|
||||
return retry.DoubleSize.If(err == errRange)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf[:ret], nil
|
||||
}
|
||||
|
||||
// ListXattr returns a slice containing strings for the name of each xattr set
|
||||
// on the file at the supplied path.
|
||||
//
|
||||
// Implements:
|
||||
// int ceph_listxattr(struct ceph_mount_info *cmount, const char *path, char *list, size_t size);
|
||||
func (mount *MountInfo) ListXattr(path string) ([]string, error) {
|
||||
if err := mount.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
|
||||
var (
|
||||
ret C.int
|
||||
err error
|
||||
buf []byte
|
||||
)
|
||||
// range from 1k to 64KiB
|
||||
retry.WithSizes(1024, 1<<16, func(size int) retry.Hint {
|
||||
buf = make([]byte, size)
|
||||
ret = C.ceph_listxattr(
|
||||
mount.mount,
|
||||
cPath,
|
||||
(*C.char)(unsafe.Pointer(&buf[0])),
|
||||
C.size_t(size))
|
||||
err = getErrorIfNegative(ret)
|
||||
return retry.DoubleSize.If(err == errRange)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
names := cutil.SplitSparseBuffer(buf[:ret])
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// RemoveXattr removes the named xattr from the open file.
|
||||
//
|
||||
// Implements:
|
||||
// int ceph_removexattr(struct ceph_mount_info *cmount, const char *path, const char *name);
|
||||
func (mount *MountInfo) RemoveXattr(path, name string) error {
|
||||
if err := mount.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if name == "" {
|
||||
return errInvalid
|
||||
}
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
cName := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(cName))
|
||||
|
||||
ret := C.ceph_removexattr(
|
||||
mount.mount,
|
||||
cPath,
|
||||
cName)
|
||||
return getError(ret)
|
||||
}
|
||||
|
||||
// LsetXattr sets an extended attribute on the file at the supplied path.
|
||||
//
|
||||
// NOTE: Attempting to set an xattr value with an empty value may cause
|
||||
// the xattr to be unset. Please refer to https://tracker.ceph.com/issues/46084
|
||||
//
|
||||
// Implements:
|
||||
// int ceph_lsetxattr(struct ceph_mount_info *cmount, const char *path, const char *name,
|
||||
// const void *value, size_t size, int flags);
|
||||
func (mount *MountInfo) LsetXattr(path, name string, value []byte, flags XattrFlags) error {
|
||||
if err := mount.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if name == "" {
|
||||
return errInvalid
|
||||
}
|
||||
var vptr unsafe.Pointer
|
||||
if len(value) > 0 {
|
||||
vptr = unsafe.Pointer(&value[0])
|
||||
}
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
cName := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(cName))
|
||||
|
||||
ret := C.ceph_lsetxattr(
|
||||
mount.mount,
|
||||
cPath,
|
||||
cName,
|
||||
vptr,
|
||||
C.size_t(len(value)),
|
||||
C.int(flags))
|
||||
return getError(ret)
|
||||
}
|
||||
|
||||
// LgetXattr gets an extended attribute from the file at the supplied path.
|
||||
//
|
||||
// Implements:
|
||||
// int ceph_lgetxattr(struct ceph_mount_info *cmount, const char *path, const char *name,
|
||||
// void *value, size_t size);
|
||||
func (mount *MountInfo) LgetXattr(path, name string) ([]byte, error) {
|
||||
if err := mount.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if name == "" {
|
||||
return nil, errInvalid
|
||||
}
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
cName := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(cName))
|
||||
|
||||
var (
|
||||
ret C.int
|
||||
err error
|
||||
buf []byte
|
||||
)
|
||||
// range from 1k to 64KiB
|
||||
retry.WithSizes(1024, 1<<16, func(size int) retry.Hint {
|
||||
buf = make([]byte, size)
|
||||
ret = C.ceph_lgetxattr(
|
||||
mount.mount,
|
||||
cPath,
|
||||
cName,
|
||||
unsafe.Pointer(&buf[0]),
|
||||
C.size_t(size))
|
||||
err = getErrorIfNegative(ret)
|
||||
return retry.DoubleSize.If(err == errRange)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf[:ret], nil
|
||||
}
|
||||
|
||||
// LlistXattr returns a slice containing strings for the name of each xattr set
|
||||
// on the file at the supplied path.
|
||||
//
|
||||
// Implements:
|
||||
// int ceph_llistxattr(struct ceph_mount_info *cmount, const char *path, char *list, size_t size);
|
||||
func (mount *MountInfo) LlistXattr(path string) ([]string, error) {
|
||||
if err := mount.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
|
||||
var (
|
||||
ret C.int
|
||||
err error
|
||||
buf []byte
|
||||
)
|
||||
// range from 1k to 64KiB
|
||||
retry.WithSizes(1024, 1<<16, func(size int) retry.Hint {
|
||||
buf = make([]byte, size)
|
||||
ret = C.ceph_llistxattr(
|
||||
mount.mount,
|
||||
cPath,
|
||||
(*C.char)(unsafe.Pointer(&buf[0])),
|
||||
C.size_t(size))
|
||||
err = getErrorIfNegative(ret)
|
||||
return retry.DoubleSize.If(err == errRange)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
names := cutil.SplitSparseBuffer(buf[:ret])
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// LremoveXattr removes the named xattr from the open file.
|
||||
//
|
||||
// Implements:
|
||||
// int ceph_lremovexattr(struct ceph_mount_info *cmount, const char *path, const char *name);
|
||||
func (mount *MountInfo) LremoveXattr(path, name string) error {
|
||||
if err := mount.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if name == "" {
|
||||
return errInvalid
|
||||
}
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
cName := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(cName))
|
||||
|
||||
ret := C.ceph_lremovexattr(
|
||||
mount.mount,
|
||||
cPath,
|
||||
cName)
|
||||
return getError(ret)
|
||||
}
|
298
cephfs/path_xattr_test.go
Normal file
298
cephfs/path_xattr_test.go
Normal file
@ -0,0 +1,298 @@
|
||||
package cephfs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetSetXattrPath(t *testing.T) {
|
||||
mount := fsConnect(t)
|
||||
defer fsDisconnect(t, mount)
|
||||
fname := "TestGetSetXattrPath.txt"
|
||||
|
||||
f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, f1.Close())
|
||||
defer func() {
|
||||
assert.NoError(t, mount.Unlink(fname))
|
||||
}()
|
||||
|
||||
for _, s := range xattrSamples {
|
||||
t.Run("roundTrip-"+s.name, func(t *testing.T) {
|
||||
err := mount.SetXattr(fname, s.name, s.value, XattrDefault)
|
||||
assert.NoError(t, err)
|
||||
b, err := mount.GetXattr(fname, s.name)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, s.value, b)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("missingXattrOnGet", func(t *testing.T) {
|
||||
_, err := mount.GetXattr(fname, "user.never-set")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("emptyNameGet", func(t *testing.T) {
|
||||
_, err := mount.GetXattr(fname, "")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("emptyNameSet", func(t *testing.T) {
|
||||
err := mount.SetXattr(fname, "", []byte("foo"), XattrDefault)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("invalidMount", func(t *testing.T) {
|
||||
m := &MountInfo{}
|
||||
err := m.SetXattr(fname, xattrSamples[0].name, xattrSamples[0].value, XattrDefault)
|
||||
assert.Error(t, err)
|
||||
_, err = m.GetXattr(fname, xattrSamples[0].name)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListXattrPath(t *testing.T) {
|
||||
mount := fsConnect(t)
|
||||
defer fsDisconnect(t, mount)
|
||||
fname := "TestListXattrPath.txt"
|
||||
|
||||
f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, f1.Close())
|
||||
defer func() {
|
||||
assert.NoError(t, mount.Unlink(fname))
|
||||
}()
|
||||
|
||||
t.Run("listXattrs1", func(t *testing.T) {
|
||||
for _, s := range xattrSamples[:1] {
|
||||
err := mount.SetXattr(fname, s.name, s.value, XattrDefault)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
xl, err := mount.ListXattr(fname)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, xl, 1)
|
||||
assert.Contains(t, xl, xattrSamples[0].name)
|
||||
})
|
||||
|
||||
t.Run("listXattrs2", func(t *testing.T) {
|
||||
for _, s := range xattrSamples {
|
||||
err := mount.SetXattr(fname, s.name, s.value, XattrDefault)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
xl, err := mount.ListXattr(fname)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, xl, 3)
|
||||
assert.Contains(t, xl, xattrSamples[0].name)
|
||||
assert.Contains(t, xl, xattrSamples[1].name)
|
||||
assert.Contains(t, xl, xattrSamples[2].name)
|
||||
})
|
||||
|
||||
t.Run("invalidMount", func(t *testing.T) {
|
||||
m := &MountInfo{}
|
||||
_, err := m.ListXattr(fname)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemoveXattrPath(t *testing.T) {
|
||||
mount := fsConnect(t)
|
||||
defer fsDisconnect(t, mount)
|
||||
fname := "TestRemoveXattrPath.txt"
|
||||
|
||||
f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, f1.Close())
|
||||
defer func() {
|
||||
assert.NoError(t, mount.Unlink(fname))
|
||||
}()
|
||||
|
||||
t.Run("removeXattr", func(t *testing.T) {
|
||||
s := xattrSamples[0]
|
||||
err := mount.SetXattr(fname, s.name, s.value, XattrDefault)
|
||||
err = mount.RemoveXattr(fname, s.name)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("removeMissingXattr", func(t *testing.T) {
|
||||
s := xattrSamples[1]
|
||||
err := mount.RemoveXattr(fname, s.name)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("emptyName", func(t *testing.T) {
|
||||
err := mount.RemoveXattr(fname, "")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("invalidMount", func(t *testing.T) {
|
||||
m := &MountInfo{}
|
||||
err := m.RemoveXattr(fname, xattrSamples[0].name)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSetXattrLinkPath(t *testing.T) {
|
||||
mount := fsConnect(t)
|
||||
defer fsDisconnect(t, mount)
|
||||
fname := "TestGetSetXattrLinkPath.txt"
|
||||
lname := "TestGetSetXattrLinkPath.lnk"
|
||||
|
||||
f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, f1.Close())
|
||||
err = mount.Symlink(fname, lname)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
assert.NoError(t, mount.Unlink(fname))
|
||||
assert.NoError(t, mount.Unlink(lname))
|
||||
}()
|
||||
|
||||
for _, s := range xattrSamples {
|
||||
t.Run("roundTrip-"+s.name, func(t *testing.T) {
|
||||
err := mount.LsetXattr(lname, s.name, s.value, XattrDefault)
|
||||
assert.NoError(t, err)
|
||||
b, err := mount.LgetXattr(lname, s.name)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, s.value, b)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("linkVsFile", func(t *testing.T) {
|
||||
s := xattrSamples[0]
|
||||
err := mount.LsetXattr(lname, s.name, s.value, XattrDefault)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// not on the file
|
||||
err = mount.LremoveXattr(fname, s.name)
|
||||
assert.Error(t, err)
|
||||
// on the link
|
||||
err = mount.LremoveXattr(lname, s.name)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("missingXattrOnGet", func(t *testing.T) {
|
||||
_, err := mount.LgetXattr(lname, "user.never-set")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("emptyNameGet", func(t *testing.T) {
|
||||
_, err := mount.LgetXattr(lname, "")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("emptyNameSet", func(t *testing.T) {
|
||||
err := mount.LsetXattr(lname, "", []byte("foo"), XattrDefault)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("invalidMount", func(t *testing.T) {
|
||||
m := &MountInfo{}
|
||||
err := m.LsetXattr(lname, xattrSamples[0].name, xattrSamples[0].value, XattrDefault)
|
||||
assert.Error(t, err)
|
||||
_, err = m.LgetXattr(lname, xattrSamples[0].name)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListXattrLinkPath(t *testing.T) {
|
||||
mount := fsConnect(t)
|
||||
defer fsDisconnect(t, mount)
|
||||
fname := "TestListXattrLinkPath.txt"
|
||||
lname := "TestListXattrLinkPath.lnk"
|
||||
|
||||
f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, f1.Close())
|
||||
err = mount.Symlink(fname, lname)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
assert.NoError(t, mount.Unlink(fname))
|
||||
assert.NoError(t, mount.Unlink(lname))
|
||||
}()
|
||||
|
||||
t.Run("listXattrs1", func(t *testing.T) {
|
||||
for _, s := range xattrSamples[:1] {
|
||||
err := mount.LsetXattr(lname, s.name, s.value, XattrDefault)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// not on the file
|
||||
xl, err := mount.LlistXattr(fname)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, xl, 0)
|
||||
// on the link
|
||||
xl, err = mount.LlistXattr(lname)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, xl, 1)
|
||||
assert.Contains(t, xl, xattrSamples[0].name)
|
||||
})
|
||||
|
||||
t.Run("listXattrs2", func(t *testing.T) {
|
||||
for _, s := range xattrSamples {
|
||||
err := mount.LsetXattr(lname, s.name, s.value, XattrDefault)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
xl, err := mount.LlistXattr(lname)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, xl, 3)
|
||||
assert.Contains(t, xl, xattrSamples[0].name)
|
||||
assert.Contains(t, xl, xattrSamples[1].name)
|
||||
assert.Contains(t, xl, xattrSamples[2].name)
|
||||
})
|
||||
|
||||
t.Run("invalidMount", func(t *testing.T) {
|
||||
m := &MountInfo{}
|
||||
_, err := m.LlistXattr(lname)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemoveXattrLinkPath(t *testing.T) {
|
||||
mount := fsConnect(t)
|
||||
defer fsDisconnect(t, mount)
|
||||
fname := "TestRemoveXattrLinkPath.txt"
|
||||
lname := "TestRemoveXattrLinkPath.lnk"
|
||||
|
||||
f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, f1.Close())
|
||||
err = mount.Symlink(fname, lname)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
assert.NoError(t, mount.Unlink(fname))
|
||||
assert.NoError(t, mount.Unlink(lname))
|
||||
}()
|
||||
|
||||
t.Run("removeXattr", func(t *testing.T) {
|
||||
s := xattrSamples[0]
|
||||
err := mount.LsetXattr(lname, s.name, s.value, XattrDefault)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// not on the file
|
||||
err = mount.LremoveXattr(fname, s.name)
|
||||
assert.Error(t, err)
|
||||
// on the link
|
||||
err = mount.LremoveXattr(lname, s.name)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("removeMissingXattr", func(t *testing.T) {
|
||||
s := xattrSamples[1]
|
||||
err := mount.LremoveXattr(lname, s.name)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("emptyName", func(t *testing.T) {
|
||||
err := mount.LremoveXattr(lname, "")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("invalidMount", func(t *testing.T) {
|
||||
m := &MountInfo{}
|
||||
err := m.LremoveXattr(lname, xattrSamples[0].name)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user