2022-04-22 13:22:40 +00:00
|
|
|
package pilorama
|
|
|
|
|
|
|
|
import (
|
2023-04-13 12:36:20 +00:00
|
|
|
"context"
|
2024-03-11 14:55:50 +00:00
|
|
|
"errors"
|
2024-02-05 11:09:58 +00:00
|
|
|
"fmt"
|
2022-05-11 13:29:04 +00:00
|
|
|
"sort"
|
2022-10-18 11:59:32 +00:00
|
|
|
"strings"
|
2022-05-11 13:29:04 +00:00
|
|
|
|
2023-03-07 13:38:26 +00:00
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/shard/mode"
|
2023-03-21 12:43:12 +00:00
|
|
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
2022-04-22 13:22:40 +00:00
|
|
|
)
|
|
|
|
|
2024-03-11 14:55:50 +00:00
|
|
|
var errInvalidKeyFormat = errors.New("invalid format: key must be cid and treeID")
|
|
|
|
|
2022-04-22 13:22:40 +00:00
|
|
|
// memoryForest represents multiple replicating trees sharing a single storage.
|
|
|
|
type memoryForest struct {
|
|
|
|
// treeMap maps tree identifier (container ID + name) to the replicated log.
|
2023-04-26 14:07:51 +00:00
|
|
|
treeMap map[string]*memoryTree
|
2022-04-22 13:22:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var _ Forest = (*memoryForest)(nil)
|
|
|
|
|
|
|
|
// NewMemoryForest creates new empty forest.
|
|
|
|
// TODO: this function will eventually be removed and is here for debugging.
|
|
|
|
func NewMemoryForest() ForestStorage {
|
|
|
|
return &memoryForest{
|
2023-04-26 14:07:51 +00:00
|
|
|
treeMap: make(map[string]*memoryTree),
|
2022-04-22 13:22:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TreeMove implements the Forest interface.
|
2023-04-13 12:36:20 +00:00
|
|
|
func (f *memoryForest) TreeMove(_ context.Context, d CIDDescriptor, treeID string, op *Move) (*Move, error) {
|
2022-05-27 12:55:02 +00:00
|
|
|
if !d.checkValid() {
|
|
|
|
return nil, ErrInvalidCIDDescriptor
|
|
|
|
}
|
|
|
|
|
|
|
|
fullID := d.CID.String() + "/" + treeID
|
2022-04-22 13:22:40 +00:00
|
|
|
s, ok := f.treeMap[fullID]
|
|
|
|
if !ok {
|
2023-04-26 14:07:51 +00:00
|
|
|
s = newMemoryTree()
|
2022-04-22 13:22:40 +00:00
|
|
|
f.treeMap[fullID] = s
|
|
|
|
}
|
|
|
|
|
2022-05-27 12:55:02 +00:00
|
|
|
op.Time = s.timestamp(d.Position, d.Size)
|
2022-04-22 13:22:40 +00:00
|
|
|
if op.Child == RootID {
|
|
|
|
op.Child = s.findSpareID()
|
|
|
|
}
|
|
|
|
|
|
|
|
lm := s.do(op)
|
|
|
|
s.operations = append(s.operations, lm)
|
2023-01-17 13:16:50 +00:00
|
|
|
return &lm.Move, nil
|
2022-04-22 13:22:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TreeAddByPath implements the Forest interface.
|
2023-04-13 12:36:20 +00:00
|
|
|
func (f *memoryForest) TreeAddByPath(_ context.Context, d CIDDescriptor, treeID string, attr string, path []string, m []KeyValue) ([]Move, error) {
|
2022-05-27 12:55:02 +00:00
|
|
|
if !d.checkValid() {
|
|
|
|
return nil, ErrInvalidCIDDescriptor
|
|
|
|
}
|
2022-05-24 13:12:50 +00:00
|
|
|
if !isAttributeInternal(attr) {
|
|
|
|
return nil, ErrNotPathAttribute
|
|
|
|
}
|
|
|
|
|
2022-05-27 12:55:02 +00:00
|
|
|
fullID := d.CID.String() + "/" + treeID
|
2022-04-22 13:22:40 +00:00
|
|
|
s, ok := f.treeMap[fullID]
|
|
|
|
if !ok {
|
2023-04-26 14:07:51 +00:00
|
|
|
s = newMemoryTree()
|
2022-04-22 13:22:40 +00:00
|
|
|
f.treeMap[fullID] = s
|
|
|
|
}
|
|
|
|
|
|
|
|
i, node := s.getPathPrefix(attr, path)
|
2023-01-25 11:12:02 +00:00
|
|
|
lm := make([]Move, len(path)-i+1)
|
2022-04-22 13:22:40 +00:00
|
|
|
for j := i; j < len(path); j++ {
|
2023-01-17 13:16:50 +00:00
|
|
|
op := s.do(&Move{
|
2022-04-22 13:22:40 +00:00
|
|
|
Parent: node,
|
2022-05-27 12:55:02 +00:00
|
|
|
Meta: Meta{
|
|
|
|
Time: s.timestamp(d.Position, d.Size),
|
2023-10-31 11:56:55 +00:00
|
|
|
Items: []KeyValue{{Key: attr, Value: []byte(path[j])}},
|
|
|
|
},
|
2022-05-27 12:55:02 +00:00
|
|
|
Child: s.findSpareID(),
|
2022-04-22 13:22:40 +00:00
|
|
|
})
|
2023-01-17 13:16:50 +00:00
|
|
|
lm[j-i] = op.Move
|
|
|
|
node = op.Child
|
|
|
|
s.operations = append(s.operations, op)
|
2022-04-22 13:22:40 +00:00
|
|
|
}
|
2022-05-23 11:32:24 +00:00
|
|
|
|
|
|
|
mCopy := make([]KeyValue, len(m))
|
|
|
|
copy(mCopy, m)
|
2023-01-17 13:16:50 +00:00
|
|
|
op := s.do(&Move{
|
2022-04-22 13:22:40 +00:00
|
|
|
Parent: node,
|
2022-05-27 12:55:02 +00:00
|
|
|
Meta: Meta{
|
|
|
|
Time: s.timestamp(d.Position, d.Size),
|
|
|
|
Items: mCopy,
|
|
|
|
},
|
|
|
|
Child: s.findSpareID(),
|
2022-04-22 13:22:40 +00:00
|
|
|
})
|
2023-04-26 14:31:43 +00:00
|
|
|
s.operations = append(s.operations, op)
|
2023-01-17 13:16:50 +00:00
|
|
|
lm[len(lm)-1] = op.Move
|
2022-04-22 13:22:40 +00:00
|
|
|
return lm, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// TreeApply implements the Forest interface.
|
2023-04-13 12:36:20 +00:00
|
|
|
func (f *memoryForest) TreeApply(_ context.Context, cnr cid.ID, treeID string, op *Move, _ bool) error {
|
2023-03-21 12:43:12 +00:00
|
|
|
fullID := cnr.String() + "/" + treeID
|
2022-04-22 13:22:40 +00:00
|
|
|
s, ok := f.treeMap[fullID]
|
|
|
|
if !ok {
|
2023-04-26 14:07:51 +00:00
|
|
|
s = newMemoryTree()
|
2022-04-22 13:22:40 +00:00
|
|
|
f.treeMap[fullID] = s
|
|
|
|
}
|
|
|
|
|
|
|
|
return s.Apply(op)
|
|
|
|
}
|
|
|
|
|
2024-10-30 08:02:52 +00:00
|
|
|
func (f *memoryForest) TreeApplyBatch(ctx context.Context, cnr cid.ID, treeID string, ops []*Move) error {
|
|
|
|
for _, op := range ops {
|
|
|
|
if err := f.TreeApply(ctx, cnr, treeID, op, true); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-10-21 13:27:28 +00:00
|
|
|
func (f *memoryForest) Init(context.Context) error {
|
2022-04-22 13:22:40 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-02-09 06:17:17 +00:00
|
|
|
func (f *memoryForest) Open(context.Context, mode.Mode) error {
|
2022-04-22 13:22:40 +00:00
|
|
|
return nil
|
|
|
|
}
|
2023-10-31 11:56:55 +00:00
|
|
|
|
2024-10-21 08:56:38 +00:00
|
|
|
func (f *memoryForest) SetMode(context.Context, mode.Mode) error {
|
2022-07-05 04:55:46 +00:00
|
|
|
return nil
|
|
|
|
}
|
2023-10-31 11:56:55 +00:00
|
|
|
|
2024-10-21 13:27:28 +00:00
|
|
|
func (f *memoryForest) Close(context.Context) error {
|
2022-04-22 13:22:40 +00:00
|
|
|
return nil
|
|
|
|
}
|
2023-06-07 11:39:03 +00:00
|
|
|
func (f *memoryForest) SetParentID(string) {}
|
2022-04-22 13:22:40 +00:00
|
|
|
|
|
|
|
// TreeGetByPath implements the Forest interface.
|
2023-04-13 12:36:20 +00:00
|
|
|
func (f *memoryForest) TreeGetByPath(_ context.Context, cid cid.ID, treeID string, attr string, path []string, latest bool) ([]Node, error) {
|
2022-05-24 13:12:50 +00:00
|
|
|
if !isAttributeInternal(attr) {
|
|
|
|
return nil, ErrNotPathAttribute
|
|
|
|
}
|
|
|
|
|
2022-04-22 13:22:40 +00:00
|
|
|
fullID := cid.String() + "/" + treeID
|
|
|
|
s, ok := f.treeMap[fullID]
|
|
|
|
if !ok {
|
|
|
|
return nil, ErrTreeNotFound
|
|
|
|
}
|
|
|
|
|
2023-04-26 14:07:51 +00:00
|
|
|
return s.getByPath(attr, path, latest), nil
|
2022-04-22 13:22:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TreeGetMeta implements the Forest interface.
|
2023-04-13 12:36:20 +00:00
|
|
|
func (f *memoryForest) TreeGetMeta(_ context.Context, cid cid.ID, treeID string, nodeID Node) (Meta, Node, error) {
|
2022-04-22 13:22:40 +00:00
|
|
|
fullID := cid.String() + "/" + treeID
|
|
|
|
s, ok := f.treeMap[fullID]
|
|
|
|
if !ok {
|
2022-05-20 08:41:37 +00:00
|
|
|
return Meta{}, 0, ErrTreeNotFound
|
2022-04-22 13:22:40 +00:00
|
|
|
}
|
|
|
|
|
2023-04-26 14:12:04 +00:00
|
|
|
return s.infoMap[nodeID].Meta, s.infoMap[nodeID].Parent, nil
|
2022-04-22 13:22:40 +00:00
|
|
|
}
|
2022-04-29 10:06:10 +00:00
|
|
|
|
2024-03-28 12:53:26 +00:00
|
|
|
// TreeSortedByFilename implements the Forest interface.
|
2024-07-10 06:30:01 +00:00
|
|
|
func (f *memoryForest) TreeSortedByFilename(_ context.Context, cid cid.ID, treeID string, nodeIDs MultiNode, start *string, count int) ([]MultiNodeInfo, *string, error) {
|
2024-03-28 12:53:26 +00:00
|
|
|
fullID := cid.String() + "/" + treeID
|
|
|
|
s, ok := f.treeMap[fullID]
|
|
|
|
if !ok {
|
2024-04-04 07:40:21 +00:00
|
|
|
return nil, start, ErrTreeNotFound
|
2024-03-28 12:53:26 +00:00
|
|
|
}
|
|
|
|
if count == 0 {
|
|
|
|
return nil, start, nil
|
|
|
|
}
|
|
|
|
|
2024-07-10 06:30:01 +00:00
|
|
|
var res []NodeInfo
|
|
|
|
|
|
|
|
for _, nodeID := range nodeIDs {
|
|
|
|
children := s.tree.getChildren(nodeID)
|
|
|
|
for _, childID := range children {
|
|
|
|
var found bool
|
|
|
|
for _, kv := range s.infoMap[childID].Meta.Items {
|
|
|
|
if kv.Key == AttributeFilename {
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
2024-04-04 07:40:21 +00:00
|
|
|
}
|
2024-07-10 06:30:01 +00:00
|
|
|
if !found {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
res = append(res, NodeInfo{
|
|
|
|
ID: childID,
|
|
|
|
Meta: s.infoMap[childID].Meta,
|
|
|
|
ParentID: s.infoMap[childID].Parent,
|
|
|
|
})
|
2024-04-04 07:40:21 +00:00
|
|
|
}
|
2024-03-28 12:53:26 +00:00
|
|
|
}
|
|
|
|
if len(res) == 0 {
|
2024-07-10 06:30:01 +00:00
|
|
|
return nil, start, nil
|
2024-03-28 12:53:26 +00:00
|
|
|
}
|
|
|
|
|
2024-11-06 07:34:16 +00:00
|
|
|
sortByFilename(res)
|
2024-07-10 06:30:01 +00:00
|
|
|
|
|
|
|
r := mergeNodeInfos(res)
|
|
|
|
for i := range r {
|
|
|
|
if start == nil || string(findAttr(r[i].Meta, AttributeFilename)) > *start {
|
2024-03-28 12:53:26 +00:00
|
|
|
finish := i + count
|
|
|
|
if len(res) < finish {
|
|
|
|
finish = len(res)
|
|
|
|
}
|
2024-07-10 06:30:01 +00:00
|
|
|
last := string(findAttr(r[finish-1].Meta, AttributeFilename))
|
|
|
|
return r[i:finish], &last, nil
|
2024-03-28 12:53:26 +00:00
|
|
|
}
|
|
|
|
}
|
2024-04-04 07:40:21 +00:00
|
|
|
last := string(res[len(res)-1].Meta.GetAttr(AttributeFilename))
|
|
|
|
return nil, &last, nil
|
2024-03-28 12:53:26 +00:00
|
|
|
}
|
|
|
|
|
2022-04-29 10:06:10 +00:00
|
|
|
// TreeGetChildren implements the Forest interface.
|
2023-07-11 08:39:17 +00:00
|
|
|
func (f *memoryForest) TreeGetChildren(_ context.Context, cid cid.ID, treeID string, nodeID Node) ([]NodeInfo, error) {
|
2022-04-29 10:06:10 +00:00
|
|
|
fullID := cid.String() + "/" + treeID
|
|
|
|
s, ok := f.treeMap[fullID]
|
|
|
|
if !ok {
|
|
|
|
return nil, ErrTreeNotFound
|
|
|
|
}
|
|
|
|
|
2023-04-26 12:23:07 +00:00
|
|
|
children := s.tree.getChildren(nodeID)
|
2023-07-11 08:39:17 +00:00
|
|
|
res := make([]NodeInfo, 0, len(children))
|
|
|
|
for _, childID := range children {
|
|
|
|
res = append(res, NodeInfo{
|
|
|
|
ID: childID,
|
|
|
|
Meta: s.infoMap[childID].Meta,
|
|
|
|
ParentID: s.infoMap[childID].Parent,
|
|
|
|
})
|
|
|
|
}
|
2022-04-29 10:06:10 +00:00
|
|
|
return res, nil
|
|
|
|
}
|
2022-05-11 13:29:04 +00:00
|
|
|
|
|
|
|
// TreeGetOpLog implements the pilorama.Forest interface.
|
2023-04-13 12:36:20 +00:00
|
|
|
func (f *memoryForest) TreeGetOpLog(_ context.Context, cid cid.ID, treeID string, height uint64) (Move, error) {
|
2022-05-11 13:29:04 +00:00
|
|
|
fullID := cid.String() + "/" + treeID
|
|
|
|
s, ok := f.treeMap[fullID]
|
|
|
|
if !ok {
|
|
|
|
return Move{}, ErrTreeNotFound
|
|
|
|
}
|
|
|
|
|
|
|
|
n := sort.Search(len(s.operations), func(i int) bool {
|
|
|
|
return s.operations[i].Time >= height
|
|
|
|
})
|
|
|
|
if n == len(s.operations) {
|
|
|
|
return Move{}, nil
|
|
|
|
}
|
|
|
|
return s.operations[n].Move, nil
|
|
|
|
}
|
2022-09-07 08:46:13 +00:00
|
|
|
|
|
|
|
// TreeDrop implements the pilorama.Forest interface.
|
2023-04-13 12:36:20 +00:00
|
|
|
func (f *memoryForest) TreeDrop(_ context.Context, cid cid.ID, treeID string) error {
|
2022-11-08 12:32:38 +00:00
|
|
|
cidStr := cid.String()
|
|
|
|
if treeID == "" {
|
|
|
|
for k := range f.treeMap {
|
|
|
|
if strings.HasPrefix(k, cidStr) {
|
|
|
|
delete(f.treeMap, k)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
fullID := cidStr + "/" + treeID
|
|
|
|
_, ok := f.treeMap[fullID]
|
|
|
|
if !ok {
|
|
|
|
return ErrTreeNotFound
|
|
|
|
}
|
|
|
|
delete(f.treeMap, fullID)
|
2022-09-07 08:46:13 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2022-10-18 11:59:32 +00:00
|
|
|
|
2022-10-18 12:49:40 +00:00
|
|
|
// TreeList implements the pilorama.Forest interface.
|
2023-04-13 12:36:20 +00:00
|
|
|
func (f *memoryForest) TreeList(_ context.Context, cid cid.ID) ([]string, error) {
|
2022-10-18 11:59:32 +00:00
|
|
|
var res []string
|
|
|
|
cidStr := cid.EncodeToString()
|
|
|
|
|
|
|
|
for k := range f.treeMap {
|
|
|
|
cidAndTree := strings.Split(k, "/")
|
|
|
|
if cidAndTree[0] != cidStr {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
res = append(res, cidAndTree[1])
|
|
|
|
}
|
|
|
|
|
|
|
|
return res, nil
|
|
|
|
}
|
2022-10-06 16:06:19 +00:00
|
|
|
|
2023-06-13 08:26:59 +00:00
|
|
|
func (f *memoryForest) TreeHeight(_ context.Context, cid cid.ID, treeID string) (uint64, error) {
|
|
|
|
fullID := cid.EncodeToString() + "/" + treeID
|
|
|
|
tree, ok := f.treeMap[fullID]
|
|
|
|
if !ok {
|
|
|
|
return 0, ErrTreeNotFound
|
|
|
|
}
|
|
|
|
return tree.operations[len(tree.operations)-1].Time, nil
|
|
|
|
}
|
|
|
|
|
2022-10-06 16:06:19 +00:00
|
|
|
// TreeExists implements the pilorama.Forest interface.
|
2023-04-13 12:36:20 +00:00
|
|
|
func (f *memoryForest) TreeExists(_ context.Context, cid cid.ID, treeID string) (bool, error) {
|
2022-10-06 16:06:19 +00:00
|
|
|
fullID := cid.EncodeToString() + "/" + treeID
|
|
|
|
_, ok := f.treeMap[fullID]
|
|
|
|
return ok, nil
|
|
|
|
}
|
2023-01-25 10:25:45 +00:00
|
|
|
|
|
|
|
// TreeUpdateLastSyncHeight implements the pilorama.Forest interface.
|
2023-04-13 12:36:20 +00:00
|
|
|
func (f *memoryForest) TreeUpdateLastSyncHeight(_ context.Context, cid cid.ID, treeID string, height uint64) error {
|
2023-01-25 10:25:45 +00:00
|
|
|
fullID := cid.EncodeToString() + "/" + treeID
|
|
|
|
t, ok := f.treeMap[fullID]
|
|
|
|
if !ok {
|
|
|
|
return ErrTreeNotFound
|
|
|
|
}
|
|
|
|
t.syncHeight = height
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// TreeLastSyncHeight implements the pilorama.Forest interface.
|
2023-04-13 12:36:20 +00:00
|
|
|
func (f *memoryForest) TreeLastSyncHeight(_ context.Context, cid cid.ID, treeID string) (uint64, error) {
|
2023-01-25 10:25:45 +00:00
|
|
|
fullID := cid.EncodeToString() + "/" + treeID
|
|
|
|
t, ok := f.treeMap[fullID]
|
|
|
|
if !ok {
|
|
|
|
return 0, ErrTreeNotFound
|
|
|
|
}
|
|
|
|
return t.syncHeight, nil
|
|
|
|
}
|
2024-02-05 11:09:58 +00:00
|
|
|
|
|
|
|
// TreeListTrees implements Forest.
|
|
|
|
func (f *memoryForest) TreeListTrees(_ context.Context, prm TreeListTreesPrm) (*TreeListTreesResult, error) {
|
|
|
|
batchSize := prm.BatchSize
|
|
|
|
if batchSize <= 0 {
|
|
|
|
batchSize = treeListTreesBatchSizeDefault
|
|
|
|
}
|
|
|
|
tmpSlice := make([]string, 0, len(f.treeMap))
|
|
|
|
for k := range f.treeMap {
|
|
|
|
tmpSlice = append(tmpSlice, k)
|
|
|
|
}
|
|
|
|
sort.Strings(tmpSlice)
|
|
|
|
var idx int
|
|
|
|
if len(prm.NextPageToken) > 0 {
|
|
|
|
last := string(prm.NextPageToken)
|
|
|
|
idx, _ = sort.Find(len(tmpSlice), func(i int) int {
|
|
|
|
return -1 * strings.Compare(tmpSlice[i], last)
|
|
|
|
})
|
|
|
|
if idx == len(tmpSlice) {
|
|
|
|
return &TreeListTreesResult{}, nil
|
|
|
|
}
|
|
|
|
if tmpSlice[idx] == last {
|
|
|
|
idx++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var result TreeListTreesResult
|
|
|
|
for idx < len(tmpSlice) {
|
|
|
|
cidAndTree := strings.Split(tmpSlice[idx], "/")
|
|
|
|
if len(cidAndTree) != 2 {
|
2024-03-11 14:55:50 +00:00
|
|
|
return nil, errInvalidKeyFormat
|
2024-02-05 11:09:58 +00:00
|
|
|
}
|
|
|
|
var contID cid.ID
|
|
|
|
if err := contID.DecodeString(cidAndTree[0]); err != nil {
|
|
|
|
return nil, fmt.Errorf("invalid format: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
result.Items = append(result.Items, ContainerIDTreeID{
|
|
|
|
CID: contID,
|
|
|
|
TreeID: cidAndTree[1],
|
|
|
|
})
|
|
|
|
|
|
|
|
if len(result.Items) == batchSize {
|
|
|
|
result.NextPageToken = []byte(tmpSlice[idx])
|
|
|
|
break
|
|
|
|
}
|
|
|
|
idx++
|
|
|
|
}
|
|
|
|
return &result, nil
|
|
|
|
}
|
2024-02-06 10:59:50 +00:00
|
|
|
|
|
|
|
// TreeApplyStream implements ForestStorage.
|
|
|
|
func (f *memoryForest) TreeApplyStream(ctx context.Context, cnr cid.ID, treeID string, source <-chan *Move) error {
|
|
|
|
fullID := cnr.String() + "/" + treeID
|
|
|
|
s, ok := f.treeMap[fullID]
|
|
|
|
if !ok {
|
|
|
|
s = newMemoryTree()
|
|
|
|
f.treeMap[fullID] = s
|
|
|
|
}
|
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return ctx.Err()
|
|
|
|
case m, ok := <-source:
|
|
|
|
if !ok {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if e := s.Apply(m); e != nil {
|
|
|
|
return e
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|