mirror of
https://github.com/ipfs/kubo.git
synced 2025-06-30 01:52:26 +08:00
Merge pull request #4641 from ipfs/feat/cat-sessions
Use a bitswap session for 'Cat'
This commit is contained in:
@ -5,10 +5,12 @@ import (
|
|||||||
|
|
||||||
core "github.com/ipfs/go-ipfs/core"
|
core "github.com/ipfs/go-ipfs/core"
|
||||||
coreiface "github.com/ipfs/go-ipfs/core/coreapi/interface"
|
coreiface "github.com/ipfs/go-ipfs/core/coreapi/interface"
|
||||||
|
namesys "github.com/ipfs/go-ipfs/namesys"
|
||||||
ipfspath "github.com/ipfs/go-ipfs/path"
|
ipfspath "github.com/ipfs/go-ipfs/path"
|
||||||
uio "github.com/ipfs/go-ipfs/unixfs/io"
|
uio "github.com/ipfs/go-ipfs/unixfs/io"
|
||||||
|
|
||||||
cid "gx/ipfs/QmcZfnkapfECQGcLZaf9B79NRg7cRa9EnZh4LSbkCzwNvY/go-cid"
|
cid "gx/ipfs/QmcZfnkapfECQGcLZaf9B79NRg7cRa9EnZh4LSbkCzwNvY/go-cid"
|
||||||
|
ipld "gx/ipfs/Qme5bWv7wtjUNGsK2BNGVUFPKiuxWrsqrtvYwCLRw8YFES/go-ipld-format"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CoreAPI struct {
|
type CoreAPI struct {
|
||||||
@ -49,12 +51,16 @@ func (api *CoreAPI) Object() coreiface.ObjectAPI {
|
|||||||
// ResolveNode resolves the path `p` using Unixfx resolver, gets and returns the
|
// ResolveNode resolves the path `p` using Unixfx resolver, gets and returns the
|
||||||
// resolved Node.
|
// resolved Node.
|
||||||
func (api *CoreAPI) ResolveNode(ctx context.Context, p coreiface.Path) (coreiface.Node, error) {
|
func (api *CoreAPI) ResolveNode(ctx context.Context, p coreiface.Path) (coreiface.Node, error) {
|
||||||
p, err := api.ResolvePath(ctx, p)
|
return resolveNode(ctx, api.node.DAG, api.node.Namesys, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveNode(ctx context.Context, ng ipld.NodeGetter, nsys namesys.NameSystem, p coreiface.Path) (coreiface.Node, error) {
|
||||||
|
p, err := resolvePath(ctx, ng, nsys, p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
node, err := api.node.DAG.Get(ctx, p.Cid())
|
node, err := ng.Get(ctx, p.Cid())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -65,17 +71,21 @@ func (api *CoreAPI) ResolveNode(ctx context.Context, p coreiface.Path) (coreifac
|
|||||||
// resolved path.
|
// resolved path.
|
||||||
// TODO: store all of ipfspath.Resolver.ResolvePathComponents() in Path
|
// TODO: store all of ipfspath.Resolver.ResolvePathComponents() in Path
|
||||||
func (api *CoreAPI) ResolvePath(ctx context.Context, p coreiface.Path) (coreiface.Path, error) {
|
func (api *CoreAPI) ResolvePath(ctx context.Context, p coreiface.Path) (coreiface.Path, error) {
|
||||||
|
return resolvePath(ctx, api.node.DAG, api.node.Namesys, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolvePath(ctx context.Context, ng ipld.NodeGetter, nsys namesys.NameSystem, p coreiface.Path) (coreiface.Path, error) {
|
||||||
if p.Resolved() {
|
if p.Resolved() {
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
r := &ipfspath.Resolver{
|
r := &ipfspath.Resolver{
|
||||||
DAG: api.node.DAG,
|
DAG: ng,
|
||||||
ResolveOnce: uio.ResolveUnixfsOnce,
|
ResolveOnce: uio.ResolveUnixfsOnce,
|
||||||
}
|
}
|
||||||
|
|
||||||
p2 := ipfspath.FromString(p.String())
|
p2 := ipfspath.FromString(p.String())
|
||||||
node, err := core.Resolve(ctx, api.node.Namesys, r, p2)
|
node, err := core.Resolve(ctx, nsys, r, p2)
|
||||||
if err == core.ErrNoNamesys {
|
if err == core.ErrNoNamesys {
|
||||||
return nil, coreiface.ErrOffline
|
return nil, coreiface.ErrOffline
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
coreiface "github.com/ipfs/go-ipfs/core/coreapi/interface"
|
coreiface "github.com/ipfs/go-ipfs/core/coreapi/interface"
|
||||||
coreunix "github.com/ipfs/go-ipfs/core/coreunix"
|
coreunix "github.com/ipfs/go-ipfs/core/coreunix"
|
||||||
|
dag "github.com/ipfs/go-ipfs/merkledag"
|
||||||
uio "github.com/ipfs/go-ipfs/unixfs/io"
|
uio "github.com/ipfs/go-ipfs/unixfs/io"
|
||||||
|
|
||||||
cid "gx/ipfs/QmcZfnkapfECQGcLZaf9B79NRg7cRa9EnZh4LSbkCzwNvY/go-cid"
|
cid "gx/ipfs/QmcZfnkapfECQGcLZaf9B79NRg7cRa9EnZh4LSbkCzwNvY/go-cid"
|
||||||
@ -30,12 +31,14 @@ func (api *UnixfsAPI) Add(ctx context.Context, r io.Reader) (coreiface.Path, err
|
|||||||
|
|
||||||
// Cat returns the data contained by an IPFS or IPNS object(s) at path `p`.
|
// Cat returns the data contained by an IPFS or IPNS object(s) at path `p`.
|
||||||
func (api *UnixfsAPI) Cat(ctx context.Context, p coreiface.Path) (coreiface.Reader, error) {
|
func (api *UnixfsAPI) Cat(ctx context.Context, p coreiface.Path) (coreiface.Reader, error) {
|
||||||
dagnode, err := api.core().ResolveNode(ctx, p)
|
ses := dag.NewSession(ctx, api.node.DAG)
|
||||||
|
|
||||||
|
dagnode, err := resolveNode(ctx, ses, api.node.Namesys, p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := uio.NewDagReader(ctx, dagnode, api.node.DAG)
|
r, err := uio.NewDagReader(ctx, dagnode, ses)
|
||||||
if err == uio.ErrIsDir {
|
if err == uio.ErrIsDir {
|
||||||
return nil, coreiface.ErrIsDir
|
return nil, coreiface.ErrIsDir
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
41
merkledag/errservice.go
Normal file
41
merkledag/errservice.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package merkledag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
cid "gx/ipfs/QmcZfnkapfECQGcLZaf9B79NRg7cRa9EnZh4LSbkCzwNvY/go-cid"
|
||||||
|
ipld "gx/ipfs/Qme5bWv7wtjUNGsK2BNGVUFPKiuxWrsqrtvYwCLRw8YFES/go-ipld-format"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrorService implements ipld.DAGService, returning 'Err' for every call.
|
||||||
|
type ErrorService struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ ipld.DAGService = (*ErrorService)(nil)
|
||||||
|
|
||||||
|
func (cs *ErrorService) Add(ctx context.Context, nd ipld.Node) error {
|
||||||
|
return cs.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *ErrorService) AddMany(ctx context.Context, nds []ipld.Node) error {
|
||||||
|
return cs.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *ErrorService) Get(ctx context.Context, c *cid.Cid) (ipld.Node, error) {
|
||||||
|
return nil, cs.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *ErrorService) GetMany(ctx context.Context, cids []*cid.Cid) <-chan *ipld.NodeOption {
|
||||||
|
ch := make(chan *ipld.NodeOption)
|
||||||
|
close(ch)
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *ErrorService) Remove(ctx context.Context, c *cid.Cid) error {
|
||||||
|
return cs.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *ErrorService) RemoveMany(ctx context.Context, cids []*cid.Cid) error {
|
||||||
|
return cs.Err
|
||||||
|
}
|
@ -146,6 +146,11 @@ func (sg *sesGetter) GetMany(ctx context.Context, keys []*cid.Cid) <-chan *ipld.
|
|||||||
return getNodesFromBG(ctx, sg.bs, keys)
|
return getNodesFromBG(ctx, sg.bs, keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Session returns a NodeGetter using a new session for block fetches.
|
||||||
|
func (ds *dagService) Session(ctx context.Context) ipld.NodeGetter {
|
||||||
|
return &sesGetter{bserv.NewSession(ctx, ds.Blocks)}
|
||||||
|
}
|
||||||
|
|
||||||
// FetchGraph fetches all nodes that are children of the given node
|
// FetchGraph fetches all nodes that are children of the given node
|
||||||
func FetchGraph(ctx context.Context, root *cid.Cid, serv ipld.DAGService) error {
|
func FetchGraph(ctx context.Context, root *cid.Cid, serv ipld.DAGService) error {
|
||||||
var ng ipld.NodeGetter = serv
|
var ng ipld.NodeGetter = serv
|
||||||
|
20
merkledag/readonly.go
Normal file
20
merkledag/readonly.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package merkledag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
ipld "gx/ipfs/Qme5bWv7wtjUNGsK2BNGVUFPKiuxWrsqrtvYwCLRw8YFES/go-ipld-format"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrReadOnly is used when a read-only datastructure is written to.
|
||||||
|
var ErrReadOnly = fmt.Errorf("cannot write to readonly DAGService")
|
||||||
|
|
||||||
|
// NewReadOnlyDagService takes a NodeGetter, and returns a full DAGService
|
||||||
|
// implementation that returns ErrReadOnly when its 'write' methods are
|
||||||
|
// invoked.
|
||||||
|
func NewReadOnlyDagService(ng ipld.NodeGetter) ipld.DAGService {
|
||||||
|
return &ComboService{
|
||||||
|
Read: ng,
|
||||||
|
Write: &ErrorService{ErrReadOnly},
|
||||||
|
}
|
||||||
|
}
|
64
merkledag/readonly_test.go
Normal file
64
merkledag/readonly_test.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package merkledag_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/ipfs/go-ipfs/merkledag"
|
||||||
|
dstest "github.com/ipfs/go-ipfs/merkledag/test"
|
||||||
|
|
||||||
|
cid "gx/ipfs/QmcZfnkapfECQGcLZaf9B79NRg7cRa9EnZh4LSbkCzwNvY/go-cid"
|
||||||
|
ipld "gx/ipfs/Qme5bWv7wtjUNGsK2BNGVUFPKiuxWrsqrtvYwCLRw8YFES/go-ipld-format"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReadonlyProperties(t *testing.T) {
|
||||||
|
ds := dstest.Mock()
|
||||||
|
ro := NewReadOnlyDagService(ds)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
nds := []ipld.Node{
|
||||||
|
NewRawNode([]byte("foo1")),
|
||||||
|
NewRawNode([]byte("foo2")),
|
||||||
|
NewRawNode([]byte("foo3")),
|
||||||
|
NewRawNode([]byte("foo4")),
|
||||||
|
}
|
||||||
|
cids := []*cid.Cid{
|
||||||
|
nds[0].Cid(),
|
||||||
|
nds[1].Cid(),
|
||||||
|
nds[2].Cid(),
|
||||||
|
nds[3].Cid(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// add to the actual underlying datastore
|
||||||
|
if err := ds.Add(ctx, nds[2]); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := ds.Add(ctx, nds[3]); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ro.Add(ctx, nds[0]); err != ErrReadOnly {
|
||||||
|
t.Fatal("expected ErrReadOnly")
|
||||||
|
}
|
||||||
|
if err := ro.Add(ctx, nds[2]); err != ErrReadOnly {
|
||||||
|
t.Fatal("expected ErrReadOnly")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ro.AddMany(ctx, nds[0:1]); err != ErrReadOnly {
|
||||||
|
t.Fatal("expected ErrReadOnly")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ro.Remove(ctx, cids[3]); err != ErrReadOnly {
|
||||||
|
t.Fatal("expected ErrReadOnly")
|
||||||
|
}
|
||||||
|
if err := ro.RemoveMany(ctx, cids[1:2]); err != ErrReadOnly {
|
||||||
|
t.Fatal("expected ErrReadOnly")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := ro.Get(ctx, cids[0]); err != ipld.ErrNotFound {
|
||||||
|
t.Fatal("expected ErrNotFound")
|
||||||
|
}
|
||||||
|
if _, err := ro.Get(ctx, cids[3]); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
41
merkledag/rwservice.go
Normal file
41
merkledag/rwservice.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package merkledag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
cid "gx/ipfs/QmcZfnkapfECQGcLZaf9B79NRg7cRa9EnZh4LSbkCzwNvY/go-cid"
|
||||||
|
ipld "gx/ipfs/Qme5bWv7wtjUNGsK2BNGVUFPKiuxWrsqrtvYwCLRw8YFES/go-ipld-format"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ComboService implements ipld.DAGService, using 'Read' for all fetch methods,
|
||||||
|
// and 'Write' for all methods that add new objects.
|
||||||
|
type ComboService struct {
|
||||||
|
Read ipld.NodeGetter
|
||||||
|
Write ipld.DAGService
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ ipld.DAGService = (*ComboService)(nil)
|
||||||
|
|
||||||
|
func (cs *ComboService) Add(ctx context.Context, nd ipld.Node) error {
|
||||||
|
return cs.Write.Add(ctx, nd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *ComboService) AddMany(ctx context.Context, nds []ipld.Node) error {
|
||||||
|
return cs.Write.AddMany(ctx, nds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *ComboService) Get(ctx context.Context, c *cid.Cid) (ipld.Node, error) {
|
||||||
|
return cs.Read.Get(ctx, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *ComboService) GetMany(ctx context.Context, cids []*cid.Cid) <-chan *ipld.NodeOption {
|
||||||
|
return cs.Read.GetMany(ctx, cids)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *ComboService) Remove(ctx context.Context, c *cid.Cid) error {
|
||||||
|
return cs.Write.Remove(ctx, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *ComboService) RemoveMany(ctx context.Context, cids []*cid.Cid) error {
|
||||||
|
return cs.Write.RemoveMany(ctx, cids)
|
||||||
|
}
|
21
merkledag/session.go
Normal file
21
merkledag/session.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package merkledag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
ipld "gx/ipfs/Qme5bWv7wtjUNGsK2BNGVUFPKiuxWrsqrtvYwCLRw8YFES/go-ipld-format"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SessionMaker is an object that can generate a new fetching session.
|
||||||
|
type SessionMaker interface {
|
||||||
|
Session(context.Context) ipld.NodeGetter
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSession returns a session backed NodeGetter if the given NodeGetter
|
||||||
|
// implements SessionMaker.
|
||||||
|
func NewSession(ctx context.Context, g ipld.NodeGetter) ipld.NodeGetter {
|
||||||
|
if sm, ok := g.(SessionMaker); ok {
|
||||||
|
return sm.Session(ctx)
|
||||||
|
}
|
||||||
|
return g
|
||||||
|
}
|
@ -35,9 +35,9 @@ func (e ErrNoLink) Error() string {
|
|||||||
// TODO: now that this is more modular, try to unify this code with the
|
// TODO: now that this is more modular, try to unify this code with the
|
||||||
// the resolvers in namesys
|
// the resolvers in namesys
|
||||||
type Resolver struct {
|
type Resolver struct {
|
||||||
DAG ipld.DAGService
|
DAG ipld.NodeGetter
|
||||||
|
|
||||||
ResolveOnce func(ctx context.Context, ds ipld.DAGService, nd ipld.Node, names []string) (*ipld.Link, []string, error)
|
ResolveOnce func(ctx context.Context, ds ipld.NodeGetter, nd ipld.Node, names []string) (*ipld.Link, []string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBasicResolver constructs a new basic resolver.
|
// NewBasicResolver constructs a new basic resolver.
|
||||||
@ -124,7 +124,7 @@ func (s *Resolver) ResolvePath(ctx context.Context, fpath Path) (ipld.Node, erro
|
|||||||
|
|
||||||
// ResolveSingle simply resolves one hop of a path through a graph with no
|
// ResolveSingle simply resolves one hop of a path through a graph with no
|
||||||
// extra context (does not opaquely resolve through sharded nodes)
|
// extra context (does not opaquely resolve through sharded nodes)
|
||||||
func ResolveSingle(ctx context.Context, ds ipld.DAGService, nd ipld.Node, names []string) (*ipld.Link, []string, error) {
|
func ResolveSingle(ctx context.Context, ds ipld.NodeGetter, nd ipld.Node, names []string) (*ipld.Link, []string, error) {
|
||||||
return nd.ResolveLink(names)
|
return nd.ResolveLink(names)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ type ReadSeekCloser interface {
|
|||||||
|
|
||||||
// NewDagReader creates a new reader object that reads the data represented by
|
// NewDagReader creates a new reader object that reads the data represented by
|
||||||
// the given node, using the passed in DAGService for data retreival
|
// the given node, using the passed in DAGService for data retreival
|
||||||
func NewDagReader(ctx context.Context, n ipld.Node, serv ipld.DAGService) (DagReader, error) {
|
func NewDagReader(ctx context.Context, n ipld.Node, serv ipld.NodeGetter) (DagReader, error) {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case *mdag.RawNode:
|
case *mdag.RawNode:
|
||||||
return NewBufDagReader(n.RawData()), nil
|
return NewBufDagReader(n.RawData()), nil
|
||||||
|
@ -17,7 +17,7 @@ import (
|
|||||||
|
|
||||||
// DagReader provides a way to easily read the data contained in a dag.
|
// DagReader provides a way to easily read the data contained in a dag.
|
||||||
type pbDagReader struct {
|
type pbDagReader struct {
|
||||||
serv ipld.DAGService
|
serv ipld.NodeGetter
|
||||||
|
|
||||||
// the node being read
|
// the node being read
|
||||||
node *mdag.ProtoNode
|
node *mdag.ProtoNode
|
||||||
@ -51,7 +51,7 @@ type pbDagReader struct {
|
|||||||
var _ DagReader = (*pbDagReader)(nil)
|
var _ DagReader = (*pbDagReader)(nil)
|
||||||
|
|
||||||
// NewPBFileReader constructs a new PBFileReader.
|
// NewPBFileReader constructs a new PBFileReader.
|
||||||
func NewPBFileReader(ctx context.Context, n *mdag.ProtoNode, pb *ftpb.Data, serv ipld.DAGService) *pbDagReader {
|
func NewPBFileReader(ctx context.Context, n *mdag.ProtoNode, pb *ftpb.Data, serv ipld.NodeGetter) *pbDagReader {
|
||||||
fctx, cancel := context.WithCancel(ctx)
|
fctx, cancel := context.WithCancel(ctx)
|
||||||
curLinks := getLinkCids(n)
|
curLinks := getLinkCids(n)
|
||||||
return &pbDagReader{
|
return &pbDagReader{
|
||||||
|
@ -12,7 +12,7 @@ import (
|
|||||||
|
|
||||||
// ResolveUnixfsOnce resolves a single hop of a path through a graph in a
|
// ResolveUnixfsOnce resolves a single hop of a path through a graph in a
|
||||||
// unixfs context. This includes handling traversing sharded directories.
|
// unixfs context. This includes handling traversing sharded directories.
|
||||||
func ResolveUnixfsOnce(ctx context.Context, ds ipld.DAGService, nd ipld.Node, names []string) (*ipld.Link, []string, error) {
|
func ResolveUnixfsOnce(ctx context.Context, ds ipld.NodeGetter, nd ipld.Node, names []string) (*ipld.Link, []string, error) {
|
||||||
switch nd := nd.(type) {
|
switch nd := nd.(type) {
|
||||||
case *dag.ProtoNode:
|
case *dag.ProtoNode:
|
||||||
upb, err := ft.FromBytes(nd.Data())
|
upb, err := ft.FromBytes(nd.Data())
|
||||||
@ -28,7 +28,8 @@ func ResolveUnixfsOnce(ctx context.Context, ds ipld.DAGService, nd ipld.Node, na
|
|||||||
|
|
||||||
switch upb.GetType() {
|
switch upb.GetType() {
|
||||||
case ft.THAMTShard:
|
case ft.THAMTShard:
|
||||||
s, err := hamt.NewHamtFromDag(ds, nd)
|
rods := dag.NewReadOnlyDagService(ds)
|
||||||
|
s, err := hamt.NewHamtFromDag(rods, nd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user