2021-08-19 06:55:22 +00:00
|
|
|
package layer
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2021-09-02 08:40:41 +00:00
|
|
|
"math"
|
2021-08-19 06:55:22 +00:00
|
|
|
"sort"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
2021-09-10 06:56:56 +00:00
|
|
|
"github.com/nspcc-dev/neofs-s3-gw/api/data"
|
2021-08-19 06:55:22 +00:00
|
|
|
"github.com/nspcc-dev/neofs-s3-gw/api/errors"
|
2022-02-08 16:54:04 +00:00
|
|
|
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
2021-08-19 06:55:22 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type objectVersions struct {
|
|
|
|
name string
|
2021-09-10 06:56:56 +00:00
|
|
|
objects []*data.ObjectInfo
|
2021-08-19 06:55:22 +00:00
|
|
|
addList []string
|
|
|
|
delList []string
|
|
|
|
isSorted bool
|
|
|
|
}
|
|
|
|
|
2022-01-18 09:40:41 +00:00
|
|
|
func FromUnversioned() VersionOption {
|
|
|
|
return func(options *versionOptions) {
|
|
|
|
options.unversioned = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type VersionOption func(*versionOptions)
|
|
|
|
|
|
|
|
type versionOptions struct {
|
|
|
|
unversioned bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func formVersionOptions(opts ...VersionOption) *versionOptions {
|
|
|
|
options := &versionOptions{}
|
|
|
|
for _, opt := range opts {
|
|
|
|
opt(options)
|
|
|
|
}
|
|
|
|
return options
|
|
|
|
}
|
|
|
|
|
2021-08-19 06:55:22 +00:00
|
|
|
const (
|
2021-11-25 15:08:02 +00:00
|
|
|
VersionsDeleteMarkAttr = "S3-Versions-delete-mark"
|
|
|
|
DelMarkFullObject = "*"
|
|
|
|
|
2021-08-19 06:55:22 +00:00
|
|
|
unversionedObjectVersionID = "null"
|
|
|
|
objectSystemAttributeName = "S3-System-name"
|
|
|
|
attrVersionsIgnore = "S3-Versions-ignore"
|
|
|
|
attrSettingsVersioningEnabled = "S3-Settings-Versioning-enabled"
|
|
|
|
versionsDelAttr = "S3-Versions-del"
|
|
|
|
versionsAddAttr = "S3-Versions-add"
|
2022-01-18 09:40:41 +00:00
|
|
|
versionsUnversionedAttr = "S3-Versions-unversioned"
|
2021-08-19 06:55:22 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func newObjectVersions(name string) *objectVersions {
|
|
|
|
return &objectVersions{name: name}
|
|
|
|
}
|
|
|
|
|
2021-09-02 08:40:41 +00:00
|
|
|
func (v *objectVersions) isAddListEmpty() bool {
|
|
|
|
v.sort()
|
|
|
|
return len(v.addList) == 0
|
|
|
|
}
|
|
|
|
|
2021-09-10 06:56:56 +00:00
|
|
|
func (v *objectVersions) appendVersion(oi *data.ObjectInfo) {
|
2021-08-19 06:55:22 +00:00
|
|
|
delVers := splitVersions(oi.Headers[versionsDelAttr])
|
|
|
|
v.objects = append(v.objects, oi)
|
2021-09-02 08:40:41 +00:00
|
|
|
|
2021-08-19 06:55:22 +00:00
|
|
|
for _, del := range delVers {
|
|
|
|
if !contains(v.delList, del) {
|
|
|
|
v.delList = append(v.delList, del)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
v.isSorted = false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *objectVersions) sort() {
|
|
|
|
if !v.isSorted {
|
|
|
|
sort.Slice(v.objects, func(i, j int) bool {
|
2021-09-02 08:40:41 +00:00
|
|
|
o1, o2 := v.objects[i], v.objects[j]
|
|
|
|
if o1.CreationEpoch == o2.CreationEpoch {
|
|
|
|
l1, l2 := o1.Headers[versionsAddAttr], o2.Headers[versionsAddAttr]
|
|
|
|
if len(l1) != len(l2) {
|
|
|
|
if strings.HasPrefix(l1, l2) {
|
|
|
|
return false
|
|
|
|
} else if strings.HasPrefix(l2, l1) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return o1.Version() < o2.Version()
|
|
|
|
}
|
|
|
|
return o1.CreationEpoch < o2.CreationEpoch
|
2021-08-19 06:55:22 +00:00
|
|
|
})
|
2021-09-02 08:40:41 +00:00
|
|
|
|
|
|
|
v.formAddList()
|
2021-08-19 06:55:22 +00:00
|
|
|
v.isSorted = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-02 08:40:41 +00:00
|
|
|
func (v *objectVersions) formAddList() {
|
|
|
|
for i := 0; i < len(v.objects); i++ {
|
|
|
|
var conflicts [][]string
|
|
|
|
for { // forming conflicts set (objects with the same creation epoch)
|
|
|
|
addVers := append(splitVersions(v.objects[i].Headers[versionsAddAttr]), v.objects[i].Version())
|
|
|
|
conflicts = append(conflicts, addVers)
|
|
|
|
if i == len(v.objects)-1 || v.objects[i].CreationEpoch != v.objects[i+1].CreationEpoch ||
|
|
|
|
containsVersions(v.objects[i+1], addVers) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(conflicts) == 1 {
|
|
|
|
v.addList = addIfNotContains(v.addList, conflicts[0])
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
commonVersions, prevConflictedVersions, conflictedVersions := mergeVersionsConflicts(conflicts)
|
|
|
|
v.addList = commonVersions
|
|
|
|
v.addList = addIfNotContains(v.addList, prevConflictedVersions)
|
|
|
|
v.addList = addIfNotContains(v.addList, conflictedVersions)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-10 06:56:56 +00:00
|
|
|
func containsVersions(obj *data.ObjectInfo, versions []string) bool {
|
2021-09-02 08:40:41 +00:00
|
|
|
header := obj.Headers[versionsAddAttr]
|
|
|
|
for _, version := range versions {
|
|
|
|
if !strings.Contains(header, version) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
func addIfNotContains(list1, list2 []string) []string {
|
|
|
|
for _, add := range list2 {
|
|
|
|
if !contains(list1, add) {
|
|
|
|
list1 = append(list1, add)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return list1
|
|
|
|
}
|
|
|
|
|
|
|
|
func mergeVersionsConflicts(conflicts [][]string) ([]string, []string, []string) {
|
|
|
|
var currentVersions []string
|
|
|
|
var prevVersions []string
|
|
|
|
minLength := math.MaxInt32
|
|
|
|
for _, conflicted := range conflicts {
|
|
|
|
if len(conflicted)-1 < minLength {
|
|
|
|
minLength = len(conflicted) - 1
|
|
|
|
}
|
2022-02-08 16:54:04 +00:00
|
|
|
// last := conflicted[len(conflicted)-1]
|
|
|
|
// conflicts[j] = conflicted[:len(conflicted)-1]
|
|
|
|
// currentVersions = append(currentVersions, last)
|
2021-09-02 08:40:41 +00:00
|
|
|
}
|
|
|
|
var commonAddedVersions []string
|
|
|
|
diffIndex := 0
|
|
|
|
LOOP:
|
|
|
|
for k := 0; k < minLength; k++ {
|
|
|
|
candidate := conflicts[0][k]
|
|
|
|
for _, conflicted := range conflicts {
|
|
|
|
if conflicted[k] != candidate {
|
|
|
|
diffIndex = k
|
|
|
|
break LOOP
|
|
|
|
}
|
|
|
|
}
|
|
|
|
commonAddedVersions = append(commonAddedVersions, candidate)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, conflicted := range conflicts {
|
|
|
|
for j := diffIndex; j < len(conflicted); j++ {
|
|
|
|
prevVersions = append(prevVersions, conflicted[j])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.Strings(prevVersions)
|
|
|
|
sort.Strings(currentVersions)
|
|
|
|
return commonAddedVersions, prevVersions, currentVersions
|
|
|
|
}
|
|
|
|
|
2021-09-21 13:08:06 +00:00
|
|
|
func (v *objectVersions) isEmpty() bool {
|
|
|
|
return v == nil || len(v.objects) == 0
|
|
|
|
}
|
|
|
|
|
2022-01-18 15:28:46 +00:00
|
|
|
func (v *objectVersions) unversioned() []*data.ObjectInfo {
|
|
|
|
if len(v.objects) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
existedVersions := v.existedVersions()
|
|
|
|
res := make([]*data.ObjectInfo, 0, len(v.objects))
|
|
|
|
|
|
|
|
for _, version := range v.objects {
|
|
|
|
if contains(existedVersions, version.Version()) && version.Headers[versionsUnversionedAttr] == "true" {
|
|
|
|
res = append(res, version)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
2022-01-18 09:40:41 +00:00
|
|
|
func (v *objectVersions) getLast(opts ...VersionOption) *data.ObjectInfo {
|
2021-09-21 13:08:06 +00:00
|
|
|
if v.isEmpty() {
|
2021-08-19 06:55:22 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-01-18 09:40:41 +00:00
|
|
|
options := formVersionOptions(opts...)
|
|
|
|
|
2021-08-19 06:55:22 +00:00
|
|
|
v.sort()
|
2021-09-02 08:40:41 +00:00
|
|
|
existedVersions := v.existedVersions()
|
2021-08-19 06:55:22 +00:00
|
|
|
for i := len(v.objects) - 1; i >= 0; i-- {
|
|
|
|
if contains(existedVersions, v.objects[i].Version()) {
|
2021-11-25 15:08:02 +00:00
|
|
|
delMarkHeader := v.objects[i].Headers[VersionsDeleteMarkAttr]
|
2021-08-19 06:55:22 +00:00
|
|
|
if delMarkHeader == "" {
|
2022-01-18 09:40:41 +00:00
|
|
|
if options.unversioned && v.objects[i].Headers[versionsUnversionedAttr] != "true" {
|
|
|
|
continue
|
|
|
|
}
|
2021-08-19 06:55:22 +00:00
|
|
|
return v.objects[i]
|
|
|
|
}
|
2021-11-25 15:08:02 +00:00
|
|
|
if delMarkHeader == DelMarkFullObject {
|
2021-08-19 06:55:22 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-09-02 08:40:41 +00:00
|
|
|
func (v *objectVersions) existedVersions() []string {
|
|
|
|
v.sort()
|
|
|
|
var res []string
|
|
|
|
for _, add := range v.addList {
|
|
|
|
if !contains(v.delList, add) {
|
|
|
|
res = append(res, add)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
2021-09-10 06:56:56 +00:00
|
|
|
func (v *objectVersions) getFiltered(reverse bool) []*data.ObjectInfo {
|
2021-08-19 06:55:22 +00:00
|
|
|
if len(v.objects) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
v.sort()
|
2021-09-02 08:40:41 +00:00
|
|
|
existedVersions := v.existedVersions()
|
2021-09-10 06:56:56 +00:00
|
|
|
res := make([]*data.ObjectInfo, 0, len(v.objects))
|
2021-08-19 06:55:22 +00:00
|
|
|
|
|
|
|
for _, version := range v.objects {
|
2021-11-25 15:08:02 +00:00
|
|
|
delMark := version.Headers[VersionsDeleteMarkAttr]
|
|
|
|
if contains(existedVersions, version.Version()) && (delMark == DelMarkFullObject || delMark == "") {
|
2021-08-19 06:55:22 +00:00
|
|
|
res = append(res, version)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-02 08:40:41 +00:00
|
|
|
if reverse {
|
|
|
|
for i, j := 0, len(res)-1; i < j; i, j = i+1, j-1 {
|
|
|
|
res[i], res[j] = res[j], res[i]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-19 06:55:22 +00:00
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *objectVersions) getAddHeader() string {
|
2021-09-02 08:40:41 +00:00
|
|
|
v.sort()
|
2021-08-19 06:55:22 +00:00
|
|
|
return strings.Join(v.addList, ",")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *objectVersions) getDelHeader() string {
|
|
|
|
return strings.Join(v.delList, ",")
|
|
|
|
}
|
|
|
|
|
2022-02-08 16:54:04 +00:00
|
|
|
func (v *objectVersions) getVersion(oid *oid.ID) *data.ObjectInfo {
|
2021-08-17 11:23:49 +00:00
|
|
|
for _, version := range v.objects {
|
2021-09-07 06:17:12 +00:00
|
|
|
if version.Version() == oid.String() {
|
|
|
|
if contains(v.delList, oid.String()) {
|
|
|
|
return nil
|
|
|
|
}
|
2021-08-17 11:23:49 +00:00
|
|
|
return version
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2022-02-28 08:02:05 +00:00
|
|
|
func (n *layer) PutBucketVersioning(ctx context.Context, p *PutSettingsParams) (*data.ObjectInfo, error) {
|
|
|
|
bktInfo, err := n.GetBucketInfo(ctx, p.BktInfo.Name)
|
2021-08-19 06:55:22 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-08-19 12:33:02 +00:00
|
|
|
metadata := map[string]string{
|
|
|
|
attrSettingsVersioningEnabled: strconv.FormatBool(p.Settings.VersioningEnabled),
|
2021-08-19 06:55:22 +00:00
|
|
|
}
|
|
|
|
|
2021-10-04 14:30:38 +00:00
|
|
|
s := &PutSystemObjectParams{
|
|
|
|
BktInfo: bktInfo,
|
|
|
|
ObjName: bktInfo.SettingsObjectName(),
|
|
|
|
Metadata: metadata,
|
|
|
|
Prefix: "",
|
2021-10-13 18:50:02 +00:00
|
|
|
Reader: nil,
|
2021-10-04 14:30:38 +00:00
|
|
|
}
|
|
|
|
|
2021-10-13 18:50:02 +00:00
|
|
|
return n.putSystemObject(ctx, s)
|
2021-08-19 06:55:22 +00:00
|
|
|
}
|
|
|
|
|
2022-02-28 08:02:05 +00:00
|
|
|
func (n *layer) GetBucketVersioning(ctx context.Context, bucketName string) (*data.BucketSettings, error) {
|
2021-08-19 06:55:22 +00:00
|
|
|
bktInfo, err := n.GetBucketInfo(ctx, bucketName)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return n.getBucketSettings(ctx, bktInfo)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (n *layer) ListObjectVersions(ctx context.Context, p *ListObjectVersionsParams) (*ListObjectVersionsInfo, error) {
|
2021-09-01 16:10:31 +00:00
|
|
|
var (
|
|
|
|
versions map[string]*objectVersions
|
2021-09-10 06:56:56 +00:00
|
|
|
allObjects = make([]*data.ObjectInfo, 0, p.MaxKeys)
|
2021-09-01 16:10:31 +00:00
|
|
|
res = &ListObjectVersionsInfo{}
|
2021-09-02 08:40:41 +00:00
|
|
|
reverse = true
|
2021-09-01 16:10:31 +00:00
|
|
|
)
|
2021-08-19 06:55:22 +00:00
|
|
|
|
|
|
|
bkt, err := n.GetBucketInfo(ctx, p.Bucket)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-09-01 16:10:31 +00:00
|
|
|
if versions, err = n.getAllObjectsVersions(ctx, bkt, p.Prefix, p.Delimiter); err != nil {
|
2021-08-19 06:55:22 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-09-01 16:10:31 +00:00
|
|
|
sortedNames := make([]string, 0, len(versions))
|
|
|
|
for k := range versions {
|
|
|
|
sortedNames = append(sortedNames, k)
|
|
|
|
}
|
|
|
|
sort.Strings(sortedNames)
|
2021-08-19 06:55:22 +00:00
|
|
|
|
2021-09-01 16:10:31 +00:00
|
|
|
for _, name := range sortedNames {
|
2021-09-02 08:40:41 +00:00
|
|
|
allObjects = append(allObjects, versions[name].getFiltered(reverse)...)
|
2021-08-19 06:55:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for i, obj := range allObjects {
|
|
|
|
if obj.Name >= p.KeyMarker && obj.Version() >= p.VersionIDMarker {
|
|
|
|
allObjects = allObjects[i:]
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
res.CommonPrefixes, allObjects = triageObjects(allObjects)
|
|
|
|
|
|
|
|
if len(allObjects) > p.MaxKeys {
|
|
|
|
res.IsTruncated = true
|
|
|
|
res.NextKeyMarker = allObjects[p.MaxKeys].Name
|
|
|
|
res.NextVersionIDMarker = allObjects[p.MaxKeys].Version()
|
|
|
|
|
|
|
|
allObjects = allObjects[:p.MaxKeys]
|
|
|
|
res.KeyMarker = allObjects[p.MaxKeys-1].Name
|
|
|
|
res.VersionIDMarker = allObjects[p.MaxKeys-1].Version()
|
|
|
|
}
|
|
|
|
|
|
|
|
objects := make([]*ObjectVersionInfo, len(allObjects))
|
|
|
|
for i, obj := range allObjects {
|
|
|
|
objects[i] = &ObjectVersionInfo{Object: obj}
|
2021-09-02 08:40:41 +00:00
|
|
|
if i == 0 || allObjects[i-1].Name != obj.Name {
|
2021-08-19 06:55:22 +00:00
|
|
|
objects[i].IsLatest = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
res.Version, res.DeleteMarker = triageVersions(objects)
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func triageVersions(objVersions []*ObjectVersionInfo) ([]*ObjectVersionInfo, []*ObjectVersionInfo) {
|
|
|
|
if len(objVersions) == 0 {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var resVersion []*ObjectVersionInfo
|
|
|
|
var resDelMarkVersions []*ObjectVersionInfo
|
|
|
|
|
|
|
|
for _, version := range objVersions {
|
2021-11-25 15:08:02 +00:00
|
|
|
if version.Object.Headers[VersionsDeleteMarkAttr] == DelMarkFullObject {
|
2021-08-19 06:55:22 +00:00
|
|
|
resDelMarkVersions = append(resDelMarkVersions, version)
|
|
|
|
} else {
|
|
|
|
resVersion = append(resVersion, version)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return resVersion, resDelMarkVersions
|
|
|
|
}
|
|
|
|
|
|
|
|
func contains(list []string, elem string) bool {
|
|
|
|
for _, item := range list {
|
|
|
|
if elem == item {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2022-02-28 08:02:05 +00:00
|
|
|
func (n *layer) getBucketSettings(ctx context.Context, bktInfo *data.BucketInfo) (*data.BucketSettings, error) {
|
2021-10-04 14:30:38 +00:00
|
|
|
objInfo, err := n.headSystemObject(ctx, bktInfo, bktInfo.SettingsObjectName())
|
2021-08-19 06:55:22 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return objectInfoToBucketSettings(objInfo), nil
|
|
|
|
}
|
|
|
|
|
2022-02-28 08:02:05 +00:00
|
|
|
func objectInfoToBucketSettings(info *data.ObjectInfo) *data.BucketSettings {
|
|
|
|
res := &data.BucketSettings{}
|
2021-08-19 06:55:22 +00:00
|
|
|
|
|
|
|
enabled, ok := info.Headers[attrSettingsVersioningEnabled]
|
|
|
|
if ok {
|
|
|
|
if parsed, err := strconv.ParseBool(enabled); err == nil {
|
|
|
|
res.VersioningEnabled = parsed
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
2021-09-10 06:56:56 +00:00
|
|
|
func (n *layer) checkVersionsExist(ctx context.Context, bkt *data.BucketInfo, obj *VersionedObject) (*data.ObjectInfo, error) {
|
2021-08-19 06:55:22 +00:00
|
|
|
versions, err := n.headVersions(ctx, bkt, obj.Name)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-01-18 09:40:41 +00:00
|
|
|
|
|
|
|
var version *data.ObjectInfo
|
|
|
|
if obj.VersionID == unversionedObjectVersionID {
|
|
|
|
version = versions.getLast(FromUnversioned())
|
|
|
|
} else {
|
2022-02-08 16:54:04 +00:00
|
|
|
id := oid.NewID()
|
2022-01-18 09:40:41 +00:00
|
|
|
if err := id.Parse(obj.VersionID); err != nil {
|
|
|
|
return nil, errors.GetAPIError(errors.ErrInvalidVersion)
|
|
|
|
}
|
|
|
|
version = versions.getVersion(id)
|
|
|
|
}
|
|
|
|
|
2021-09-07 06:17:12 +00:00
|
|
|
if version == nil {
|
2021-08-19 06:55:22 +00:00
|
|
|
return nil, errors.GetAPIError(errors.ErrInvalidVersion)
|
|
|
|
}
|
|
|
|
|
2021-09-07 06:17:12 +00:00
|
|
|
return version, nil
|
2021-08-19 06:55:22 +00:00
|
|
|
}
|